05 Voxel Cone Tracing
05 Voxel Cone Tracing
方向光和自发光已经写入 DirectRadiance,但一个体素表面接收的间接光仍然需要从周围体素查询。当前实现不沿单一细线发射大量 Ray,而是用有限数量的 Cone 覆盖法线半球:距离增加时采样足迹变宽,同时选择更粗的 Radiance Mip,把半球积分压缩为空间层级上的若干次查询。
一、Cone Tracing
Ray 是一条理想的零半径路径,每一步只询问一个近似点;Cone 则有一个随距离扩张的截面,代表一簇相邻方向和空间位置。Ray 更接近精确路径,但在体素场中容易受到离散空洞和采样噪声影响;Cone 直接把有限体素分辨率带来的空间平均纳入采样模型。
VoxelGI_TraceIndirect 的起点是体素中心归一化后的坐标,方向由法线建立的切线基变换到世界/体素查询空间。每条 Cone 从 FirstStep / Resolution 开始,随后按 StepScale 增长步长;每一步先检查 VoxelGI_BoxDistance,离开体素盒就结束。Cone 不是对几何表面做精确求交,而是对 Radiance 和 Opacity 体积进行步进累积。
二、Cone Footprint
设 Cone 半角为 ,沿中心线前进的距离为 ,则半径和直径近似为:
Shader 中的 offset 使用归一化体素坐标,计算直径时再乘 _VoxelGIResolution 转回体素单位:
这里的最小值 1 表示最细的 Mip 不再细分到小于一个体素的足迹。VoxelGI_CalculateMip 使用 :直径每扩大一倍,采样层级增加一级;随后将结果限制在 IndirectMinMip 与 IndirectMaxMip 之间。

因此 Mip 选择不是额外的模糊开关,而是 Cone 的空间覆盖与 3D 纹理层级之间的尺度匹配:近处读取细节,远处让相邻体素先被平均再参与一次查询。
三、Radiance Mipmap
DirectRadiance 和启用第二次反弹后的 FinalRadiance 都由 GenerateMipChain 生成完整 Mip。Unity 不会自动为这类 Random Write 3D 纹理建立所需层级,项目使用两个 Compute Kernel 手动完成:
MipmapGeneration读取源 Mip 中对应的 2×2×2 体素,求和后乘以 ,把结果写入帧内Mip Scratch的目标层。CopyTexture3D从 Scratch 的同一层读取,再写回目标 3D 纹理的对应 Mip。- 循环从
sourceMip = 0开始,直到MipCount - 1;目标分辨率每层右移一位,最低保持为 1。
这种 Mip 是对 RGBA 一起求平均的体积层级,因此 Radiance 的 RGB 和占用 A 会保持同一空间足迹。采样时 SampleLevel 的显式 LOD 由 Cone 直径决定,不能把普通 2D 纹理的屏幕导数直接当作 3D Cone 尺度。
四、半球采样
每个占用体素先用 VoxelGI_GetTangentBasis 为法线构造稳定的切线基。VoxelGI_FibonacciHemisphere 在局部半球上使用黄金角序列:
局部方向为 ,再乘以切线基得到以法线为上半球的 Cone 方向。质量关键字把 Cone 数量设为 1、4、8 或 16;它改变的是半球方向覆盖数量,而不是单条 Cone 的步进语义。
方向并不等权地直接相加。每条 Cone 使用:
VoxelGI_TraceIndirect 在所有 Cone 完成步进后按 saturate(dot(direction, normal)) 累积,并以权重和归一化。这让倾斜于表面的方向自然减弱,也避免仅因 Cone 数量变化而线性改变亮度。
五、辐射与遮挡积分
每一步从 DirectRadiance 的 SampleLevel 取得 float4 sampleValue。RGB 是该空间足迹内的辐射,A 是体素占用/遮挡。Cone 的累积状态也是 float4 cone,其 RGB 记录已经收到的辐射,A 记录遮挡饱和度;更新形式为:
其中 是本步 sampleValue, 是已累积的 A, 对应 IndirectAlphaAttenuation。当 A 接近 1 时,新样本的贡献被压低;当 Cone 的 A 达到 0.95,或达到 IndirectMaxSteps,该条步进停止。离开体素 Bounds 也会立即停止。
这里的 Opacity 不是透明材质的颜色混合 alpha,而是体素占用提供的遮挡近似。一个 Blocker-only 体素只有 A 没有表面 RGB,依然能让 Cone 尽快收敛到遮挡状态;相反,Radiance 的 RGB 仍来自 DirectRadiance 中的 Emissive 或方向光直射。
六、Second Bounce
VoxelIndirectLighting 以 DirectRadiance 为输入,对每个有表面的体素调用 VoxelGI_TraceIndirect,得到来自周围体素的半球辐射 bounce。它不会递归读取刚刚写出的 FinalRadiance,而是把一次间接反弹乘以当前体素的 Albedo 与强度后加回直接结果:
FinalRadiance 的 RGB 是这次合成后的值,A 仍复制当前体素的占用值。这里的 SecondBounce 名称表示在直接光之后再进行一次表面反射近似,不是无限次路径追踪;关闭它时,Lighting Pass 在 Direct Mip 生成后结束,保留 DirectRadiance 作为当前可用辐射结果。
FinalRadiance 完成后也会单独生成 Mip 链。这样,直接辐射和一次反弹后的辐射分别拥有自己的空间层级,后续查询可以按管线选择读取的资源,而不会把 Direct 与 Final 的生产阶段混成一张纹理。
七、近似误差
Voxel Cone Tracing 的误差来自离散化和积分近似的共同作用。首先,体素把几何压到有限格子,薄墙、尖角和小物体可能被平均或丢失;其次,Cone 足迹对应的 Mip 会把 2×2×2 邻域平均,远处细节会变成更平滑的辐射。
遮挡 A 的占用模型还可能产生漏光:一个体素内部的几何位置、厚度和方向被压缩成单个占用值,Cone 不知道真正的表面交点。相反,宽 Cone、粗 Mip 和较大的步进会让遮挡提前扩散,表现为过度变暗或接触阴影变软。
能量也不保证严格守恒。Albedo/Normal 在体素化时做离散平均,Emissive 有固定点量化与每体素样本平均;半球方向是有限的 Fibonacci 集合,步进次数、A 衰减和 indirectIntensity 又会改变积分截断。最终结果应理解为在当前 Bounds、分辨率和 Cone 角度下对辐射场的稳定近似,而不是解析渲染方程的精确解。
