在场景中放置数百棵树、数千颗石头、大量特效粒子——每帧的 DrawCall 数量直接决定了游戏的帧率上限。 GPU Instancing 让 CPU 只发起一次绘制指令GPU 就能把相同 Mesh 渲染到不同位置、不同颜色的数百个物体上。本文系统讲解其原理、URP 配置方式以及代码实现细节。1DrawCall 是什么为什么昂贵每当 CPU 要求 GPU 绘制一批三角形时就会发出一次DrawCall。在此之前CPU 还需要完成状态设置State Setup上传材质参数、绑定纹理、切换着色器变体……这些工作发生在 CPU 侧并且每次 DrawCall 都要重复一遍。当场景中有 500 棵树每棵树是独立 GameObjectUnity 就会发出接近 500 次 DrawCall。 CPU 在状态切换上耗尽了时间GPU 的绝大部分计算单元却在等待指令处于空闲状态。 这就是CPU 成为瓶颈的本质原因。经验法则移动端帧率目标 60fps 时建议每帧 DrawCall 控制在 100 以内PC 端相对宽松但超过 2000 时也会明显感受到 CPU 瓶颈。2GPU Instancing 工作原理GPU Instancing 的核心思路是一次 DrawCall 一个实例数据缓冲区Instance Buffer。 CPU 将所有实例的差异化数据位置矩阵、颜色、自定义属性……打包进一个结构化缓冲区上传 GPU GPU 在执行顶点着色器时用内置变量unity_InstanceID索引该缓冲区取出自己的那份数据 在同一批三角形上独立完成变换和着色。两个关键前提相同 Mesh相同 Material同一 Shader 变体不同 Transform位置/旋转/缩放不同每实例属性颜色、自定义 Float 等只要 Mesh 和 Material 相同Unity 就可以自动合批若每个实例有差异化颜色则需要通过MaterialPropertyBlock或在 Shader 中声明UNITY_INSTANCING_BUFFER来传递每实例数据。3URP 中启用 GPU Instancing在 URP 下GPU Instancing 的启用路径与 Built-in 管线略有不同共有三种方式AMaterial Inspector 一键开启选中使用 URP/Lit 或自定义 Shader 的 Material → Inspector 面板底部 → 勾选Enable GPU Instancing。这是最简单的方式适合不需要额外属性的场景。BGraphics.DrawMeshInstanced / DrawMeshInstancedIndirect通过 C# API 手动调度完全绕过 GameObject 系统。适合粒子、群集 AI、程序化生成场景可与 ComputeShader 配合实现 GPU 端驱动绘制。C自定义 URP Shader 支持 Instancing在 Shader 中添加#pragma multi_compile_instancing和UNITY_INSTANCING_BUFFER_START宏即可声明每实例属性颜色、强度等由 Unity 运行时自动填充。URP 注意URP 的 Forward Renderer 默认支持 GPU Instancing但需要确保Universal Render Pipeline Asset中没有关闭批处理选项SRP Batcher与GPU Instancing不同后者在 SRP Batcher 开启时对自定义属性仍有效。4Shader 编写支持 Instancing 的 URP Lit下面是一个完整的 URP UnlitShader支持 GPU Instancing 并允许每个实例拥有独立颜色。 重点关注三个宏multi_compile_instancing、UNITY_INSTANCING_BUFFER_START以及顶点着色器中的UNITY_SETUP_INSTANCE_ID。// InstancedColorUnlit.shader — URP 自定义 Unlit支持每实例颜色 Shader Custom/InstancedColorUnlit { Properties { _BaseColor (Base Color, Color) (1,1,1,1) } SubShader { Tags { RenderTypeOpaque RenderPipelineUniversalPipeline } Pass { Name ForwardLit Tags { LightMode UniversalForward } ​ HLSLPROGRAM #pragma vertex vert #pragma fragment frag // ↓ 关键开启 Instancing 变体 #pragma multi_compile_instancing ​ #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl ​ // ── 每实例属性缓冲区 ── UNITY_INSTANCING_BUFFER_START(_Props) UNITY_DEFINE_INSTANCED_PROP(float4, _BaseColor) UNITY_INSTANCING_BUFFER_END(_Props) ​ struct Attributes { float4 positionOS : POSITION; UNITY_VERTEX_INPUT_INSTANCE_ID }; ​ struct Varyings { float4 positionCS : SV_POSITION; float4 color : COLOR; }; ​ Varyings vert(Attributes IN) { UNITY_SETUP_INSTANCE_ID(IN); // 绑定当前实例 ID Varyings OUT; OUT.positionCS TransformObjectToHClip(IN.positionOS.xyz); // 读取该实例自己的颜色 OUT.color UNITY_ACCESS_INSTANCED_PROP(_Props, _BaseColor); return OUT; } ​ half4 frag(Varyings IN) : SV_Target { return IN.color; } ENDHLSL } } }核心在于UNITY_INSTANCING_BUFFER_START(_Props)~UNITY_INSTANCING_BUFFER_END(_Props)之间声明的属性Unity 会为每个实例维护一份独立副本通过UNITY_ACCESS_INSTANCED_PROP访问。5C# 脚本Graphics.DrawMeshInstancedGraphics.DrawMeshInstanced允许在不创建任何 GameObject 的情况下 每帧向 GPU 提交最多1023 个实例单次调用限制。 超过 1023 时需要手动分批或改用DrawMeshInstancedIndirect。using UnityEngine; ​ /// summary /// 使用 Graphics.DrawMeshInstanced 批量绘制 N 个实例 /// 不创建任何 GameObject完全在 CPUGPU 层面工作 /// /summary public class InstancedRenderer : MonoBehaviour { [Header(Mesh Material)] public Mesh instanceMesh; public Material instanceMaterial; ​ [Header(Instancing)] public int instanceCount 500; public Vector3 spawnRange new Vector3(20f, 0f, 20f); ​ // ── 内部缓存 ────────────────────────── private Matrix4x4 [] _matrices; private MaterialPropertyBlock _mpb; private static readonly int ColorID Shader.PropertyToID(_BaseColor); ​ void Start () { _matrices new Matrix4x4[instanceCount]; _mpb new MaterialPropertyBlock(); ​ var colors new Vector4[instanceCount]; ​ for (int i 0; i instanceCount; i) { // 随机位置 Vector3 pos new Vector3( Random.Range(-spawnRange.x, spawnRange.x), 0f, Random.Range(-spawnRange.z, spawnRange.z)); _matrices[i] Matrix4x4.TRS(pos, Quaternion .identity, Vector3 .one); ​ // 随机颜色将存入 PropertyBlock colors[i] new Vector4( Random.value, Random.value, Random.value, 1f); } ​ // 批量设置颜色到 MaterialPropertyBlock _mpb.SetVectorArray(ColorID, colors); } ​ void Update () { // ★ 每帧一次调用 → GPU 渲染 instanceCount 个实例 Graphics .DrawMeshInstanced ( instanceMesh, 0, // submeshIndex instanceMaterial, _matrices, instanceCount, _mpb); } }性能提示在Awake/Start中预先分配矩阵数组和MaterialPropertyBlock 在Update中每帧调用DrawMeshInstanced但不要每帧重新new数组—— 这会触发大量 GC 分配反而拖慢性能。6MaterialPropertyBlock每实例差异化颜色若使用场景中真实存在的 GameObject而非纯脚本绘制可以通过Renderer.SetPropertyBlock配合MaterialPropertyBlock实现在不破坏合批的前提下为每个实例设置不同颜色——这是有 GameObject 时的推荐做法。using UnityEngine; ​ /// summary /// 挂载在每个 GameObject 上通过 MaterialPropertyBlock /// 设置独立颜色不破坏 GPU Instancing 合批 /// /summary [RequireComponent(typeof(Renderer))] public class PerInstanceColor : MonoBehaviour { [SerializeField] Color instanceColor Color.white; ​ // PropertyID 缓存避免每帧 string hash private static readonly int ColorID Shader .PropertyToID(_BaseColor); ​ void Start () { var rend GetComponentRenderer(); var mpb new MaterialPropertyBlock(); ​ // 先读取已有值再覆盖目标属性避免清除其他属性 rend.GetPropertyBlock(mpb); mpb.SetColor(ColorID, instanceColor); ​ // ★ SetPropertyBlock 不会创建材质副本 rend.SetPropertyBlock(mpb); } }常见误区直接修改renderer.material.color会为该 GameObject 创建一份材质副本Instance Material 破坏合批并增加内存。应始终使用MaterialPropertyBlock或renderer.sharedMaterial。7与 SRP Batcher / Static Batching 的区别批处理方式适用场景每实例属性运行时动态移动内存开销最大实例数Static Batching完全静止的物体岩石、建筑❌ 不支持❌ 不支持高合并顶点缓存—Dynamic Batching小三角形数量的动态物体❌ 不支持✅ 支持低顶点数 900SRP BatcherURP/HDRP 下任意 Shader❌ 不支持CBer 统一✅ 支持低无明确限制GPU Instancing大量相同 Mesh 的物体✅ 支持颜色/属性✅ 支持较低1023DrawMeshInstancedIndirect InstancingGPU 端驱动超大数量✅ 完全支持✅ 支持最低GPU 缓冲区无限制SRP Batcher vs GPU Instancing两者可以共存。SRP Batcher 优化的是 Shader 常量缓冲区的上传效率 GPU Instancing 减少的是 DrawCall 次数本身。对于相同 Mesh 差异化颜色的场景GPU Instancing 是唯一选项 对于不同 Mesh 相同 Shader的场景SRP Batcher 更合适。8性能对比与注意事项帧率 / DrawCall 提升估算1000 个相同 Cube注意事项 Checklist⚠️阴影 DrawCall 独立计算Shadow Caster Pass 与 Main Pass 分开启用阴影后 DrawCall 约翻倍。可在 URP Asset 中限制阴影距离减少参与阴影的实例数。⚠️Skinned Mesh 不支持 DrawMeshInstanced骨骼动画网格需改用 GPU Skinning ComputeBuffer或使用第三方方案Animancer GPU / AnimationBaker。✅Frustum Culling 仍然有效Unity 会在 CPU 侧剔除不在视锥内的实例不会因为 Instancing 关闭剔除。✅LOD Group 与 Instancing 兼容不同 LOD 等级的实例会分批提交不影响合批逻辑但不同 LOD 属于不同批次。ℹ️DrawMeshInstanced 单次上限 1023超出时在应用层分段循环调用或切换至DrawMeshInstancedIndirectComputeBuffer 方案无数量限制。