当前位置: 首页 > news >正文

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

// 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

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 时"的推荐做法。

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 BatcherURP/HDRP 下任意 Shader❌ 不支持(CBer 统一)✅ 支持无明确限制
GPU Instancing大量相同 Mesh 的物体✅ 支持(颜色/属性)✅ 支持较低1023(DrawMeshInstanced)
Indirect 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:超出时在应用层分段循环调用,或切换至DrawMeshInstancedIndirect(ComputeBuffer 方案,无数量限制)。

http://www.jsqmd.com/news/585007/

相关文章:

  • 生活真正的难,不是没人帮你,而是很多时候只能靠自己慢慢熬过去
  • 【高清视频】PCIe 5.0 144 Lane 8槽位 PCIe Switch卡实拍讲解
  • Local AI MusicGen成本效益:相比外包音乐制作节省90%开支
  • 结束语 从写代码到指挥AI 写代码你的下一个十年
  • OpenClaw技能开发入门:为Phi-3-vision-128k-instruct定制截图OCR模块
  • 操作系统原理学习助手:Phi-4-mini-reasoning解答进程、线程与内存管理难题
  • Qwen2.5-VL-7B-Instruct环境部署:torch29环境兼容性验证与降级策略
  • 重新定义人机交互:Agent时代的产品设计新思维
  • 快速上手AI开发:PyTorch-2.x-Universal-Dev-v1.0镜像使用全攻略
  • Pixel Language Portal 开发环境配置:WSL 中 Ubuntu 系统与模型本地测试
  • 实测Image-to-Video图像转视频生成器:高清流畅的视频生成效果
  • Ostrakon-VL视觉扫描与MySQL数据关联:跨模态信息检索实战
  • 使用阿里小云KWS模型构建多语言语音唤醒系统
  • DDColor黑白照片智能上色:人物修复选460-680,建筑修复选960-1280
  • 【Winform】控件修改需要注意的事项
  • Qt 点击按钮组切换界面
  • SmallThinker-3B开源镜像实操:边缘部署+草稿加速双场景落地指南
  • 文墨共鸣大模型C盘清理建议与垃圾文件智能识别
  • 藏在Claude Code里的小惊喜!187种Loading状态词,告别单调编程等待
  • opencode和文心快码比较
  • 清音刻墨惊艳案例:Qwen3为儿童故事音频生成带停顿标记的SRT字幕
  • 基于STM32的电气火灾监测无线有线传输系统探索
  • CogVideoX-2b真实案例:从“一只橘猫骑摩托”到成片全记录
  • Qwen2.5-7B快速迁移:模型复制与路径配置实战
  • 使用VS Code远程开发并调试HunyuanVideo-Foley模型服务
  • 基于 MiniRocket 的 NGAFID 维护前后航班二分类:复现与工程化实践
  • ERP到底是一个怎么样的存在?为何有那么多的方面?如何学习?
  • CLIP ViT-H-14图像特征服务实操手册:GPU显存监控与批处理调优技巧
  • 口碑好的太原传媒艺考机构推荐
  • FFX风格AI绘画实战:用SPIRAN ART SUMMONER生成史诗级游戏场景