最终贡献

场景最终的雾效由远景面片与近处体积雾混合贡献。

(无贡献)

(远景面片雾)

(面片+体积雾)

(200m处密度预积分的最终结果)

质量与性能设计总结

该体积雾管线综合使用了以下策略:

策略 作用
摄像机视锥 Froxel 只计算当前可见方向,避免构建大范围世界体素
256×144×128 低分辨率网格 大幅降低介质与灯光计算量
对数 Z 分布 把深度精度集中到相机附近
世界空间密度查询 网格随摄像机移动,雾仍锁定在地形和局部体积
蓝噪声与子 Froxel 抖动 减少规则采样条纹
上一帧重投影 稳定低分辨率体积、降低闪烁
空间滤波 缓解 Froxel 块状感和噪声
沿 Z 前向积分 将逐像素 ray march 转换为一次 3D 查表
180–200 m 远雾交叉淡化 以低成本延伸到摄像机远裁剪范围

摘要

本文基于 RenderDoc 捕获,分析 __tex_volumetric_fog_3d_image 的生成、空间覆盖、贴地消光、光照染色、沿 Z 积分以及最终场景合成流程。

该纹理不是离线烘焙资产,而是每帧动态生成的、绑定当前摄像机视锥的 Froxel(frustum voxel)体积。它覆盖相机当前视场内沿视线约 0–200 m 的范围,XY 分辨率为 256×144,Z 方向使用 128 个对数分布切片。EID 2937 计算每个 Froxel 的局部散射和光学厚度,并与上一帧历史重投影结果混合;EID 2943 对局部结果进行空间滤波和从近到远的前向积分;EID 11086 根据屏幕像素与场景深度对积分体积采样,将雾合成到场景颜色中。

核心结论如下:

  • 体积网格随摄像机移动和旋转,但网格内容通过世界坐标、地形高度和世界空间 Fog Volume 计算,因此雾在视觉上锁定于场景。
  • 高质量体积雾覆盖约 0–200 m;180–200 m 与远雾交叉淡化,之后由 __tex_far_fog 接管。
  • 贴地消光由“Froxel 世界高度减地形高度”产生,当前高度范围约 5 m,采用幂函数向上衰减。
  • 绿色主要来自 near_ground_fog_color、全局 fog_color 和局部 Fog Volume 颜色,随后再乘环境光、太阳光及局部灯光。
  • 最终 3D 纹理的 RGB 是相机到各深度的累计入射散射,A 是累计光学厚度,并非普通透明度。

1. 分析范围与方法

1.1 捕获信息

项目
捕获文件 D:\captures\HD2_Fog\01.rdc
图形 API D3D11
输出分辨率 1920×1080
体积纹理分辨率 256×144×128
主要生成事件 EID 2937、EID 2943
主要合成事件 EID 11086

1.2 关键资源

资源 ResourceId 格式/尺寸 用途
volumetric_fog_3d_image_tmp 409309 RGBA16F,256×144×128 当前帧局部散射与局部光学厚度
volumetric_fog_3d_image 409313 RGBA16F,256×144×128 空间滤波并沿 Z 积分后的最终体积
volumetric_fog_3d_image_history 409317 RGBA16F,256×144×128 上一帧历史输入及下一帧历史输出
volumetric_fog_3d_reprojection_history 409321 R8,256×144×128 历史重投影有效性/置信度
EID 11086 输出 UAV 408694 RGBA16F,1920×1080 合成大气、云和体积雾后的场景颜色

整体生产链如下:

2. 体积覆盖范围

2.1 不是世界空间固定盒子

__tex_volumetric_fog_3d_image 的 XYZ 并不直接对应世界坐标 XYZ:

  • X/Y 对应当前屏幕中的视线方向。
  • Z 对应沿该视线到摄像机的距离。

EID 2937 从 vThreadID.xyz 生成 Froxel 坐标,用反投影矩阵重建视线,再得到世界位置:

1
2
3
float2 uv = (froxelXY + 0.5 + temporalJitter) / float2(256, 144);
float3 viewRay = ReconstructWorldRay(uv, inv_view_proj);
float3 worldPosition = cameraPosition + viewRay * FroxelDistance(froxelZ);

因此覆盖区域只位于摄像机当前视场内,而不是摄像机周围 360° 的球体或立方体。摄像机移动或旋转时网格也随之更新;但密度、颜色和光照都使用 worldPosition 查询,所以雾不会像一个摄像机盒子那样在地面上滑动。

2.2 XY 分辨率

体积 XY 为 256×144,画面为 1920×1080:

1
2
1920 / 256 = 7.5 pixels
1080 / 144 = 7.5 pixels

一个 XY Froxel 大约覆盖 7.5×7.5 个输出像素。Shader 还依据帧号和蓝噪声对子 Froxel 位置进行抖动,并用历史重投影降低低分辨率网格造成的闪烁和阶梯感。

2.3 Z 使用对数分布

EID 11086 使用以下关系将射线距离映射到 3D 纹理 Z:

1
2
float z = log2(1.0 + 0.145 * distance) * 0.203795;
z = clamp(z, 0.0, 1.0 - 0.5 / 128.0);

逆映射为:

1
float distance = 6.896552 * (pow(30.0, z) - 1.0);

由此得到:

归一化 Z 射线距离
0 0 m
0.25 约 9.2 m
0.5 约 30.9 m
0.75 约 81.5 m
1 200 m

对数分布把更多切片分配给相机附近,使近处角色、地面和局部灯光获得更高的深度精度。

2.4 为什么可以将单位解释为米

RenderDoc 本身只记录无量纲浮点数,但同一 Shader 将摄像机高度与常数 6,360,500 直接相加,用于行星大气几何。该数与 6,360.5 km 的类地行星半径一致,因此该渲染系统采用约 1 世界单位等于 1 m 的比例。

其他尺度也与此一致:

参数 数值 技术解释
相机近裁剪面 0.05 5 cm
相机远裁剪面 5000 5 km
摄像机高度坐标 15.64 约 15.6 m
贴地雾高度 5 5 m
高质量体积范围 200 200 m

严格地说,单位没有存储为元数据;但行星半径和摄像机坐标直接参与同一运算,使“1 单位约等于 1 m”具有很高置信度。

2.5 与远雾的衔接

EID 11086 从 180 m 开始混合 __tex_far_fog

1
float farBlend = saturate((distance - 180.0) * 0.05);

这意味着:

  • 0–180 m:主要使用 Froxel 体积雾。
  • 180–200 m:Froxel 体积雾与远雾交叉淡化。
  • 200 m 以后:主要由远雾覆盖。

3. 体积纹理是否每帧生成

它不是离线烘焙纹理。当前单帧捕获中的资源顺序为:

  • EID 2937:读取 RID 409317(上一帧 History)
  • EID 2937:写入 RID 409309(本帧局部结果)
  • EID 2943:读取 RID 409309
  • EID 2943:写入 RID 409313(本帧最终积分体积)
  • EID 2943:写入 RID 409317(供下一帧使用)

EID 2937 同时使用:

  • camera_viewcamera_last_view
  • camera_inv_viewcamera_last_inv_view
  • camera_last_view_projection
  • frame_number = 100609
  • frame_history_invalidation = 0

frame_history_invalidation = 0 表示本帧历史有效。Shader 将当前 Froxel 世界位置投影回上一帧 3D 网格,采样 History 后与当前结果混合:

1
2
3
4
5
6
7
float3 previousUVW = ProjectToPreviousFogGrid(
worldPosition,
camera_last_view_projection
);

float4 history = FogHistory.SampleLevel(historySampler, previousUVW, 0);
float4 localResult = lerp(history, currentResult, currentFrameWeight);

发生镜头切换或历史失效时,frame_history_invalidation 会使当前帧权重变为 1,从而丢弃历史。由此可见,该系统是“每帧计算当前估计 + 上一帧时间累积”,而非静态烘焙。

4. EID 2937:局部介质计算

4.1 输入来源

EID 2937 的 CS 使用 17 个 SRV、2 个 UAV、9 个常量缓冲和 10 个采样器。主要输入包括:

  • 太阳阴影图与云阴影。
  • 蓝噪声和 3D 动态噪声。
  • 地形高度图、地形自发光图。
  • 局部 Fog Volume 的颜色、密度和参数。
  • Clustered lights、灯光阴影矩阵、局部灯光阴影图和 IES 数据。
  • 上一帧体积雾历史。
  • 当前与上一帧摄像机矩阵。

该阶段并不是把场景几何直接体素化,而是对每个视锥 Froxel 重建世界位置,然后查询世界空间中的介质和光照来源。

4.2 贴地消光

Shader 使用 __tex_lighting_heightmap 查询 Froxel 下方地形高度:

1
2
3
4
5
6
7
8
float2 heightUV = worldPosition.xy
* float2(1.0 / 2048.0, -1.0 / 2048.0)
+ 0.5;

float terrainHeight = LightingHeightmap.SampleLevel(..., heightUV, 0).r
* 1000.0 - 500.0;

float heightAboveGround = worldPosition.z - terrainHeight;

当前贴地雾参数为:

1
2
3
4
5
near_ground_fog_height       = 5.0
near_ground_fog_curve = 1.5
near_ground_fog_density = 0.15
near_ground_fog_noise_amount = 0.7
near_ground_fog_color = (0.4784, 0.6235, 0.3647)

高度遮罩等价于:

1
2
3
4
float heightMask = pow(
1.0 - saturate(heightAboveGround / 5.0),
1.5
);
离地高度 高度遮罩
0 m 1
1 m 0.716
2.5 m 0.354
4 m 0.089
≥5 m 0

Shader 允许约 6 m 的地下容差,用于避免高度图精度、Froxel 尺寸和实际地表不完全一致时出现断层。高度遮罩还会乘:

  • 动态 3D 噪声遮罩。
  • 近距离渐入和远距离渐出遮罩。
  • 视线方向调制。
  • Fog Volume 有效性与总雾启用状态。

近似表达为:

1
2
3
4
5
6
7
float nearGroundDensity =
near_ground_fog_density
* heightMask
* animatedNoise
* distanceMask
* directionMask
* validityMask;

其中距离遮罩在相机最近几米内渐入,并在约 100–200 m 渐出,以避免雾贴脸和远端切断。

4.3 局部光学厚度

贴地密度会与局部 Fog Volume、全局大气雾和噪声贡献相加:

1
2
3
4
5
float totalDensity =
localFogVolumeDensity
+ atmosphericDensity
+ nearGroundDensity
+ additionalNoiseDensity;

然后乘当前 Froxel 沿射线方向的段长:

1
float localOpticalDepth = totalDensity * froxelSegmentLength;

volumetric_fog_3d_image_tmp.a 存储的是该段的局部光学厚度,而不是 0–1 Alpha。其透射率需要通过 Beer–Lambert 定律得到:

1
float transmittance = exp(-localOpticalDepth);

5. 雾的颜色与光照

5.1 介质颜色来源

绿色首先来自介质本身,而非单一绿色灯光。

局部 Fog Volume 颜色来自:

1
2
__tex_combined_fog_volumes_color_density
ResourceId::409390

该纹理在当前帧已经呈现明显的橄榄绿色:

贴地雾根据自身密度占总密度的比例,将局部 Fog Volume 颜色混向 near_ground_fog_color

1
2
3
4
5
6
float nearFraction = nearGroundDensity / (totalDensity + 0.0001);
float3 mediumColor = lerp(
localFogVolumeColor,
near_ground_fog_color,
nearFraction
);

随后还会与全局雾色混合:

1
fog_color = (0.1260, 0.5884, 0.5099)

因此当前帧介质颜色由三种绿色来源共同形成:

  1. 局部 Fog Volume 的橄榄绿色。
  2. near_ground_fog_color 的黄绿色。
  3. fog_color 的青绿色。

5.2 环境光

环境光近似为:

1
2
3
float3 ambientLight =
atmospheric_lookup[3].rgb
+ atmosphere_light_color * fog_light_ambient_intensity;

当前值为:

1
2
3
atmospheric_lookup[3]       = (0.0060, 0.0185, 0.0030)
atmosphere_light_color = (1.3493, 1.5655, 1.7070)
fog_light_ambient_intensity = 0.02

因此:

1
ambientLight ≈ (0.0330, 0.0498, 0.0372)

绿色通道仍然最高。环境散射近似为:

1
2
float opacity = 1.0 - exp(-localOpticalDepth);
float3 ambientScattering = mediumColor * ambientLight * opacity;

5.3 太阳光

当前太阳参数为:

1
2
3
4
5
sun_color                = (1.3493, 1.5655, 1.7070)
fog_sun_intensity = 5.0
fog_forwardscatter_phase = 0.7
fog_backscatter_phase = 0.1
fog_backscatter_lerp = 0.5

直接太阳散射近似为:

1
2
3
4
5
6
float3 sunScattering =
mediumColor
* (sun_color * fog_sun_intensity)
* phase(viewRay, sunDirection)
* sunShadow
* opacity;

太阳颜色自身略偏蓝,但与绿色介质相乘后仍由绿色通道主导。例如贴地颜色乘太阳颜色和强度约为:

$$
(0.478, 0.624, 0.365) \times (1.349, 1.566, 1.707) \times 5 \approx (3.23, 4.88, 3.11)
$$

5.4 局部灯光与地形自发光

Shader 后半段遍历 Clustered Volumetric Lights,并应用:

  • 灯光颜色与距离衰减。
  • Spot/面光源方向衰减。
  • IES 分布。
  • 局部灯光阴影图。
  • 体积散射相函数。

局部灯光同样会乘 mediumColor,所以强灯可以产生局部异色亮斑,但不会轻易消除整体绿色基调。

5.5 EID 2937 的输出语义

局部结果可以概括为:

1
2
3
4
5
6
7
8
float3 localScattering =
ambientScattering
+ sunScattering
+ localLightScattering
+ terrainEmissiveScattering;

volumetric_fog_3d_image_tmp.rgb = localScattering;
volumetric_fog_3d_image_tmp.a = localOpticalDepth;

绿色在此阶段已经写入 RGB。EID 2943 不会重新染色,只负责滤波和积分。

6. EID 2943:为什么沿 Z 前向积分

6.1 连续体积渲染方程

距离相机为 D 的表面颜色可写为:

$$
L(D) = L_{\text{surface}}T(0,D) + \int_0^D T(0,s)S(s),\mathrm{d}s
$$

其中:

  • $S(s)$ 是位置 $s$ 的局部入射散射。
  • $T(0,s)$ 是该位置到相机的透射率。
  • $T=e^{-\tau}$。
  • $\tau$ 是累计光学厚度。

6.2 离散 Froxel 积分

对于第 i 个 Z 切片:

1
2
3
4
accumulatedScattering +=
localScattering[i] * exp(-accumulatedOpticalDepth);

accumulatedOpticalDepth += localOpticalDepth[i];

等价的复合运算为:

$$
(S_a,\tau_a)\oplus(S_b,\tau_b) = \left(S_a + e^{-\tau_a}S_b,;\tau_a+\tau_b\right)
$$

远处雾产生的散射在到达相机前必须穿过近处雾,因此要乘近处累计透射率。

EID 2943 会把每个 Z 位置的前缀积分都写入最终 3D 纹理:

  • Z0:相机到切片 0 的累计结果
  • Z1:相机到切片 1 的累计结果
  • Zn:相机到切片 n 的累计结果

因此最终纹理语义为:

  • RGB:相机到该深度的累计入射散射
  • A:相机到该深度的累计光学厚度

6.3 为什么独立为一个 Pass

EID 2937 的局部介质和光照计算可以对所有 Froxel 并行执行,而 Z 积分存在前后切片依赖。将两者拆开可以:

  • 让昂贵的灯光、阴影和密度计算完全并行。
  • 在第二阶段对局部结果进行空间滤波。
  • 每个 XY 列沿 Z 顺序累积。
  • 让后续像素只需一次 3D 采样,而不是每像素遍历最多 128 个切片。
  • 让同一积分纹理被多个后续材质和 Pass 复用。

当前捕获中最终纹理只写入一次,却在之后被读取 89 次,说明预积分结果具有明显的复用价值。

7. EID 11086:最终场景合成

EID 11086 是 8×8×1 线程组的 Compute Dispatch,输出到 1920×1080 RGBA16F UAV。它首先生成屏幕 UV,并根据线性深度重建表面沿视线的实际距离:

1
2
3
float2 uv = (pixel + 0.5) / float2(1920, 1080);
float3 viewRay = ReconstructWorldRay(uv);
float distance = linearDepth / dot(viewRay, cameraForward);

随后映射到体积 Z 并只采样一次:

1
2
float z = log2(1.0 + 0.145 * distance) * 0.203795;
float4 fog = FogVolume.SampleLevel(fogSampler, float3(uv, z), 0);

对于 180 m 以外的距离,Shader 使用当前累计透射率正确拼接远雾:

1
2
3
4
5
float farBlend = saturate((distance - 180.0) * 0.05);
float T = exp(-fog.a);

fog.rgb += farBlend * T * farFog.rgb;
fog.a += farBlend * farFog.a;

基础合成关系为:

1
2
float T = exp(-fog.a);
float3 output = sceneColor * T + fog.rgb;

实际 Shader 还加入了与场景亮度有关的透射补偿,以避免高亮物体在浓雾中衰减过重:

1
2
3
float brightness = 1.0 - exp(-2.0 * luminance(sceneColor));
float effectiveT = T + T * (1.0 - T) * brightness;
float3 output = sceneColor * effectiveT + fog.rgb;

本帧 fog_enabled = 1,因此该结果被实际选用。

8. 输出前后对比

EID 11086 前,同一 UAV 主要包含地表、角色和直接光照:

EID 11086 后,大气、云和体积雾已经完成合成:

需要注意:这组前后对比包含 EID 11086 的全部大气、云和雾贡献,不是对 __tex_volumetric_fog_3d_image 的单独隔离测试。但资源绑定、DXBC 数据流以及生产链已经明确表明,近景和中景贴地绿色散射的主体在 EID 2937/2943 中生成,并在 EID 11086 中通过 fog.rgb 合成。

9. 质量与性能设计总结

该体积雾管线综合使用了以下策略:

策略 作用
摄像机视锥 Froxel 只计算当前可见方向,避免构建大范围世界体素
256×144×128 低分辨率网格 大幅降低介质与灯光计算量
对数 Z 分布 把深度精度集中到相机附近
世界空间密度查询 网格随摄像机移动,雾仍锁定在地形和局部体积
蓝噪声与子 Froxel 抖动 减少规则采样条纹
上一帧重投影 稳定低分辨率体积、降低闪烁
空间滤波 缓解 Froxel 块状感和噪声
沿 Z 前向积分 将逐像素 ray march 转换为一次 3D 查表
180–200 m 远雾交叉淡化 以低成本延伸到摄像机远裁剪范围

从架构上看,这不是一个单独的“雾后处理”,而是一套完整的摄像机相对体积光照缓存:EID 2937 负责世界空间介质和光照采样,EID 2943 将其转换为可直接查询的积分体积,EID 11086 再以极低的逐像素成本完成场景合成。

10. 结论

__tex_volumetric_fog_3d_image 是一个每帧更新、带历史重投影的摄像机视锥体积。它在约 0–200 m 内用 256×144×128 Froxel 表示当前视场中的局部介质和光照,并以对数 Z 分布提高近景精度。

贴地雾通过 Froxel 世界高度与地形高度之差生成:在地面最浓,约 5 m 内按 1.5 次幂衰减,并叠加动态噪声、距离和方向遮罩。绿色首先来自局部 Fog Volume、near_ground_fog_color 和全局 fog_color,随后乘环境光、太阳光和局部灯光形成局部散射。

EID 2943 将局部散射和光学厚度从近到远积分,使每个 Z 切片都存储相机到该深度的累计结果。EID 11086 因而只需根据场景深度采样一次 3D 纹理,即可得到物理意义上正确的累计雾散射与透射率,并在 180–200 m 区间平滑切换到远雾。