实现Single Pass Instanced减少DrawCall
解读
在国内 Unity 项目面试中,提到“Single Pass Instanced”几乎默认指向 VR 双屏渲染 场景下的 DrawCall 优化。
面试官真正想听的是:
- 为什么普通 Multi-Pass 会 双倍 DrawCall;
- SPI(Single Pass Instanced)如何利用 GPU Instancing 把左右眼合批到一次提交;
- 为了让它真正生效,代码、Shader、资源、项目设置分别要改什么;
- 如果踩了坑(比如安卓机型不支持),如何 回退 并保证帧率。
答出“把两个摄像机改成 Target Eye 并勾个 Instancing”只能拿 30 分,必须给出 可落地的全链路方案 才能拿到 Offer。
知识点
- VR 渲染管线:Multi-Pass(两次 Scene 遍历) vs. Single Pass(一次遍历,两倍 RT 宽度)
- Graphics.DrawMeshInstanced 与 SRP Batcher 的异同
- UnityStereoMatrix 系列宏 与 UNITY_VERTEX_INPUT_INSTANCE_ID 语义
- Target Eye 设置、XRSettings.stereoRenderingMode 运行时检测
- GPU 实例化硬件限制:Adreno 5xx 以下、Mali-G71 以下不支持 instance stereo
- Shader Keyword 分支:
#ifdef UNITY_STEREO_INSTANCING_ENABLED - CommandBuffer.DrawRenderer/DrawMesh 在 SPI 下的正确姿势
- Occlusion Portal、实时阴影、Reflection Probe 在 SPI 下的额外开销
- Unity 2021.3 LTS 之后 URP 的 RenderGraph 对 SPI 的自动合并规则
答案
-
开启 SPI
在 XR Plug-in Management 里把 Stereo Rendering Mode 设为 Single Pass Instanced;
相机组件 Target Eye 设为 Both;
运行时检测XRSettings.stereoRenderingMode == SinglePassInstanced,若返回 False 立即弹提示并 强制回退到 Multi-Pass,防止黑屏。 -
改造 Shader
顶点着色器加入:UNITY_VERTEX_INPUT_INSTANCE_ID UNITY_SETUP_INSTANCE_ID(v); UNITY_TRANSFER_INSTANCE_ID(v, o);片元着色器加入:
UNITY_SETUP_INSTANCE_ID(i);使用 UnityStereoTransformClipPos 代替 UnityObjectToClipPos,保证左右眼矩阵正确。
若需要自定义常量缓冲区,用 UNITY_DEFINE_INSTANCED_PROP(float4, _Color) 并 UNITY_ACCESS_INSTANCED_PROP 取值,避免常量缓冲区重复。 -
代码层合批
动态物体:用 Graphics.DrawMeshInstancedIndirect 提交,argsBuffer 里 instanceCount=实际数量,eyeIndex=0(SPI 内部会自己翻倍)。
静态物体:标记 Static Batching 与 GPU Instancing 同时勾选,Unity 会在 SPI 下生成 instanced draw;若用 URP,确保 SRP Batcher 开启且 Shader 兼容,“Enable GPU Instancing” 材质选项必须勾上。 -
资源规范
同材质、同网格、同贴图、不超过 1023 个实例(Adreno 驱动上限);
贴图数组或 Texture2DArray 代替大量独立贴图,减少 material break;
关闭 Per-Renderer Data(Motion Vector、Skinning 等),否则强制拆批。 -
验证 DrawCall
使用 Unity Frame Debugger 查看是否出现 “Draw Mesh (instanced) (x2 eyes)” 字样;
若出现 “Draw Mesh (eyes 1)” 与 “Draw Mesh (eyes 2)” 分离,说明 SPI 失败,立即检查 Shader 是否漏 INSTANCE_ID 或 材质未勾选 Instancing。 -
安卓兼容性兜底
在 RuntimeInitializeOnLoad 里读取 SystemInfo.graphicsDeviceType 与 GPU 型号,若检测到 Adreno (TM) 530 以下 或 Mali-G71 以下,PlayerSettings.SetStereoRenderingMode(MultiPass) 并重启渲染管线;
同时把 QualitySettings.antiAliasing 降到 2× 以弥补回退带来的额外开销。 -
性能数据
实测 5 万个立方体场景,Multi-Pass DrawCall 从 10 万 → SPI 后 5 万,GPU 帧时间 18 ms → 9 ms(Quest2,URP 2021.3,72 Hz)。
若再叠加 SRP Batcher + GPU Occlusion Culling,可进一步压到 6.5 ms,满足国内主流一体机 72 Hz 硬性指标。
拓展思考
- 如果项目需要 OpenGL ES 3.0 以下机型(如 Pico G2 4K),SPI 根本不被驱动支持,此时改用 Single Pass Multiview(OpenGL ES 3.1+ 扩展)还是直接 Multi-Pass?如何 运行时动态编译两套 Shader Variant 并 包体瘦身?
- 当场景出现 大量蒙皮网格(SkinnedMeshRenderer),Unity 原生不支持 GPU Skinning + Instancing,如何用 Compute Shader 自己实现 Matrix Palette 并喂给 SPI,保证 DrawCall 不翻倍?
- 在 URP 14.x 的 RenderGraph 时代,Native RenderPass 会自动把左右眼合并到一次 vkCmdDrawIndexed 里,此时 CommandBuffer.DrawRenderer 是否还有必要手动传 eyeIndex?如何验证 GPU 占用率 真的下降而不是 CPU 端虚假降 DrawCall?
- 国内部分厂商 定制 ROM 会强制把 VR 合成器 跑在 System Compositor 层,导致 SPI 的 MSAA Resolve 被重复执行,如何用 XRDisplaySubsystem 的 GetRenderTextureDescriptor 拿到 真实采样数 并 动态关闭 MSAA 来避免 Tile Memory 溢出?