02 体素空间与 3D GBuffer
02 体素空间与 3D GBuffer
本文建立 Voxel GI 的空间与数据契约:先区分相机影响范围和体素化范围,再定义体素网格、坐标变换、体素中心与 3D UV,最后说明 3D GBuffer 的通道语义和分辨率约束。所有公式对应当前插件的体素空间约定。
一、Volume 与 Voxelization Bounds
体素化最容易混淆的是两个范围。VoxelGIVolume 挂载对象上的 Collider 是相机影响区:相机在区域内时才启用 GI;独立 GameObject 上的 Voxelization Bounds 才提供体素网格覆盖的世界空间。前者控制“何时生效”,后者控制“写入哪里”。
TryGetVoxelGridBounds 读取独立 Collider 的世界 AABB,并以最长轴扩成世界轴对齐立方体:
Bounds source = m_VoxelizationBounds.bounds;
float side = Mathf.Max(source.size.x, source.size.y, source.size.z);
Bounds grid = new Bounds(source.center, Vector3.one * side);引用必须指向独立的、启用中的 BoxCollider;体素网格保留原始 Bounds 的中心,边长使用最大轴长度。这样三个轴共享同一个 voxelSize,后续边界求交与空间采样不会发生非均匀缩放。
二、体素网格
设标准化网格的最小点为 ,边长为 ,分辨率为 。网格的基本量是:
网格坐标使用体素作为单位,因此范围约为 。世界点先减去 bounds.min,再除以 voxelSize,就得到位于网格中的连续坐标。这个坐标不是 3D Texture 的归一化 UV,二者要到采样阶段才连接起来。
2.1 体素中心
整数索引 标识一个格子。格子中心不是整数角点,而是:
id + 0.5 让材质、法线与阴影使用格子的代表位置。若把索引角点直接当成中心,整个体素数据会出现半个体素的空间偏移。
三、坐标变换
VoxelGIFrameMatrices 保存同一体素网格的正反向矩阵:VoxelToWorld 负责将体素单位转换到世界空间,WorldToVoxel 负责逆向查询。具体构造见下节。
3.1 VoxelToWorld
对索引中心进行正向变换:
VoxelGIFrameMatrices.Create 中与该矩阵相关的实现摘录如下:
float side = bounds.size.x;
float voxelSize = side / resolution;
Matrix4x4 voxelToWorld = Matrix4x4.TRS(
bounds.min,
Quaternion.identity,
Vector3.one * voxelSize);
Matrix4x4 worldToVoxel = voxelToWorld.inverse;设 ,网格最小点为 ,则 VoxelToWorld 的齐次矩阵为:
对于连续体素坐标 ,矩阵乘法结果为:
令连续坐标取整数索引对应的体素中心:
则体素中心的世界坐标为:
连续体素坐标可以直接表示空间位置;整数索引只有加上 后,才表示对应体素的中心。
输入坐标以“体素”为单位;TRS 的平移将网格原点放在 bounds.min,单位缩放将一个网格单位变成 voxelSize 世界单位,identity 旋转来自世界轴对齐立方体;WorldToVoxel 是该矩阵的逆。Lighting 阶段先用 float3(id) + 0.5 把整数索引转为体素中心,再乘以 VoxelToWorld,保证世界位置位于体素中心。
3.2 WorldToVoxel
逆变换把世界坐标变成以体素为单位的连续网格坐标:
例如 Bounds 为 ,最大轴边长 。当 时,voxelSize=2,世界点 对应:
它位于索引 的体素内部。需要注意,网格坐标仍是体素单位,不能在这一步误当作纹理 UV。
四、体素索引与采样坐标
3D Texture 的采样需要归一化坐标:
屏幕查询的核心路径是:
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);项目中同时存在三种坐标:
| 名称 | 范围/单位 | 用途 |
|---|---|---|
| 世界坐标 | Unity 世界单位 | 物体、相机、Bounds |
| 体素网格坐标 | 约 | 网格定位与体素步进 |
| 纹理坐标 | Texture3D.SampleLevel 与空间查询 |
把已经除过 的 UV 再除一次,或者把索引坐标直接传给 SampleLevel,都会使采样位置错误。反向从纹理坐标回到世界空间时,应先乘以 R,再使用 VoxelToWorld 和体素中心约定。
五、3D GBuffer
体素 GBuffer 使用多张持久 3D 纹理保存不同语义的数据。各纹理统一以 R16G16B16A16_SFloat 表达,A 通道主要承载体素占用信息:
| 资源 | RGB | A | Mipmap | 主要消费者 |
|---|---|---|---|---|
AlbedoOpacity | Albedo,通常为 0–1 | Opacity/占用率 | 否 | 体素光照、调试 |
Normal | 世界法线编码到 0–1 | 占用率 | 否 | 体素光照、调试 |
Emissive | 纯 HDR Emissive | 占用率 | 否 | 直接光照、调试 |
DirectRadiance | 自发光与方向光直射 | 占用率 | 是 | 间接光、Screen Trace |
FinalRadiance | 直接辐射与间接结果 | 占用率 | 是 | Screen Trace、调试 |
A 通道不是相机颜色的透明度。它代表体素是否被表面或 GI 阻挡体占用,并为后续空间查询提供遮挡依据;透明 Render Queue 不写入体素,Alpha Clip 则在通过裁剪后才贡献表面数据。法线写入前编码到 0–1,读取时再还原到 范围。
Albedo、Normal、Emissive 描述体素中的表面属性,Radiance 描述光照阶段产生的辐射结果。将纯 Emissive 与直接/间接辐射分开,可以让光照阶段保留明确的数据来源,也使 Screen Trace 能按当前管线分支选择可用的辐射纹理。
六、空间分辨率
分辨率 表示每个轴上的体素数量,整个规则网格的体素总数为:
当前设置把 voxelResolution 限制在 16–256,并归一到允许范围内最接近的 2 的幂;默认值为 128。因此 时,网格包含 个体素,单个体素边长为:
提高 会在同一 Bounds 中划分出更细的空间单元,降低单个体素承载的几何范围;降低 则得到更粗的空间表示。无论选择哪一个值,WorldToVoxel、体素中心和 3D UV 都必须使用同一个 side 与 R,否则坐标契约会失效。
