06 Screen Trace
06 Screen Trace
Voxel Cone Tracing 把辐射写进了三维体素场,但最终要为每一个屏幕像素找到自己的查询起点。Screen Trace 将相机的深度和法线还原为表面点,再把该点送入体素空间,沿法线半球发射若干条 Cone,读取 DirectRadiance 或 FinalRadiance。因此它连接的是“屏幕上看见的表面”和“屏幕外仍然存在的体素辐射”,输出仍是一张屏幕空间间接光纹理。
一、屏幕空间查询
RecordRenderGraph 只在 cameraDepthTexture 和 cameraNormalsTexture 都有效时登记 Screen Trace。Pass 同时导入当前相机的 Radiance 3D 纹理:启用 SecondBounce 时选 FinalRadiance,否则选 DirectRadiance;蓝噪声纹理也是可选的外部输入。Render Graph 把深度、法线、Radiance 和噪声声明为读,把与相机颜色同尺寸的 Screen Trace 纹理声明为写,执行函数用全屏三角形调用 VoxelGI_ScreenTraceFragment。
Pass 只负责本帧的空间查询。它不会重新体素化,也不会把 Radiance 写回三维场;所有追踪参数都由当前帧快照设置到全局 Shader 参数中,包含首步、步长倍率、最大步数、Cone 角度、质量和强度。
二、世界坐标重建
屏幕 UV 的深度值是投影后的非线性值,不能直接当作世界坐标的第三个分量。Shader 先从 _VoxelGIScreenDepth 读取 rawDepth,再按平台的 UNITY_REVERSED_Z 约定判断天空:反向 Z 下 rawDepth <= 0 提前返回,普通 Z 下 rawDepth >= 1 提前返回。天空没有可靠表面位置,继续查询只会把相机射线错误地送入体素盒。
对有效像素,URP 提供的 ComputeWorldSpacePosition 使用屏幕 UV、原始深度和 UNITY_MATRIX_I_VP 做逆投影:
其中 UNITY_MATRIX_I_VP 是当前相机 GPU 投影矩阵与 View 矩阵乘积的逆。这样得到的 worldPosition 是相机真正看到的表面点,而不是位于近裁剪面或远裁剪面上的猜测点。屏幕法线则从 _VoxelGIScreenNormals 读取并 normalize,它作为 Cone 半球的朝向;深度负责位置,法线负责方向,两者语义不能互换。

三、体素坐标转换
体素场的 WorldToVoxel 是以体素为单位的逆变换。世界点先乘矩阵得到连续网格坐标,再除以分辨率 R 转成 Texture3D 所需的归一化坐标:
当前实现直接表达为:
float3 voxel = mul(_VoxelGIWorldToVoxel,
float4(worldPosition, 1.0)).xyz
/ _VoxelGIResolution;
if (any(voxel <= 0.0) || any(voxel >= 1.0))
return float4(0.0, 0.0, 0.0, 1.0);voxel 现在是 意义下的连续 3D UV,而不是整数体素索引。SampleLevel 读取 Radiance 时也使用这个坐标;若把它再次除以 R,会把查询压缩到体素盒的一小块区域。边界采用严格的 >0 与 <1 内部语义,落在盒面上的点被视为无效,避免后续 Cone 在边界采样到未定义的外部区域。
四、Ray–AABB 求交
归一化体素盒是 、 的 AABB。对射线
可以用 slab 法得到每个轴的进入与离开距离:
交换每一轴的较小/较大值后,整体交段为:
当 ,或 时,射线与体素盒没有可用交点。若起点在盒内, 可直接取当前点,追踪从表面位置向法线半球前进。
Screen Trace 的 VoxelGI_TraceCone 没有再调用单独的 slab 函数,而是在每一步计算 coordinate = origin + direction * offset,只要任一分量 <= 0 或 >= 1 就 break。这相当于使用单位 AABB 的出界条件作为 检查:先前已经确认起点在盒内,因此不需要再次求进入段。该检查对每条 Cone 都成立,也让 Cone 足迹尚未扩张到盒外时可以继续读取 3D Radiance。
盒外像素返回全零间接光。它包括天空像素,也包括深度重建位置落在 Voxelization Bounds 之外的可见表面;这不是异常,而是“屏幕几何”和“体素场覆盖范围”之间的明确数据边界。
五、屏幕 Cone Tracing
每个有效像素以重建的 voxel 为起点,用屏幕法线建立切线基。VoxelGI_SampleHemisphere 把索引映射到单位半球:半径使用平方根分布,方位角使用黄金比例序列;抖动来自蓝噪声,未提供蓝噪声时则使用屏幕像素哈希。这样每个像素的方向集合不必与固定的规则网格重合。
Cone 的步进在归一化体素空间完成。首步为 ScreenFirstStep / R,并以 lerp(0.5, 1.5, jitter) 改变起点;每次采样后执行 stepLength *= ScreenStepScale。设当前偏移为 、Cone 半角为 ,对应的体素直径为:
直径从归一化步进乘回 R,再选择与空间足迹匹配的 Radiance Mip。近处保持细节,远处让更粗的 3D Mip 代表更大的 Cone 截面。当前实现从 _VoxelGITraceRadiance 使用 SAMPLE_TEXTURE3D_LOD 读取 float4,RGB 是辐射,A 是体素占用/遮挡。
单条 Cone 的累积以 A 作为遮挡衰减:
当累计 A 达到 0.95 或步数达到 ScreenMaxSteps 就结束;离开 AABB 也立即结束。多条 Cone 再按 加权,最后除以 Cone 数量并乘 ScreenIntensity。质量设置把数量映射为 1、2、4 或 8;这是对半球方向覆盖的离散近似,不改变单条 Cone 的步进和 Mip 规则。
六、信息互补
屏幕信息和体素信息各自拥有对方没有的部分。Depth/Normals 给出了当前相机看见的精确表面位置与局部方向,却只覆盖屏幕内的第一层可见表面;被遮挡的墙面、相机背后的几何以及屏幕外的发光体不会出现在这些纹理中。体素 Radiance 则覆盖 Voxelization Bounds 内的三维场景,可从当前表面沿半球方向查询屏幕之外的辐射,但它受限于体素分辨率、Bounds 和占用平均。
因此 Screen Trace 的职责是以屏幕表面确定“在哪里接收光、朝哪里采样”,以体素场确定“附近空间有什么辐射和遮挡”。天空或盒外位置没有可靠的体素查询,返回零;体素场也不替代相机颜色,因为它只输出间接光。后续阶段可以把这张屏幕空间结果作为独立输入继续处理,保持屏幕重建、体素查询和最终颜色之间的边界清晰。
