05 Voxel Cone Tracing
05 Voxel Cone Tracing
方向光和自发光已经写入 DirectRadiance,但一个体素表面接收的间接光仍然需要从周围体素查询。当前实现不沿单一细线发射大量 Ray,而是用有限数量的 Cone 覆盖法线半球:距离增加时采样足迹变宽,同时选择更粗的 Radiance Mip,把半球积分压缩为空间层级上的若干次查询。indirectConeAngle 表示 Cone 角度(全角),Shader 在正切公式中使用它的半角 。
一、Cone Tracing
Ray 是一条理想的零半径路径,每一步只询问一个近似点;Cone 则有一个随距离扩张的截面,代表一簇相邻方向和空间位置。Ray 更接近精确路径,但在体素场中容易受到离散空洞和采样噪声影响;Cone 直接把有限体素分辨率带来的空间平均纳入采样模型。
VoxelGI_TraceIndirect 的起点是体素中心归一化后的坐标,方向由法线建立的切线基变换到世界/体素查询空间。每条 Cone 从 FirstStep / Resolution 开始,随后按 StepScale 增长步长;每一步先检查 VoxelGI_BoxDistance,离开体素盒就结束。Cone 不是对几何表面做精确求交,而是对 Radiance 和 Opacity 体积进行步进累积。
二、Cone Footprint
indirectConeAngle 是 Cone 角度(全角);设其半角为 ,沿中心线前进的距离为 ,则半径和直径近似为:
Shader 中的 offset 使用归一化体素坐标,计算直径时再乘 _VoxelGIResolution 转回体素单位:
这里的最小值 1 表示最细的 Mip 不再细分到小于一个体素的足迹。VoxelGI_CalculateMip 使用 :直径每扩大一倍,采样层级增加一级;随后将结果限制在 IndirectMinMip 与 IndirectMaxMip 之间。
下面是一组仅用于计算演示的参数组合:取默认体素分辨率 ,FirstStep=1、StepScale=2、Cone 角度(全角),所以半角 ;IndirectMinMip=0,而 时资源的最高 Mip 为 7。FirstStep 的实际范围是 –(默认 1),StepScale 是 –(默认 1),Cone 角度是 –(默认 ),这组示例中的 StepScale=2 是范围内的非默认值。
表中每一行是取样前的状态,footprint 使用 Shader 的
,mip 是
在上下限截断前的连续值。完成该行取样后,严格执行stepLength *= StepScale; offset += stepLength,再进入下一行:
| 第几次取样 | stepLength(归一化) | 累计 offset(归一化) | footprint(体素) | mip(连续值) |
|---|---|---|---|---|
| 0 | 0.0078125 | 0.0078125 | 3.464 | 1.792 |
| 1 | 0.0156250 | 0.0234375 | 10.392 | 3.377 |
| 2 | 0.0312500 | 0.0546875 | 24.249 | 4.600 |
| 3 | 0.0625000 | 0.1171875 | 51.962 | 5.699 |
例如第 0 行取样完成后,步长先从 乘以 2 变为 ,再把它加到原有 offset,所以第 1 行的 offset 是 ,而不是 。这正是 Shader 中 stepLength *= StepScale; offset += stepLength 的顺序。

因此 Mip 选择不是额外的模糊开关,而是 Cone 的空间覆盖与 3D 纹理层级之间的尺度匹配:近处读取细节,远处让相邻体素先被平均再参与一次查询。
三、Radiance Mipmap
DirectRadiance 和启用第二次反弹后的 FinalRadiance 都由 GenerateMipChain 生成完整 Mip。Unity 不会自动为这类 Random Write 3D 纹理建立所需层级,项目使用两个 Compute Kernel 手动完成:
3.1 手动 Mipmap
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 尺度。
3.2 3D Load/Store
普通 Texture3D<float4> 能够被采样,不代表同一设备也能把它作为可随机写入的 RWTexture3D<float4>。当前插件的 DirectRadiance、FinalRadiance 和帧内 Mip Scratch 都使用 R16G16B16A16_SFloat;Mip 生成时还会把 Scratch 的指定 Mip 绑定为 UAV,随后再把同一层写回目标 Radiance 纹理。因此运行后端必须同时支持 RGBA16F 三维纹理的 Load/Store、可写 Mip Level,以及 Compute Shader 对这些资源的调度与同步。
Renderer Feature 在创建资源前使用以下条件关闭不满足能力的设备:
SystemInfo.supportsComputeShaders &&
SystemInfo.supports3DTextures &&
SystemInfo.IsFormatSupported(
GraphicsFormat.R16G16B16A16_SFloat,
GraphicsFormatUsage.LoadStore)其中 supports3DTextures 只回答三维纹理这一资源类型是否可用,GraphicsFormatUsage.LoadStore 才约束该浮点格式能否走随机读写路径。这个检查是当前实现的必要门槛,而不是对所有图形后端的统一保证;即使体素化已从 Texture Atomic 改为 Buffer Atomic,Radiance Mipmap 仍保留这项跨平台能力要求。
四、半球采样
每个占用体素先用 VoxelGI_GetTangentBasis 为法线构造稳定的切线基。VoxelGI_FibonacciHemisphere 在局部半球上使用黄金角序列:
局部方向为 ,再乘以切线基得到以法线为上半球的 Cone 方向。质量关键字把 Cone 数量设为 1、4、8 或 16;它改变的是半球方向覆盖数量,而不是单条 Cone 的步进语义。
方向并不等权地直接相加。每条 Cone 使用:
VoxelGI_TraceIndirect 在所有 Cone 完成步进后按 saturate(dot(direction, normal)) 累积,并以 max(weightSum, 1e-4) 归一化。这让倾斜于表面的方向自然减弱,也避免仅因 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 角度下对辐射场的稳定近似,而不是解析渲染方程的精确解。
