如何剔除屏幕外草
解读
面试官问“如何剔除屏幕外草”,并不是想听“把草删掉”这种表面答案,而是考察你对大规模植被渲染的系统性优化思路。国内项目(尤其是开放世界、数字孪生、军事仿真)动辄要在移动端跑几十万株草,GPU 耗时与带宽是性能红线。能否用CPU 级剔除 + GPU 级裁剪 + 引擎级合批三级联动,把“看不见”的草在提交 DrawCall 前就干掉,是评分关键。回答必须体现:
- 你理解 Unity 的渲染提交管线(Culling → Batch → Draw)。
- 你能针对草这种面片密集、分布随机、随风摆动的特殊对象给出量化指标(屏幕误差、像素占比、HZB 层级)。
- 你掌握移动端 Tile-Based 架构的带宽瓶颈,知道为什么“草”比“石头”更怕 Overdraw。
知识点
- 视锥剔除(Frustum Culling)
Unity 内置 CullingGroup API,可传入自定义包围盒数组,在 JobSystem 中并行筛选,1 万簇草地耗时 < 0.2 ms。 - 遮挡剔除(Occlusion Culling)
移动端不适合 GPU 实时 HZB,需预烘焙草地密度图(Density Map),运行时按像素采样,一次采样决定 4×4 m² 区域是否丢弃,内存占用 < 0.5 MB。 - 距离裁剪与 LOD
国内主流标准:< 20 m 高模草(3 层交叉面片),20–60 m 低模草(1 层面片),> 60 m 直接剔除,切换距离用屏幕误差公式计算,保证 1080p 下像素占比 < 0.5 %。 - 硬件级 Early-Z 与 Cluster Culling
在 Shader 里做簇级裁剪:把 32 株草打包成一个 Cluster,传入中心坐标与半径,在 VS 中用球体投影测试判断整簇是否落在屏幕外,若不可见则直接clip(-1),节省 32 次 VS 插值。 - GPU Instancing + DOTS Instanced Rendering
使用Graphics.RenderMeshIndirect,配合 ComputeBuffer 做可见性索引压缩,把剔除后的索引压成连续数组,一次 Draw 调用渲染 65535 株草,在 Mali-G78 上 CPU 端耗时 < 0.05 ms。 - 平台差异
iOS A 系列芯片对无索引的三角形带更友好;高通 Adreno 对小实例化尺寸(< 256 字节)带宽最优;华为麒麟 9000 需关Early-Z 强制深度写入以避免草面片 AlphaTest 击穿。
答案
分三步落地:
- 数据预处理
在 Editor 阶段把草地分块(Chunk=16×16 m),每块预烘焙密度图与高度图,同时生成 3 级 LOD 网格,顶点色 a 通道存储弯曲强度,减少运行时计算。 - 运行时 CPU 剔除
主线程每帧仅更新玩家周围 9 块 Chunk,用CullingGroup.Parallel做视锥剔除,返回可见索引列表;随后用密度图做遮挡剔除,不可见 Chunk 直接跳过,耗时 < 0.3 ms。 - GPU 端零开销裁剪
将可见 Chunk 的草簇中心写进StructuredBuffer<float4> clusterData,在 Compute Shader 中并行判断球体屏幕投影半径,若投影 < 1 pixel 则标记不可见;最终用RenderMeshIndirect提交,DrawCall 数量 = 可见 Chunk 数(通常 < 30),在红米 Note12 Pro 上 60 fps 可跑 80 万株草,GPU 帧时间 < 12 ms。
拓展思考
- 风场交互
如果项目需要“角色踩倒草地”,可在 Compute Shader 里用SDF 碰撞体实时修改弯曲强度,但需把结果双缓冲回读 CPU 做物理碰撞,避免 GPU-CPU 同步 stall。 - WebGL 兼容性
WebGL2.0 不支持RenderMeshIndirect,可退化成实例化数组 + 可见性掩码,用uint位域打包 32 株可见性,一次 Draw 最多 1023 个实例,在 Chrome 移动端仍可跑 20 万株。 - 数字孪生超大场景
当场景 > 64 km² 时,密度图分辨率需四叉树流式加载,用Addressables+LZ4 压缩保证内存 < 200 MB;同时把剔除算法搬进Unity ECS Sytem,利用Chunk-Based 并行把 CPU 耗时再降 50 %。