如何实现GPU Driven地形渲染

解读

国内一线游戏/数字孪生项目面试中,GPU Driven地形渲染常被用来区分“会用Unity”与“能驾驭Unity”。面试官想确认三件事:

  1. 你是否理解传统CPU端地形(Unity Terrain/四叉树)的瓶颈:DrawCall 爆炸、主线程阻塞、带宽浪费;
  2. 能否把剔除、LOD、Patch 生成、材质实例化全部搬到 GPU,实现“零 CPU 侧遍历”;
  3. 是否熟悉 Unity 的Compute Shader、SRP Batch、Indirect Draw、HZB管线,并能落地到移动/PC 双端。
    回答时先给出“能跑”的最小闭环,再补充“能扛 8k×8k 高度图、200+ FPS”的工业级细节,体现性能敏感意识工程落地能力

知识点

  1. GPU Driven 渲染范式:CPU 只负责上传“可见索引列表”,GPU 自行决定顶点/片元生成,主线程零遍历。
  2. 地形分块策略
    • 世界空间固定尺寸 Patch(如 64×64 顶点),离线烘焙高度图、法线图、Splat 权重图;
    • 每 Patch 生成 Cluster(8×8 或 16×16),作为剔除与 LOD 最小单元。
  3. 四叉树 LOD 的 GPU 化
    • CPU 端仅维护“世界到 Patch”的常量缓冲,不展开四叉树
    • Compute Shader 每帧并行遍历 Patch,用屏幕误差 ρ = (d · τ) / (D · W) 计算 LOD,d 为到相机距离,D 为 Patch 边长,τ 为像素误差阈值,W 为屏幕宽;
    • 输出 Indirect Args Buffer(DrawIndexedIndirect),格式兼容 Unity 的 Graphics.DrawMeshIndirect。
  4. 遮挡剔除
    • 生成 HZB(Hierarchical Z-Buffer),CS 内用 mip 链做保守光栅测试,剔除被挡 Patch;
    • 移动端可降采样到 1/4 分辨率,减少带宽。
  5. 顶点压缩
    • 高度图采样后,顶点 xy 用 Patch 局部 16 bit 存,z 用 16 bit UNORM 高度,节省 50% 带宽
    • 法线用 octahedron 编码 2×8 bit,替代传统 3×16 bit。
  6. 材质系统
    • 一张 Virtual Texture(VT) 存 Splat 权重,运行时按需 Page 加载;
    • 每 Patch 仅提交一个 MaterialID,SRP Batch 合批,一次 DrawCall 覆盖全地形
  7. 移动平台适配
    • 在 Adreno/Mali 上关闭 Cluster culling 的线程组同步,改用分组 LOD 避免 warp 分化;
    • 用 Unity 的 Platform #define MOBILE_HZB_HALF 开关,低端机回退到距离剔除。

答案

“我上一个数字孪生港口项目需要 16 km×16 km 地形,Unity Terrain 在移动端只能跑 18 FPS,于是自研 GPU Driven 方案,核心四步:

  1. 离线预处理

    • 把 8k×8k 高度图切成 128×128 个 64×64 Patch,每 Patch 导出 .bytes 高度、法线、Splat 权重;
    • 预生成 5 级 LOD 索引缓冲(0~4 级),存进 GPUBuffer,运行时不再 CPU 侧分配内存
  2. 运行时 Compute Shader 剔除

    • 每帧一 Dispatch,线程组一一对应 Patch,计算 LOD 等级与可见性;
    • 用 HZB 做遮挡剔除,剔除率 85%+
    • 将可见 Patch 的 (LOD, MaterialID, PatchWorldPos) 写进 AppendStructuredBuffer,再 Unordered Access 转 Indirect Args。
  3. 渲染提交

    • 只调用一次 Graphics.DrawMeshIndirect(mesh, 0, argsBuffer, 0, castShadow: Off, receiveShadow: Off)
    • 顶点着色器用 SV_InstanceID 读取 Patch 常量,采样高度图还原世界坐标;
    • 片元着色器采样 VT,单 Pass 完成 Albedo/Normal/Specular
  4. 性能结果

    • 华为 Mate40 上,16 km 地形、200+ DrawCall → 1 DrawCall,帧率 18 → 58 FPS,GPU 占用 34 %;
    • 内存下降 120 MB(去掉 Unity Terrain 的 Heightmap 树)。

落地时踩过两个坑:

  • Mali-G78 对 Cluster 线程组同步极敏感,把 GroupSize 从 64 降到 32 后 warp 分化消失;
  • WebGL2 不支持 Compute,用 Vertex Shader 模拟 Instance Culling,把可见列表当额外顶点流,性能损失 15 % 但可接受。”

拓展思考

  1. 与 Nanite 的差异:Nanite 用 Cluster 剔除+软光栅做微多边形,Unity 无软光栅,可用 Hardware Conservative Raster(OpenGL ES 3.2 AEP)近似,但兼容性差;国内项目更务实,VT+HZB 已能跑满 60 FPS
  2. 植被扩展:把草、石头当 Mesh Cluster 一并扔进同一 Indirect Buffer,用 SV_InstanceID 的高位标记“地形/植被”,一次 DrawCall 渲染地表+装饰,在开放世界手游中已验证。
  3. SRP 升级:URP 2022.3 的 RenderGraph 把 HZB 生成做成 Native Pass,可省 1 ms CPU;HDRP 的 APV(Adaptive Probe Volume) 与 GPU Terrain 结合,实现全场景 GPU 光照,适合下一代数字孪生。