03 Compute 体素化
03 Compute 体素化
体素空间与 3D GBuffer 定义了数据应该存在哪里;本篇处理另一半问题:如何把 Unity 场景中的 Renderer、网格和材质转换为对三维网格的并发写入。当前实现不要求材质额外提供 Voxelization Pass,而是由 Compute Shader 读取 Raw Vertex/Index Buffer,按三角形覆盖范围写入原子累积 Buffer,最后 Resolve 成浮点 3D GBuffer。
一、Renderer 输入
体素化从 Renderer Registry 的快照开始。它会排除未启用、非活动层级、forceRenderingOff、LayerMask 不匹配、Bounds 与体素立方体不相交的 Renderer;只保留包含三角形 SubMesh、处于 Opaque Render Queue 且允许贡献表面的对象。
MeshRenderer 与 SkinnedMeshRenderer 都能进入同一流程。普通 Mesh 读取静态网格缓冲区;Skinned Mesh 读取当前蒙皮后的顶点缓冲区。它们最终都转换为三角形、材质参数和对象变换,再交给同一个 VoxelizeMesh Kernel。
Registry 提供的是候选对象集合,不替代 URP 的正常渲染。体素化只额外建立场景的三维表面缓存,普通材质仍继续走 URP/Lit 的可见表面 Pass。
二、网格缓冲区
普通 Mesh 使用 Mesh.GetVertexBuffer 与 Mesh.GetIndexBuffer 取得原始缓冲区。对于 SkinnedMeshRenderer,位置、法线和 UV 所在的 stream 使用 SkinnedMeshRenderer.GetVertexBuffer,从而读取当前变形后的顶点;索引仍来自 Mesh.GetIndexBuffer。
Compute Shader 将位置、法线、UV 和索引输入声明为 ByteAddressBuffer。它不假定所有模型共用同一顶点布局,而是由 C# 同时传入每个属性的 stream、stride、offset、format 和 dimension。Shader 根据顶点编号计算属性字节地址,再把原始 word 解码为需要的分量。
这条输入路径让 Compute 阶段直接处理几何数据,同时避免复制一套 URP/Lit 表面 Shader。体素化关心的是表面在三维网格里的离散覆盖,最终画面仍由常规渲染路径产生。
三、顶点与索引格式
一个顶点属性的读取地址由顶点编号、stream、stride 和属性 offset 决定:
Position、Normal 与 TexCoord0 可以位于不同 stream。若 Mesh 缺少 Normal,体素化使用三角形面法线;若缺少 UV,则以零坐标采样材质。C# 把 VertexAttributeFormat 和 dimension 编码为参数,Shader 据此读取 Float32、Float16 或归一化格式。
每个 SubMesh 提供 indexStart、indexCount 与 baseVertex。逻辑索引先映射到 Index Buffer 地址,再按 16 位或 32 位 index format 解码:
位置经过 ObjectToWorld 和 WorldToVoxel 后才进入覆盖测试。法线使用对象矩阵的逆转置变换,以保持非均匀缩放下的正确方向。
四、SubMesh Dispatch
每个 Triangle SubMesh 对应一次 VoxelizeMesh Dispatch。三角形数量由该 SubMesh 的 indexCount 得到:
VoxelizeMesh 的线程组大小为 ,每个线程处理一个三角形。Dispatch 组数以 CeilDiv 计算,Shader 通过 SV_DispatchThreadID 的边界判断跳过超过 triangleCount 的线程,因此三角形数量不需要恰好是 64 的倍数。
Dispatch 前会绑定 ObjectToWorld、NormalToWorld、indexStart、baseVertex、Raw Buffer 描述与材质参数。相同 Kernel 因而能够处理不同顶点布局、不同材质和不同 SubMesh。
五、材质数据
体素化复用 URP/Lit 的常用属性。_BaseMap 与 _BaseColor 决定 Albedo;_EmissionMap 与 _EmissionColor 只在 _EMISSION 关键字开启时写入 HDR Emissive;_AlphaClip 与 _Cutoff 决定一个采样点是否在写入前被丢弃。
透明 Render Queue 整体不参与当前体素化。Alpha Clip 与透明不同:它仍是离散表面,只是在采样 BaseMap 后按 Cutoff 丢弃不足以覆盖的部分。因此树叶和栅栏等 Alpha Clip 材质能够写入体素,半透明玻璃不会被当作不透明 GI 表面。
重心坐标同时插值 UV、世界法线和第三维位置。材质采样发生在候选体素中心,而不是顶点上,因此纹理、Emission 与裁剪都与真实覆盖位置对应。
六、主轴投影
三角形位于三维体素网格中,但覆盖测试适合先在二维完成。Kernel 计算体素空间面法线:

叉积的长度表示由两条边向量张成的平行四边形面积,方向则由右手定则决定;因此交换顶点绕序会同时反转法线方向,而法线分量随后用于选择主轴投影。
再选择绝对分量最大的轴:
RGB 坐标轴由用户生成的 CoordinateArrow.glb 组成,滑块可改变法线分量;Demo 会高亮最大绝对分量对应的主轴与投影平面。下方静态 SVG 继续作为固定 主轴案例。
沿该轴观察时,三角形投影面积最大,不容易退化为细线。选择 时在 平面测试;选择 或 时相应使用 或 平面,未投影的第三个坐标随后由重心权重恢复。
七、覆盖测试
投影后的三角形先计算二维包围盒,只遍历可能被覆盖的整数格点。对每个候选体素中心 ,Kernel 计算三条有向边函数:
三条边函数与三角形有向面积组合为重心坐标。若采样点位于三条边的同侧,便属于投影三角形;重心权重可插值 UV、世界法线以及未投影的第三维,再恢复三维体素坐标。
细小或倾斜三角形可能从离散格点之间穿过。conservativeRasterization 开启后,包围盒和边函数容差按 conservativeScale 扩张,减少这种空洞;代价是更多候选体素、更多材质采样与更高的原子竞争。
八、并发累积
不同三角形常会覆盖同一个体素,因此不能依赖线程写入顺序。三维体素坐标先被压平为线性地址:
本帧使用 AlbedoAccumulation、NormalAccumulation、EmissiveAccumulation 与 OpacityAccumulation 四个原子 Buffer。Albedo 与 Normal 通过 Compare-And-Swap 循环维护量化后的增量平均;Opacity 使用 InterlockedMax 保留最大遮挡;Emissive 使用 uint4,以 1024 固定点缩放 RGB,并用第四个分量记录样本数。
这些 Buffer 是并发写入的中间态,不是 3D GBuffer。本次 Pass 结束后,它们会被 Resolve Kernel 转换为浮点三维纹理。
九、GBuffer Resolve
体素化由三个 Compute Kernel 串成一个阶段:
| Kernel | 线程组 | 职责 |
|---|---|---|
ClearVoxelAccumulation | 清空四个原子累积 Buffer | |
VoxelizeMesh | 每线程处理一个三角形并写入累积 Buffer | |
ResolveVoxelAccumulation | 解码累积值并写入浮点 3D GBuffer |
Resolve 时,没有表面样本的体素写入零 Albedo 与默认编码法线;有表面样本的体素解码平均 Albedo、Normal 和 Emissive,并写入持久的 AlbedoOpacity、Normal、Emissive 纹理。Opacity 保持占用和遮挡语义,而不是相机颜色的透明度。至此,Renderer、Raw Mesh Buffer、材质属性和三角形覆盖都被压缩为规则三维数据,后续光照阶段只需读取这些 GBuffer。
