04 体素光照注入
04 体素光照注入
体素化结束后,规则网格里已经有了表面属性,但这些属性还不是光。本篇把边界收在 DirectRadiance:从 AlbedoOpacity、Normal、独立 Emissive 和方向光阴影开始,说明项目如何得到“自发光 + 当前方向光直射”的体素辐射结果。后续辐射查询所需的其他阶段不在本文展开。
一、辐射体积
体素阶段的 Resolve 结果提供三个输入 3D 纹理:AlbedoOpacity 的 RGB 是表面反照率、A 是占用/遮挡;Normal 的 RGB 是编码到 0–1 的世界法线、A 复用占用值;Emissive 的 RGB 是独立的 HDR 自发光。它们描述“这里有什么表面”,而不是“这里已经收到多少光”。
VoxelGICameraContext 为每个 Base Game Camera 持有这些跨帧体素纹理。光照线程以体素中心回到世界空间,读取 GBuffer;未占用体素直接写零,只有占用体素才继续进行法线、阴影和方向光计算。数据边界可以概括为:
其中 A 通道不是相机颜色的透明度,而是后续体素查询判断占用和遮挡的约定。
二、方向光阴影空间
方向光没有像点光源那样由位置决定阴影体积,项目使用 Light.transform.forward 表示光照方向。VoxelGIFrameMatrices.Create 围绕体素 Bounds 构造一台虚拟光源相机:令 range = bounds.extents.magnitude,把相机放在 bounds.center - forward * range,再用方向光朝向构造 ShadowView。正交投影覆盖 [-range, range],近裁剪面为 0.01,远裁剪面为 range * 2 + 0.01,确保整个体素立方体落入深度范围。
这里的空间关系可以用一张静态图概括:方向光线彼此平行,正交相机把体素 Bounds 投影到 Shadow Depth;写入 ShadowDepth 时光栅阶段固定使用 Raster Depth Bias (1, 2.5),而 ShadowNormalBias/ShadowSunBias 不是这一步的光栅状态,它们只在 DirectLighting 采样位置时施加偏移;不可见但需要阻挡 GI 的 Blocker-only 几何只负责写遮挡,不贡献表面颜色。
Unity 的观察空间和 Transform 的局部 +Z 约定不同,创建 View 后会翻转第三行以匹配渲染观察方向。ShadowProjection 使用正交矩阵;启用 Reversed-Z 的平台还会翻转投影的深度相关项。最后用 scale-bias 将齐次 NDC 的 XY 和 Z 从 映射到 ,组合成 WorldToShadow:
是 ShadowDepth 的采样坐标, 是比较值。它与体素空间的 WorldToVoxel 是两套不同的坐标变换:前者服务方向光投影,后者服务 3D 网格定位。
RecordShadow 把跨帧的 ShadowDepth 导入 Render Graph,ExecuteShadow 清空深度、设置阴影视口和 ShadowView/ShadowProjection,然后恢复相机 View/Projection。绘制的是允许投射 Voxel Shadow 的 Renderer;这张纹理只为本次体素光照服务,不是 URP 相机的普通阴影贴图。

这张 ShadowDepth 的写入还固定调用 SetGlobalDepthBias(1f, 2.5f),并在绘制完成后恢复为零。它解决的是光栅化深度上的自阴影痤疮;DirectLighting 读取深度前再使用按体素尺寸缩放的 Normal/Sun Bias,二者位于不同阶段,不能把它们当成同一个参数。
三、ShadowCaster 与 Blocker
阴影绘制优先查找材质的 ShadowCaster Pass。因此标准 Universal Render Pipeline/Lit 材质的阴影裁剪和相关属性可以直接复用,不需要额外写一份“体素阴影材质”。透明 Render Queue 不进入这条遍历,Alpha Clip 仍可在 URP 的 ShadowCaster 中保留正确的裁剪行为。
当材质没有 ShadowCaster 时,运行时会尝试 VoxelGIShadow。VXGIBlocker.shader 只有这个 Pass:它使用当前注入的光源 View/Projection,把几何写入 Shadow Depth;ColorMask 0 表明它不贡献颜色,ZWrite On 才是它唯一需要的光栅副作用。它没有 UniversalForward,所以语义是 Blocker-only,而不是可以被正常相机渲染的表面材质。
Blocker-only 还要通过 Compute 体素化写入占用:_VoxelGIOpacityOnly 分支只对 OpacityAccumulation 执行 InterlockedMax(..., 65535),不写 Albedo、Normal 或 Emissive。这样同一几何体同时满足两种约束:在方向光 Shadow Depth 中挡住太阳光,在体素辐射查询中挡住空间传播;它不会制造表面反射或发光。
因此,ShadowCaster 与 VoxelGIShadow 的职责可以分开看:前者按材质已有规则投射阴影,后者让没有可见表面的几何成为 GI 遮挡体。两者最终都服务方向光的可见性判断,但只有普通表面 GBuffer 才提供 Albedo、Normal 和 Emissive。
四、直接光照
VoxelDirectLighting 只处理 A 通道大于零的体素。它从 Normal 读取 编码并还原到 ,从体素中心求世界位置,再使用方向光方向计算 Lambert 余弦项:
阴影函数先做两种以体素尺寸缩放的偏移:沿法线加 ShadowNormalBias,沿光照方向的反向项减去 ShadowSunBias。偏移后的世界点经 WorldToShadow 得到 ,若坐标超出阴影投影范围则返回完全可见;否则从 ShadowDepth 取存储深度,并按 _VoxelGIReversedZ 选择比较方向。比较结果是 。
当前方向光直接项对应:
其中 是 Albedo, 是方向光颜色, 是 Light.intensity, 是 lightIntensity。方向光的选择有确定优先级:若 RenderSettings.sun 非空、处于启用状态且类型为 Directional,优先使用它;否则以 FindObjectsByType<Light>(FindObjectsInactive.Exclude, FindObjectsSortMode.None) 的无排序结果搜索,并返回遍历中第一个活动 Directional Light。项目不注入点光源或聚光灯。没有方向光时,直接项为零,但 Emissive 仍然进入 DirectRadiance。
五、HDR Emissive
Emissive 不从 AlbedoOpacity 推导,也不与方向光阴影相乘。体素化阶段读取 _EmissionMap 和 _EmissionColor,只有 _EMISSION 等价开关开启时才写入;Resolve 后成为独立的 Emissive 3D 纹理。累积时 RGB 以 1024 固定点编码并限制单次值不超过 64,Resolve 再除以量化倍率和样本数,得到每体素的平均 HDR 自发光。
光照阶段使用 emissive = Emissive.rgb * emissiveIntensity,与方向光直接项相加:
其中 是独立 Emissive 纹理读取值。DirectRadiance 每次重写时都重新读取纯 Emissive,而不是读取上一轮辐射结果;方向光、阴影或强度变化因此不会让自发光逐帧叠加。Emissive 的 A 通道仍复制占用值,保持辐射纹理和空间遮挡的通道契约一致。
六、DirectRadiance
DirectRadiance 的 RGB 是“自发光 + 当前方向光直射”的线性辐射近似,A 是体素占用/遮挡值;它不是材质 GBuffer,也不是屏幕颜色。VoxelGIRenderPass.Lighting.cs 在 VoxelDirectLighting 后为它生成空间 Mip,供后续辐射查询使用。
每一级 Mip 由上一层的 体素平均得到:Compute 阶段先把结果写入当前 Lighting Pass 的 Mip Scratch,再复制回目标 3D 纹理对应层级。这里的 Mip 是对 DirectRadiance 的空间足迹预积分,不改变这篇文章的边界:到此为止,体素已经拥有可被后续阶段读取的方向光直射辐射。

