在场景中放置数百棵树、数千颗石头、大量特效粒子------每帧的 DrawCall 数量直接决定了游戏的帧率上限。 GPU Instancing 让 CPU 只发起一次绘制指令,GPU 就能把相同 Mesh 渲染到不同位置、不同颜色的数百个物体上。本文系统讲解其原理、URP 配置方式以及代码实现细节。
1DrawCall 是什么,为什么昂贵
每当 CPU 要求 GPU 绘制一批三角形时,就会发出一次 DrawCall 。在此之前,CPU 还需要完成 状态设置(State Setup) :上传材质参数、绑定纹理、切换着色器变体......这些工作发生在 CPU 侧,并且 每次 DrawCall 都要重复一遍。
当场景中有 500 棵树,每棵树是独立 GameObject,Unity 就会发出接近 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 管线略有不同,共有三种方式:
A
Material Inspector 一键开启
选中使用 URP/Lit 或自定义 Shader 的 Material → Inspector 面板底部 → 勾选 Enable GPU Instancing。这是最简单的方式,适合不需要额外属性的场景。
B
Graphics.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。
cs
// InstancedColorUnlit.shader --- URP 自定义 Unlit,支持每实例颜色
Shader "Custom/InstancedColorUnlit"
{
Properties
{
_BaseColor ("Base Color", Color) = (1,1,1,1)
}
SubShader
{
Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" }
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.DrawMeshInstanced
Graphics.DrawMeshInstanced 允许在不创建任何 GameObject 的情况下, 每帧向 GPU 提交最多 1023 个实例 (单次调用限制)。 超过 1023 时需要手动分批或改用 DrawMeshInstancedIndirect。
cs
using UnityEngine;
/// <summary>
/// 使用 Graphics.DrawMeshInstanced 批量绘制 N 个实例
/// 不创建任何 GameObject,完全在 CPU+GPU 层面工作
/// </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 时"的推荐做法。
cs
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 = GetComponent<Renderer>();
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 | 小三角形数量的动态物体 | ❌ 不支持 | ✅ 支持 | 低 | 顶点数 < 900 |
| SRP Batcher | URP/HDRP 下任意 Shader | ❌ 不支持(CBer 统一) | ✅ 支持 | 低 | 无明确限制 |
| GPU Instancing | 大量相同 Mesh 的物体 | ✅ 支持(颜色/属性) | ✅ 支持 | 较低 | 1023(DrawMeshInstanced) |
| Indirect Instancing | GPU 端驱动,超大数量 | ✅ 完全支持 | ✅ 支持 | 最低(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 :超出时在应用层分段循环调用,或切换至 DrawMeshInstancedIndirect(ComputeBuffer 方案,无数量限制)。
