← ALL ARTICLES
VFX与模拟4 MIN READ

Froxel体积雾

全库目录221
本文目录8

沿视线逐步计算雾,每个表面都要重复积分。Froxel 方案把视锥切成三维网格,先算每段散射,再沿深度保存累计结果。这样,墙面只需按自己的深度查一次纹理,就能得到相机到墙之间的雾;代价则转移到网格精度、历史重投影与三维资源访问上。

Froxel 体积雾

局部体积雾的主流实时方案是把视锥离散成体素网格。由于这些体素是视锥形而非立方体(沿深度方向逐渐变大),通常称为 froxel(frustum voxel)。

预算示例可取 160×90×64160\times90\times64。深度方向一般按指数分布,让近处体素更密——与透视投影的采样密度需求一致。

整个方案分两步:

  1. 散射计算:每个 froxel 求一次光照散射与消光,结果写入三维纹理;
  2. 积分:沿深度方向累计散射与透射,得到从相机到指定深度的介质结果。散射需乘之前各段的透射率,通常不能直接做普通数值相加。

第二步是关键。有了透射加权的前缀结果,着色时只需按像素深度采样一次三维纹理就能拿到总散射量,复杂度 O(1)O(1),不必沿视线逐步进行光线步进。

透射加权的前缀合成

每段保存局部散射 segmentRadiance 与段透射 segmentT。输入为沿同一视线排列的切片,输出累计散射和透射。两者可以写入同一个 RGBA 资源。

hlsl
float3 accumulatedRadiance = 0.0;
float accumulatedT = 1.0;
for (uint slice = 0; slice < sliceCount; ++slice)
{
    float4 segment = LoadSegment(uint3(pixel, slice));
    accumulatedRadiance += accumulatedT * segment.rgb;
    accumulatedT *= segment.a;
    StorePrefix(uint3(pixel, slice),
        float4(accumulatedRadiance, accumulatedT));
}

设第一段为(L=2,T=0.5),第二段为(L=4,T=0.25),前缀得到(4,0.125)。背景颜色8经这两段后是4+0.125*8=5;直接把散射相加会误得7。交换顺序则得到(4.5,0.125),说明并行扫描可以重新分组,却必须保持由近到远的顺序。

切片长度需要参与局部段积分。合成不透明表面时使用 radiance + transmittance * surfaceColor。并行前缀算法可利用段合成的结合律,但线程局部的多切片循环还需处理分段前缀,不能只拼接各线程的局部累积。

沿深度复用

一个线程可以负责同一 XY 列中的一段连续深度切片。第 z 组的范围为 [z*slicesPerThread, min((z+1)*slicesPerThread, depthCount)),派发边缘先检查 XY 索引。已知前一格的远端射线距离,即可复用为后一格的近端距离,再由两者之差计算本段长度。

这一步写出每格的局部散射与透射率。每段线程组只知道自己的起点,跨组累计需要后面的前缀积分,不能把组内从一开始累乘的透射率当作相机到该点的结果。连续处理减少坐标重建工作,也降低 Z 方向并行度;是否更快需在同一网格规模下计时。

时域累积

逐帧独立计算的体积雾噪点明显,提高采样数的代价又太高。标准做法是时域累积:每帧用抖动偏移采样,再与历史结果混合。

典型的混合权重偏向历史(例如当前帧仅占七分之一),用多帧摊薄噪声:

hlsl
float3 reprojUVW = CalcReprojectUVW(reprojPosWS, _PrevCameraPositionWS,
                                    _PrevViewProjectionMatrix, ...);
if (all(reprojUVW >= 0.0) && all(reprojUVW <= 1.0))
{
    real3 history = SAMPLE_TEXTURE3D_LOD(_PrevVolumetricFog, s_linear, reprojUVW, 0).rgb;
    current = lerp(current, history, TAA_HISTORY_BLEND_WEIGHT);
}

一个关键细节:本例的历史网格以未抖动的 froxel 中心定位,散射估计再使用抖动位置。若历史坐标没有按前帧网格的抖动约定转换,查找位置会随抖动摆动,表现为持续闪烁——这与抖动本身要消除的噪声是两回事,且更难察觉成因。

实现上需要双缓冲三维纹理,每帧交换当前与历史。超出历史范围(相机快速移动、首帧)时要回退到纯当前帧结果。

散射与积分融合

散射与积分分成两个 Kernel 的代价是:一整张三维纹理的写入加读取、一次额外派发、一个 UAV 屏障。

若一个线程完整负责一条 XY 视线的全部深度切片,可以边计算局部散射边合成累计结果。若不同线程只负责部分切片,仍需跨段前缀处理,不能仅加局部累加器。融合可能降低中间纹理访问,也会减少 Z 方向并行度。

是否划算取决于三维纹理的带宽成本与并行度损失的相对大小,需要在目标平台实测。这类“多趟换并行度”的权衡在 Compute 管线中很常见。

对两段 (L1,T1)、(L2,T2),按近到远合成为 (L1+T1*L2,T1*T2)。交换两段会改变颜色贡献,结合三段却与分组方式无关,因此可以做保序并行扫描。示例中局部散射保存的是本段已经积分的辐亮度;若存的是每米源项,必须先乘相应积分因子,不能直接进入前缀合成。

验证方法

  • 固定同一视线与非负消光,检查累计透射不增。
  • 对比直接步进与三维积分结果,检查深度切片插值。
  • 相机切换和灯光突变时拒绝过期历史。
  • 融合 kernel 前后对比整帧耗时、寄存器压力和输出误差。

相关主题

参考资料