06 Screen Trace
06 Screen Trace
Voxel Cone Tracing 把辐射写进了三维体素场,但最终要为每一个屏幕像素找到自己的查询起点。Screen Trace 将相机的深度和法线还原为表面点,再把该点送入体素空间,沿法线半球发射 Cone,读取当前可用的 Radiance。它连接的是“屏幕上看见的表面”和“屏幕外仍然存在的体素辐射”,输出仍是一张屏幕空间间接光纹理。
一、屏幕空间查询
RecordRenderGraph 只在 cameraDepthTexture 和 cameraNormalsTexture 都有效时登记 Screen Trace。Pass 读取当前相机的 Radiance 3D 纹理和可选蓝噪声,写入与相机颜色同尺寸的 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,而不是整数体素索引。边界采用严格的内部语义,落在盒面外或盒面上的屏幕表面返回零间接光;这一步也明确了后文 Ray–AABB 讨论的前提:项目的 Cone 起点已经在盒内。
四、Ray–AABB 求交
4.1 Slab 推导是概念模型
下面给出的完整 slab 推导是理解一般 Ray–AABB 求交的概念模型。它说明一条射线如何得到进入段和离开段,适用于需要从盒外起点求交的通用实现;它不是当前 Screen Trace 每一步的实际调用路径。
归一化体素盒是 、 的 AABB。对射线
逐轴求两个平面交点:
交换每一轴的较小/较大值后,整体交段为:
当 ,或 时,射线与体素盒没有可用交点;若起点在盒外,通常从 开始;若起点在盒内,入段已经结束,只需关注 。
4.2 项目实现:盒内起点逐步出界
当前 VoxelGI_TraceCone 采用更窄的实际路径:屏幕表面经过上一节的 voxel <= 0/voxel >= 1 检查后,Cone 起点必定在单位体素盒内。项目没有在每条 Cone 上再次运行完整 slab 求交,也不单独求 t_enter。
每步只计算:
float3 coordinate = origin + direction * offset;
if (any(coordinate <= 0.0) || any(coordinate >= 1.0))
break;这个逐步检查就是当前实现对盒内起点的 t_exit 近似:只要采样坐标离开任一轴的 内部区间,就停止该条 Cone。这样既表达了“只能查询 Voxelization Bounds 内的辐射”,也避免把面向盒外起点的通用求交代码误读成项目实际路径。盒外像素、天空和越界步进均返回零间接光。
五、屏幕 Cone Tracing
每个有效像素以重建的 voxel 为起点,用屏幕法线建立切线基。VoxelGI_SampleHemisphere 把索引映射到单位半球,抖动来自蓝噪声,未提供蓝噪声时使用屏幕像素哈希;这一步只负责生成屏幕端方向集合。
单条 Cone 的步进沿归一化体素空间进行:首步使用 ScreenFirstStep / R 并加入 jitter,之后以 ScreenStepScale 增长步长;每一步执行上一节的盒内出界检查,再从 Radiance 3D Texture 读取样本。Cone 足迹到 Mip 的尺度关系、SampleLevel 的显式 LOD、体素 A 通道的遮挡衰减与停止条件已经在第 05 篇 Voxel Cone Tracing中定义,这里只保留 Screen Trace 的特有边界,不重复整套推导。
屏幕端的 coneCount 有一个优先分支:TemporalActive 时强制为 1;否则才按 Screen Quality 映射为 1、2、4 或 8。多条 Cone 虽然按法线余弦参与 result,但最终是除以 coneCount 而不是 weightSum,再乘 ScreenIntensity。因此这里的输出应称为“屏幕空间间接光近似”,不是经过 weightSum 归一的严格 Irradiance;Temporal 开启且 Motion Vector 有效时,项目可以将每帧的单 Cone 方向交给后续历史阶段累积。

六、信息互补
Depth/Normals 给出了当前相机看见的精确表面位置与局部方向,却只覆盖屏幕内的第一层可见表面;被遮挡的墙面、相机背后的几何以及屏幕外的发光体不会出现在这些纹理中。体素 Radiance 则覆盖 Voxelization Bounds 内的三维场景,可从当前表面沿半球方向查询屏幕之外的辐射,但受限于体素分辨率、Bounds 和占用平均。
因此 Screen Trace 的职责是以屏幕表面确定“在哪里接收光、朝哪里采样”,以体素场确定“附近空间有什么辐射和遮挡”。天空、盒外位置和越界步进没有可靠的体素查询,返回零;体素场也不替代相机颜色,因为它只输出屏幕空间间接光。
