一、和大气球体相交
如前所述,我们计算大气层穿过的分段的光学深度的唯一方法是通过数值积分。这意味着将区间分成长度为 ds 的较小段,并假设每个段的密度是恒定的,然后计算每个段的光学深度。
如上图所示, 的光学深度使用 4 个样本计算,每个样本仅考虑段本身中心的密度。
显然,第一步是找到点 A 和点 B。如果我们假设我们渲染的是一个球体,Unity 将尝试渲染其表面。屏幕上的每个像素对应于球体上的一个点。在下面的图中,该点称为 O,表示原点。在表面着色器中,O 对应于 Input 结构中的 worldPos 变量。着色器的处理到此为止;我们唯一可用的信息是 O、指示视图射线方向的方向 D,以及以 C 为中心、半径为 R 的大气球体。挑战在于计算 A 和 B。最快的方法是使用几何方法,将问题简化为找到大气球体和来自摄像机的视图射线之间的交点。
首先,我们应该注意到 O、A 和 B 都位于视图射线上。这意味着我们可以将它们的位置表示为不是三维空间中的点,而是从原点开始的视图射线上的距离。虽然 A 是实际的点(在着色器中表示为 float3),但 AO 是其到原点 O 的距离(作为浮点数)。A 和 AO 都是指示同一点的两种有效方式,有:
其中上方的标记 表示任意点 X 和 Y 之间的段的长度。
出于效率原因,在着色器代码中,我们将使用 AO 和 BO,并从 OT 计算它们:
我们还应该注意,段 和 的长度相同。现在,我们需要找到交点的是计算 和 。
段 最容易计算。如果我们观察上图,可以看到 是向量 CO 投影到视图射线上。从数学上讲,这种投影可以使用点积完成。如果你熟悉着色器,可能会知道点积是衡量两个方向“对齐程度”的一种方法。当它应用于两个向量并且第二个向量的长度为单位时,它变成了一个投影运算符:
应该注意, 是一个三维向量,而不是 C 和 O 之间的段的长度。
接下来,我们需要计算段 的长度。这可以使用勾股定理在三角形 上计算。事实上,有:
这意味着:
段 的长度仍然未知。然而,它可以再次应用勾股定理在三角形 上计算:
现在我们已经获得了所有所需的量。总结一下:
这组方程包含平方根。它们仅在非负数上定义。如果 ,那么就没有解,这意味着视图射线与球体不相交。
我们可以将此转换为以下 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;
}
这不是一个单一的值,而是三个要返回的值:、 以及是否存在交点。这两个段的长度使用 out 关键字返回,该关键字使函数对这些参数的任何更改在其终止后保持持久性。
二、行星碰撞
需要额外考虑的问题是,某些视线射线会击中行星,因此它们穿过大气层的旅程会提前终止。一种方法是重新审查上面提到的推导。
一种更简单但效率较低的方法是运行rayIntersect两次,然后根据需要调整结束点。
这段代码的意思是:
// 与大气层球体的交点
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;