大气散射(六)和大气球体相交

356 阅读4分钟

一、和大气球体相交

如前所述,我们计算大气层穿过的分段的光学深度的唯一方法是通过数值积分。这意味着将区间分成长度为 ds 的较小段,并假设每个段的密度是恒定的,然后计算每个段的光学深度。

image.png 如上图所示,AB\overline{AB} 的光学深度使用 4 个样本计算,每个样本仅考虑段本身中心的密度。

显然,第一步是找到点 A 和点 B。如果我们假设我们渲染的是一个球体,Unity 将尝试渲染其表面。屏幕上的每个像素对应于球体上的一个点。在下面的图中,该点称为 O,表示原点。在表面着色器中,O 对应于 Input 结构中的 worldPos 变量。着色器的处理到此为止;我们唯一可用的信息是 O、指示视图射线方向的方向 D,以及以 C 为中心、半径为 R 的大气球体。挑战在于计算 A 和 B。最快的方法是使用几何方法,将问题简化为找到大气球体和来自摄像机的视图射线之间的交点。

image.png 首先,我们应该注意到 O、A 和 B 都位于视图射线上。这意味着我们可以将它们的位置表示为不是三维空间中的点,而是从原点开始的视图射线上的距离。虽然 A 是实际的点(在着色器中表示为 float3),但 AO 是其到原点 O 的距离(作为浮点数)。A 和 AO 都是指示同一点的两种有效方式,有:

A=O+AODA = O + \overline{AO}\,D

B=O+BODB = O + \overline{BO}\,D

其中上方的标记 XY\overline{XY} 表示任意点 X 和 Y 之间的段的长度。

出于效率原因,在着色器代码中,我们将使用 AO 和 BO,并从 OT 计算它们:

AO=OTAT\overline{AO} = \overline{OT} - \overline{AT}

BO=OT+BT\overline{BO} = \overline{OT} + \overline{BT}

我们还应该注意,段 AT\overline{AT}BT\overline{BT} 的长度相同。现在,我们需要找到交点的是计算 AO\overline{AO}AT\overline{AT}

OT\overline{OT} 最容易计算。如果我们观察上图,可以看到 OT\overline{OT} 是向量 CO 投影到视图射线上。从数学上讲,这种投影可以使用点积完成。如果你熟悉着色器,可能会知道点积是衡量两个方向“对齐程度”的一种方法。当它应用于两个向量并且第二个向量的长度为单位时,它变成了一个投影运算符:

OT=(CO)D\overline{OT} = \left(C-O\right) \cdot D

应该注意,(CO)\left(C-O\right) 是一个三维向量,而不是 C 和 O 之间的段的长度。

接下来,我们需要计算段 AT\overline{AT} 的长度。这可以使用勾股定理在三角形 ACT\overset{\triangle}{ACT} 上计算。事实上,有:

R2=AT2+CTR^2 = \overline{AT}^2 + \overline{CT}

这意味着:

AT=R2CT\overline{AT} = \sqrt{R^2 - \overline{CT}}

CT\overline{CT} 的长度仍然未知。然而,它可以再次应用勾股定理在三角形 OCT\overset{\triangle}{OCT} 上计算:

CO2=OT2+CT2\overline{CO}^2 = \overline{OT}^2 + \overline{CT}^2

CT=CO2OT2 \overline{CT} = \sqrt{\overline{CO}^2 - \overline{OT}^2}

现在我们已经获得了所有所需的量。总结一下:

OT=(CO)D\overline{OT} = \left( C - O \right) \cdot D

CT=CO2OT2 \overline{CT} = \sqrt{\overline{CO}^2 - \overline{OT}^2}

AT=R2CT2 \overline{AT} = \sqrt{R^2 - \overline{CT}^2}

AO=OTAT\overline{AO} = \overline{OT} - \overline{AT}

BO=OT+AT\overline{BO} = \overline{OT} + \overline{AT}

这组方程包含平方根。它们仅在非负数上定义。如果 R2>CT2R^2 > \overline{CT}^2,那么就没有解,这意味着视图射线与球体不相交。

我们可以将此转换为以下 Cg 函数:

bool rayIntersect
(
    // 射线
    float3 O, // 原点
    float3 D, // 方向
    // 球体
    float3 C, // 中心
    float R,    // 半径
    out float AO, // 第一个交点的时间
    out float BO  // 第二个交点的时间
)
{
    float3 L = C - O;
    float DT = dot (L, D);
    float R2 = R * R;
    float CT2 = dot(L,L) - DT*DT;
    
    // 交点在圆外部
    if (CT2 > R2)
        return false;
    float AT = sqrt(R2 - CT2);
    float BT = AT;
    AO = DT - AT;
    BO = DT + BT;
    return true;
}

这不是一个单一的值,而是三个要返回的值:AO\overline{AO}BO\overline{BO} 以及是否存在交点。这两个段的长度使用 out 关键字返回,该关键字使函数对这些参数的任何更改在其终止后保持持久性。

二、行星碰撞

需要额外考虑的问题是,某些视线射线会击中行星,因此它们穿过大气层的旅程会提前终止。一种方法是重新审查上面提到的推导。

一种更简单但效率较低的方法是运行rayIntersect两次,然后根据需要调整结束点。

image.png 这段代码的意思是:

// 与大气层球体的交点
float tA;    // 大气层进入点(worldPos + V * tA)
float tB;    // 大气层退出点(worldPos + V * tB)
if (!rayIntersect(O, D, _PlanetCentre, _AtmosphereRadius, tA, tB))
    return fixed4(0,0,0,0); // 视线射线朝向深空
// 射线是否穿过行星核心?
float pA, pB;
if (rayIntersect(O, D, _PlanetCentre, _PlanetRadius, pA, pB))
    tB = pA;