解释ScriptableBatchRendererGroup的CPU Culling
解读
国内Unity项目普遍追求“低端机也能跑60帧”,而Draw Call与Culling开销往往是瓶颈。ScriptableBatchRendererGroup(SBRG)是Unity 2021.2后正式开放的高性能批量渲染接口,它把“哪些物体可见”的决策权从引擎内部拿到用户侧,允许我们在C#层完全自定义Culling逻辑,从而绕过引擎默认Pipeline的冗余计算,实现零GC、多线程、Burst加速的可见性筛选。面试官问“CPU Culling”并不是想听“视锥剔除”概念,而是想看候选人是否亲手写过SBRG的Culling代码,是否理解Unity渲染线程与主线程同步机制,以及能否在真机低端安卓上把Culling耗时压到0.2 ms以内。
知识点
- SBRG渲染管线位置:位于Custom SRP的Render Pass中,不依赖GameObject,只认BatchRendererGroup.AddBatch时传入的RendererDescription(worldBounds、layer、materialID…)。
- CPU Culling入口:继承
ScriptableRenderContext的BeginCulling回调,在OnPerformCulling委托里收到BatchCullingInput(包含viewType、viewMatrix、projectionMatrix、cullingPlanes[]、lodParameters等),全部数据都是burst-compatible的blittable结构。 - 剔除算法:
- 平面剔除:用Burst编译的SIMD一次性测试8个Plane,AABB与Plane距离>0则丢弃。
- LOD&遮挡:可手动接入Octree+Hi-Z GPU Occlusion回读结果,或预烘焙PVS,回读结果用NativeBitArray标记可见性,避免主线程同步。
- 层掩码:用
uint layerMask = 1u << gameLayer & input.viewLayerMask一次位运算完成。
- 输出结构:填充
BatchCullingOutput里的drawCommands(NativeList<BatchDrawCommand>),每个元素记录visibleIndex、materialID、meshID、subMeshIndex、perInstanceDataOffset,内存布局必须与GPU Instancing格式对齐。 - 线程模型:
OnPerformCulling默认在Unity渲染工作线程调用,不与主线程抢时间片;若手动调用CompleteCullingAsync可再丢给JobSystem,实现多相机并行剔除。 - 性能红线:
- 禁止在Culling里
new任何托管对象,全部用NativeArray+UnsafeList。 - 禁止访问
Transform.component,所有bounds提前缓存到NativeHashMap<Entity,AABB>。 - 安卓低端机关卡**<5000个静态物体时,Culling耗时必须<0.2 ms**,否则面试官会直接判“经验不足”。
- 禁止在Culling里
答案
ScriptableBatchRendererGroup的CPU Culling是指在C#层利用Burst+Jobs对渲染实体进行可见性筛选,并直接填充BatchCullingOutput.drawCommands供GPU Instancing使用,完全绕过引擎默认的GameObject与Renderer.Culling。核心步骤:
- 在
BatchRendererGroup.onPerformCulling += MyCullingCallback注册回调; - 在回调里用
BatchCullingInput.cullingPlanes做SIMD视锥剔除,用layerMask与lodParameters做层与LOD剔除,可额外读回GPU Occlusion的可见性BitArray; - 将可见物体的
visibleIndex、materialID、meshID按顺序写入BatchCullingOutput.drawCommands,内存连续、无GC; - 返回后Unity渲染线程直接拿这份命令列表做DrawMeshInstancedProcedural,主线程零等待。
这样能把Culling耗时压到亚毫秒级,在荣耀9X、红米Note10等低端机上静态场景5000物体+动态角色200人仍可维持60 FPS。
拓展思考
- 移动端GPU Occlusion回读延迟2~3帧,如何做到无pop?
答:用双缓冲历史可见性+渐进式fade,在Culling Job里混合上一帧可见性与当前回读结果,alpha阈值每帧插值0.05,玩家肉眼无法察觉。 - 大世界海量植被(>100 k棵草)如何用SBRG?
答:把草地按8×8×8m Chunk预烘焙到ComputeBuffer IndirectArgs,CPU Culling只到Chunk级别,GPU端再用CS做细粒度剔除,实现0.1 ms CPU耗时+百万草渲染。 - 热更新框架(HybridCLR)能否动态注册新的
BatchRendererGroup?
答:可以,但必须在主线程提前new BatchRendererGroup并把onPerformCulling绑定到AOT注册的静态委托,热更层只能替换委托里的逻辑,不能new新的委托,否则il2cpp会触发JIT异常。