解释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以内

知识点

  1. SBRG渲染管线位置:位于Custom SRP的Render Pass中,不依赖GameObject,只认BatchRendererGroup.AddBatch时传入的RendererDescription(worldBounds、layer、materialID…)。
  2. CPU Culling入口:继承ScriptableRenderContextBeginCulling回调,在OnPerformCulling委托里收到BatchCullingInput(包含viewType、viewMatrix、projectionMatrix、cullingPlanes[]、lodParameters等),全部数据都是burst-compatible的blittable结构
  3. 剔除算法
    • 平面剔除:用Burst编译的SIMD一次性测试8个Plane,AABB与Plane距离>0则丢弃。
    • LOD&遮挡:可手动接入Octree+Hi-Z GPU Occlusion回读结果,或预烘焙PVS,回读结果用NativeBitArray标记可见性,避免主线程同步。
    • 层掩码:用uint layerMask = 1u << gameLayer & input.viewLayerMask一次位运算完成。
  4. 输出结构:填充BatchCullingOutput里的drawCommands(NativeList<BatchDrawCommand>),每个元素记录visibleIndex、materialID、meshID、subMeshIndex、perInstanceDataOffset内存布局必须与GPU Instancing格式对齐
  5. 线程模型OnPerformCulling默认在Unity渲染工作线程调用,不与主线程抢时间片;若手动调用CompleteCullingAsync可再丢给JobSystem,实现多相机并行剔除
  6. 性能红线
    • 禁止在Culling里new任何托管对象,全部用NativeArray+UnsafeList
    • 禁止访问Transform.component所有bounds提前缓存到NativeHashMap<Entity,AABB>
    • 安卓低端机关卡**<5000个静态物体时,Culling耗时必须<0.2 ms**,否则面试官会直接判“经验不足”。

答案

ScriptableBatchRendererGroup的CPU Culling是指在C#层利用Burst+Jobs对渲染实体进行可见性筛选,并直接填充BatchCullingOutput.drawCommands供GPU Instancing使用,完全绕过引擎默认的GameObject与Renderer.Culling。核心步骤:

  1. BatchRendererGroup.onPerformCulling += MyCullingCallback注册回调;
  2. 在回调里用BatchCullingInput.cullingPlanesSIMD视锥剔除,用layerMasklodParameters层与LOD剔除,可额外读回GPU Occlusion的可见性BitArray;
  3. 将可见物体的visibleIndexmaterialIDmeshID按顺序写入BatchCullingOutput.drawCommands内存连续、无GC
  4. 返回后Unity渲染线程直接拿这份命令列表做DrawMeshInstancedProcedural主线程零等待
    这样能把Culling耗时压到亚毫秒级,在荣耀9X、红米Note10等低端机上静态场景5000物体+动态角色200人仍可维持60 FPS

拓展思考

  1. 移动端GPU Occlusion回读延迟2~3帧,如何做到无pop
    答:用双缓冲历史可见性+渐进式fade,在Culling Job里混合上一帧可见性与当前回读结果,alpha阈值每帧插值0.05,玩家肉眼无法察觉。
  2. 大世界海量植被(>100 k棵草)如何用SBRG?
    答:把草地按8×8×8m Chunk预烘焙到ComputeBuffer IndirectArgs,CPU Culling只到Chunk级别,GPU端再用CS做细粒度剔除,实现0.1 ms CPU耗时+百万草渲染
  3. 热更新框架(HybridCLR)能否动态注册新的BatchRendererGroup
    答:可以,但必须在主线程提前new BatchRendererGroup并把onPerformCulling绑定到AOT注册的静态委托,热更层只能替换委托里的逻辑,不能new新的委托,否则il2cpp会触发JIT异常