Unity URP 下的 GPU Instancing减少 DrawCall 的关键技术

在场景中放置数百棵树、数千颗石头、大量特效粒子------每帧的 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_instancingUNITY_INSTANCING_BUFFER_START 宏,即可声明每实例属性(颜色、强度等),由 Unity 运行时自动填充。

URP 注意: URP 的 Forward Renderer 默认支持 GPU Instancing,但需要确保 Universal Render Pipeline Asset 中没有关闭批处理选项(SRP BatcherGPU Instancing 不同,后者在 SRP Batcher 开启时对自定义属性仍有效)。

4Shader 编写:支持 Instancing 的 URP Lit

下面是一个完整的 URP UnlitShader,支持 GPU Instancing 并允许每个实例拥有独立颜色。 重点关注三个宏:multi_compile_instancingUNITY_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), 破坏合批并增加内存。应始终使用 MaterialPropertyBlockrenderer.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 方案,无数量限制)。

相关推荐
小贺儿开发2 小时前
Unity3D LED点阵屏幕模拟
http·unity·浏览器·网络通信·led·互动·点阵屏
RReality4 小时前
【Unity Shader】 溶解效果实战教程
unity·游戏引擎
mxwin4 小时前
Unity URP SRP Batcher 完全指南 URP/HDRP 下的核心批处理机制,大幅降低 CPU 开销
unity·游戏引擎·shader·单一职责原则
小清兔4 小时前
unity中的音频相关_笔记
笔记·unity·音视频
RPGMZ4 小时前
RPGMZ游戏引擎 宠物战斗游戏基础功能实现
javascript·游戏·游戏引擎·宠物·rpgmz·rpgmakermz·宠物战斗系统
The森17 小时前
cocos2d-x棋牌项目-模块2:GameView、Node 与 zOrder
游戏引擎·cocos2d
mxwin19 小时前
Unity Shader 渲染队列 (Render Queue):控制 Geometry、Transparent、Overlay 等队列确保半透明物体渲染正确
unity·游戏引擎
mxwin20 小时前
Unity Shader Alpha Test 与 Alpha Blend:透明度测试与混合的实现及排序问题
unity·游戏引擎
小贺儿开发20 小时前
Unity3D 拼图互动游戏
游戏·unity·人机交互·2d·拼图·互动