ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Unity GPU Instancing:让万级同屏物体性能提升的批处理方案

2026/10/1 9:21:15 拓冰建站 浏览量
Unity GPU Instancing:让万级同屏物体性能提升的批处理方案 项目概述GPU Instancing 到底能解决什么问题做 Unity 项目优化尤其是移动端游戏迟早会遇到一个绕不开的性能瓶颈同屏物体太多Draw Call绘制调用暴涨帧率直接崩盘。大面积草丛、密集的碎石堆、满屏的敌人、铺天盖地的弹幕特效这些东西单看每个模型面数都不高但数量一上去CPU 就忙于把一条条渲染命令发给 GPU哪怕每个物体只有几百个三角形也能把主线程活活拖垮。这时候GPU InstancingGPU实例化几乎是唯一能够在不大幅牺牲画面效果的前提下把性能拉回来的手段。GPU Instancing 的核心思路非常简单粗暴与其让 CPU 一个接一个地发送渲染命令不如把一批相同模型的变换信息打包成一份数据一次性交给 GPU让 GPU 自己按这份数据绘制出成百上千个实例。用一句话概括就是“相同的东西告诉 GPU 画一百遍而不是让 CPU 跑一百趟”。这套机制最早是桌面端图形 API 时代就有的老技术在 DirectX 和 OpenGL 时代都有对应实现而 Unity 把它封装成了开箱即用的功能对项目开发者相当友好。这篇内容适合谁主要是那些已经在 Unity 里做过一段时间场景搭建、正被性能问题折磨的开发者——你可能试过把场景里几百棵树砍到几十棵才勉强跑得动但你完全可以用 GPU Instancing 把它们全部保留下来还能维持 60 帧。也适合那些刚开始接触渲染管线和批处理机制的初中级开发者我会把原理、踩坑点、实操代码一次性讲透。读完之后你至少能顺手完成一个“几千个同类物体同屏绘制不掉帧”的小场景并且知道怎么判断项目里的什么东西适合做实例化、什么东西该绕开。需要先说明一点这篇文章聊的是 Unity 内置渲染管线Built-in Render Pipeline和 SRP可编程渲染管线下的 GPU Instancing 通用玩法案例以标准着色器和自定义着色器为主。URP 和 HDRP 下大部分用法通用但光照处理上有些差异我会单独提。1. 为什么你渲染了几万棵树GPU 还在摸鱼在动手写代码之前先花点时间弄清楚这套机制为什么有效否则你后面调参数、排查问题时会完全摸不着头脑。1.1 CPU 才是“绘制成百上千物体”的真正瓶颈很多人有个误区画面卡了第一反应是“GPU 不够强”。但实际上大量小物体同屏的场景里GPU 往往只用了不到一半的算力真正累死的是 CPU。传统美术资源的绘制流程大致是这样的CPU 逐个处理每个物体的可见性判定、剔除操作然后收集物体的网格、材质、变换矩阵封装成一条 Draw Call通过图形 API 传给驱动驱动再交给 GPU 执行。一个物体一条命令1000 个物体就是 1000 条命令。而每条命令从 CPU 到 GPU 的传递、驱动层对状态切换的处理、GPU 的提交等待每一步都有不小的开销。现代显卡贵的是并行计算能力不是这种顺序执行的小规格任务你让 GPU 去处理 1000 个三角形级别的任务它一个瞬间就画完了但 CPU 把这 1000 次调用的参数准备好、发出去可能就要耗时 10 到 20 毫秒。打个比方你要发 1000 张快递单给同一个仓库如果一张一张打电话通知光拨号就累死你如果直接做一张 Excel 表发给仓库管理员对方拿着表一次就能全发完。GPU Instancing 的本质就是把那张“Excel 表”整理好——一个网格、一个材质、一份数组变换矩阵颜色等自定义属性用一条 Draw Call 提交让 GPU 批量绘制。实测中1000 个 Cube 用传统方式绘制可能需要 1000 条 Draw Call而启用 Instancing 后通常能压到 1 条受批次限制时是几条。1.2 GPU Instancing 和 Static/Dynamic Batching 不是一回事Unity 里还有另外两种批处理机制静态合批Static Batching和动态合批Dynamic Batching。很多人以为它们和 Instancing 是一回事其实它们解决瓶颈的方式完全不同。静态合批把场景里标记为 Static 的物体在构建时合并成一个大的网格。好处是运行时不需要 CPU 逐物体处理代价是内存占用上涨相当于为合批多存了一份合并后的网格而且只适用于完全静止不动的物体。动态合批CPU 在每帧把多个小物体的小网格临时合并成一个网格再提交。机制上比 Instancing 更灵活但只支持 900 个顶点以内的网格Shader Model 2 平台是 300 顶点而且每次合批都有 CPU 额外的网格合并开销移动端上经常得不偿失。GPU Instancing不合并网格不走 CPU 拼网格的路而是通过单个可被实例化的着色器 GPU 并行实例化能力完成批量渲染。动态物体的支持非常好——你只需要更新传入的变换数组不需要像 Static Batching 那样构建时固化一切。遇到项目里的决策我的常见建议是大量静止物体建筑、地形装饰用静态合批或者直接烘进场景里动态且重复的物体敌人、弹幕、角色特效用 GPU Instancing而动态合批在移动端尽量少用它的性价比在真实项目中往往不高。SRP Batcher 是另一条路它是针对材质属性更新的优化机制和 Instancing 能共存后面我会提到但二者解决的问题不同。1.3 一个简单公式理解性能收益如果你熟悉 Profiler 里的数据可以把收益概括为假设一个网格的常规 Draw Call 开销是 N它由驱动调用、状态切换等组成而实例化 batch 的单次开销约等于 1 数据上传开销几十 KB 量级。几千个实例的 CPU 侧开销可以从几千次降低到几十次这是一个数量级以上的差距。GPU 侧因为大量实例并行生成渲染管线的顶点处理阶段仍然要为每个实例跑一遍顶点着色器但整体吞吐远高于提交上千条小命令所以帧耗能显著下降。需要警惕的是实例化不是没有上限的。单个批次能容纳的实例数受平台和着色器限制PC 上通常是 1024 个左右移动端旧设备可能是 500 个。物体超过这个数量Unity 会自动拆分成多个批次本质上是“两条 Excel 表”但和之前的上千次调用相比依旧是降维打击。2. 动手前的准备哪些物体能实例化不是所有物体都能套 GPU Instancing也不是所有材质天然支持。这个环节决定了你项目的最终优化上限值得仔细看。2.1 前提条件是“同一个材质 同一个网格”Instancing 的本质是“一次提交、批量绘制”所以它对物体有硬性要求网格必须相同允许不同缩放和位置但不能不同 Mesh材质必须相同同一个材质实例或者至少是同一个 Shader 且关键属性一致。如果你的场景里有 100 棵材质完全一样的树只是位置、大小、旋转不同那完美符合但如果其中有 5 棵树用了另一种绿色那就得把这 5 棵单独拎出去或者给它们单独准备一份实例化数据用 MaterialPropertyBlock 指定不同颜色详见后文。对于角色和敌人如果一套模型对应多种颜色皮肤同样不会直接实例化——除非你用 MaterialPropertyBlock 传入各自的颜色、粗糙度、贴图参数这样才能把“长得不一样但是结构相同”的模型放进同一个批次。这是高级用法我会在后面专门展开。2.2 材质和 Shader 必须开启“Enable GPU Instancing”即便你用了标准着色器Standard Shader材质面板上的实例化开关默认也是关的。需要手动勾选材质 Inspector 右下角的“Enable GPU Instancing”选项。这个勾选动作本质上是让 Unity 把材质的属性从“每材质一份”编译成“每实例一份”的变体keyword着色器内部会生成支持实例化 ID 的属性和缓冲逻辑。如果是你自己写的 Shader需要做几件事在 Shader 末尾加上#pragma multi_compile_instancing让 Unity 生成支持实例化的变体变体会增加打包体积需要清楚这个成本。属性块的声明方式要允许实例化使用UNITY_INSTANCING_BUFFER_START(Props)语法在 CBUFFER 中声明实例化属性。顶点/片元着色器中需要通过UNITY_SETUP_INSTANCE_ID(v);获取当前实例 ID并且通过UNITY_ACCESS_INSTANCED_PROP访问实例化属性这样 GPU 才知道你取的是哪个实例的数据。如果用了unity_ObjectToWorld矩阵默认情况它在实例化 Shader 下会失效你需要用UNITY_MATRIX_M宏来获取矩阵它内部会根据是否实例化自动换算。自己写实例化 Shader 是我在实际项目中踩过最深的一次坑。最初接手一个老旧项目所有草地的 Shader 都是几个 TA 自己写的顶点动画 Shader我当时没有在意 GPU Instancing 支持结果大量草全部无法批处理帧率惨不忍睹。后来逐行排查发现就是少了UNITY_SETUP_INSTANCE_ID。这个宏只占一行但忘了它渲染结果直接错乱。2.3 材质参数的随机化MaterialPropertyBlock 的正确用法实际场景里几百个物体如果不做任何差异化画面会呆板得像复制粘贴。解决思路是给每个实例传入少量差异化数据比如颜色偏移、随机缩放、风力摆动幅度等。可以直接修改材质实例会造成材质分离破坏批处理正确做法是用 MaterialPropertyBlock。MaterialPropertyBlock 是一个轻量级的数据块允许你为每个渲染器单独覆盖部分着色器属性而不会创建新的材质实例。配合 GPU Instancing 使用Unity 会把每个实例的 property block 属性打包进实例化数据 buffer传给 GPUGPU 在每实例访问时自动拿对应的属性值。用法如下MaterialPropertyBlock props new MaterialPropertyBlock(); for (int i 0; i count; i) { renderer[i].GetPropertyBlock(props); props.SetColor(_Color, colors[i]); props.SetFloat(_RandomSeed, Random.value); renderer[i].SetPropertyBlock(props); }需要注意MaterialPropertyBlock 里设置的属性必须是 Shader 里声明为实例化属性位于UNITY_INSTANCING_BUFFER_START区块内的字段否则这些属性会导致该物体跳出实例化流程回到逐物体绘制的低效路径。换句话说写 Shader 的人要清楚地知道项目里哪些属性会被美术频繁差异化。一个常见的迷惑点材质面板上直接修改颜色能否做到差异化的同时保持实例化答案是不能。材质是共享资源你改了 A 物体材质的颜色B 物体跟着一起变。想要“每实例不同颜色”就必须靠 MaterialPropertyBlock。2.4 Graphics.DrawMeshInstanced绕过组件系统的高效路径如果是大量临时物体、粒子、动态生成的地形植被为每个物体创建一个 GameObject 并挂上 MeshRenderer 反而成为瓶颈因为 GameObject 本身的 Update 开销、Transform 同步开销都在。这时候推荐直接用 Graphics.DrawMeshInstanced 或 Graphics.RenderMeshPrimitives 之类的 API。基础用法using UnityEngine; public class InstancedCubeSpawner : MonoBehaviour { public Mesh mesh; public Material material; public int count 10000; public float radius 100f; private Matrix4x4[] matrices; private MaterialPropertyBlock block; void Start() { matrices new Matrix4x4[count]; Vector4[] colors new Vector4[count]; for (int i 0; i count; i) { Vector3 pos Random.insideUnitSphere * radius; Quaternion rot Quaternion.Euler(Random.Range(0f, 360f), Random.Range(0f, 360f), Random.Range(0f, 360f)); Vector3 scale Vector3.one * Random.Range(0.5f, 2f); matrices[i] Matrix4x4.TRS(pos, rot, scale); colors[i] new Vector4(Random.value, Random.value, Random.value, 1f); } block new MaterialPropertyBlock(); block.SetVectorArray(_Colors, colors); } void Update() { Graphics.DrawMeshInstanced(mesh, 0, material, matrices, count, block, UnityEngine.Rendering.ShadowCastingMode.On, true); } }这段代码能在没有任何 GameObject 组件的情况下渲染出 10000 个 Cube性能表现相当亮眼。你在 Update 里唯一要做的事就是更新 matrices 数组比如让每个实例按自己的轴旋转然后调用一次 API。但这里有性能陷阱matrices 数组是在 CPU 上维护的每帧如果重算 10000 个矩阵且不做优化CPU 侧虽然不会发 10000 条 Draw Call但矩阵计算本身也可能吃几毫秒。真实项目中通常会把计算迁到 GPU 端Compute Shader 或者顶点 Shader 里做动画CPU 只需要传一份静态的变换数组和一个全局时间变量即可。这也解释了为什么 GPU Instancing 常常和 GPU 驱动的动画方案如 GPU 粒子、植被风动配合使用。3. 三种常用实战姿势从简单到进阶同一个需求可以是简单场景里挂组件完事也可以是大量生成且需要动态更新的复杂系统。我把常见做法整理成三档你可以按项目情况选。3.1 姿势一少量物体直接用 MeshRenderer 材质开关如果你的物体数量在几百以内而且它们是场景中挂载了脚本、需要交互的比如可以被拾取、被攻击那就没必要用 Graphics.DrawMeshInstanced。直接创建 GameObject给每个挂上 MeshRenderer在材质上勾选 Enable GPU Instancing 就行。Unity 的渲染管线和 Culling 会尽量把它们合并到批次中。本质上这是一种“让引擎自行发挥”的方式。前提还是材质必须相同不能每棵树一个材质实例。这种方案胜在简单不会破坏 GameObject 体系也方便后续做逐个物体的逻辑控制。但它的合并效率取决于 Unity 内部对场景渲染数据的提交方式且遇到 UI 层级穿插时可能被打断。适合数量可控的装饰物、小规模摆放。3.2 姿势二大量静态物体用 Graphics.DrawMeshInstanced 一梭子丢给 GPU这就是上面代码示意的路线。适合数量上千、不需要单独交互逻辑的物体最典型的案例是植被、碎石、重复的街景设施。有几个优化细节值得强调剔除Graphics.DrawMeshInstanced同样会被 Unity 的相机视锥剔除官方实现里Unity 会根据参数传入的 bounds 做包围盒剔除但这只是一个大包围盒所有实例都在里面不能做到精细到每个实例的剔除。因此如果物体分散在地图各处全图用一个超大包围盒相机没看到的那部分也会继续绘制只是还在视锥内的话。解决方案有两个一是手动按区块拆分调用每个区块一个 DrawMeshInstanced每个区块的 bounds 就是该区块物体包围盒二是用 CullingGroup 或自定义判断决定哪几个区块需要绘制。阴影投射通过castShadows参数控制。ShadowCastingMode.On 会使得每个实例参与阴影贴图渲染又是一批实例化绘制如果不需要阴影可以关掉性能再上一个台阶。但要注意大面积植被如果完全没有阴影画面真实感会大打折扣建议结合实际场景光照方案权衡。接收阴影实例化渲染的物体接收阴影模式下如果用的是标准 Shader材质里需要开启 Receive Shadows自写 Shader 则要加入相应阴影采样代码实例化下的采样逻辑和普通物体略有差异很容易踩坑。3.3 姿势三高密度动态物体配合 GPU 端动画这一步是最能体现 GPU Instancing 精髓的地方。想想游戏里一片风吹动的麦田麦穗数量可能达到 10 万甚至 100 万每个麦穗都在随风摆动如果 CPU 端每帧更新每个麦穗的矩阵哪怕只是做正弦波偏移100 万个矩阵的计算也会压垮 CPU。这时候正确做法是用一个静态的矩阵数组只存每个麦穗的初始位置/旋转/缩放传给 GPU。在顶点着色器里根据实例 ID 获取初始矩阵再叠加一个基于时间 位置或随机种子的风力偏移让每个麦穗动态摆动。CPU 每帧只需要调用一次 DrawMeshInstanced 并传入一个全局时间变量剩下的全部由 GPU 并行执行。实现层面在 Shader 里使用实例化属性UNITY_INSTANCING_BUFFER_START(Props) UNITY_DEFINE_INSTANCED_PROP(float4, _Position) UNITY_DEFINE_INSTANCED_PROP(float, _RandomSeed) UNITY_INSTANCING_BUFFER_END(Props) // 在顶点着色器里 float4 worldPos mul(UNITY_MATRIX_M, float4(v.vertex.xyz, 1.0)); worldPos.y sin(_Time.y * 2.0 UNITY_ACCESS_INSTANCED_PROP(Props, _RandomSeed) * 6.283) * 0.3;这样你可以在一次渲染调用中处理几十万个动态顶点而且 CPU 几乎不参与逐实例更新。这种模式在 PC 上可以做到百万级实例而帧率不掉移动端根据 GPU 算力差一些但几十万级依旧可行。我做过一个“十万颗小行星环绕星球”的演示项目恒星和陨石带全用这套方案。CPU 侧每帧只更新一个旋转角度参数GPU 端用角度和实例 ID 计算每个小行星的轨道位置最后单批渲染Profiler 显示 Draw Call 只有个位数。这放在传统逐物体渲染下是不可想象的。4. 核心实操一个完整案例的前后对比下面用一个可复现的完整案例把整套流程从头到尾走一遍并用 Unity Profiler 数据做个前后对比。4.1 场景准备与原始性能采集创建一个新场景放置一个 Plane 作为地面写一个生成器脚本生成 5000 个相同 Cube0.5 到 2 倍随机缩放每个 Cube 上挂 MeshRenderer使用同一个材质Standard Shader默认参数。不勾选 GPU Instancing。运行场景打开 Window Analysis Profiler观察 Draw Call 数和帧耗时。在这个配置下5000 个 GameObject 的 Draw Call 大约是 5000实际可能合并一部分因为同一材质同一网格下 Unity 会自动做动态合批不会——5000 个相同 Cube 使用相同材质顶点数少动态合批可能介入但实际上受顶点数限制且场景复杂后不一定合批。为了数据稳定建议直接使用不同缩放和旋转因为矩阵不同动态合批是不参与的。观察到的帧耗时在我的测试设备上大约 35~50msDraw Call 在 5000 左右CPU 和 GPU 耗时都很高。接着把材质面板下方的 Enable GPU Instancing 勾上。再次运行Draw Call 会骤降到 8 条以内5000 / 1024 ≈ 5 批。帧耗时降到 10ms 左右。这一对比让人立刻明白 GPU Instancing 的价值启用前瓶颈在 CPU 提交命令启用后 CPU 几乎不再参与逐物体提交GPU 并行能力被充分利用。4.2 用 Graphics.DrawMeshInstanced 移除组件开销第二步去掉 5000 个 GameObject改为一个空 GameObject 生成器脚本使用 DrawMeshInstanced 渲染。创建矩阵数组时确保包含位置、旋转、缩放材质同样开启实例化MatieralPropertyBlock 给每个实例赋随机颜色。这一步不仅能减少 Draw Call还会省去 GameObject 的 Transform 管理和每帧脚本遍历CPU 侧主线程耗时从 20ms 级别降到 1ms 以下。代码略作扩展支持每帧轻微旋转以展示动态更新成本void Update() { for (int i 0; i count; i) { Quaternion deltaRot Quaternion.Euler(0f, 30f * Time.deltaTime, 0f); matrices[i] * Matrix4x4.Rotate(deltaRot); } Graphics.DrawMeshInstanced(mesh, 0, material, matrices, count, block); }注意这种 CPU 更新矩阵的写法在 5000 个实例时没问题但到了 5 万、50 万时就会暴露瓶颈。如果要做 50 万级别请直接走 GPU 端动画矩阵数组保持静态把动画放到 Shader 里。4.3 数据测量与合批数量核算实例化批次数量公式ceil(实例总数 / 单批次最大实例数)。单批次最大实例数取决于 shader 中声明的实例化属性和平台常量缓冲限制。PC 上最常见的值是 1024因此5000 实例5000 / 1024 4.88向上取整 5 批。10000 实例10000 / 1024 9.76向上取整 10 批。50000 实例50000 / 1024 48.8向上取整 49 批。这些批次之间没有状态切换开销每批内部的缓冲已经打包好所以 49 批的耗时远小于 50000 批。实测比你用 Profiler 能看到的 DrawCall 计数少很多因为 DrawMeshInstanced 在 Profiler 的一帧统计里显示为一次 DrawMeshInstanced 调用而不是 49 次 Mesh.Draw 条目。还有个小知识点单批次最大实例数还受限于着色器里声明的实例化属性数量。属性越多每个实例需要上传的数据量越大批次容量反而下降。比如你为每个实例传了 4 个 float4 颜色向量批次容量可能降到 512 或更低不要在材质里放一堆不必要的实例化属性。5. 容易翻车的几个场景和排查思路技术本身不复杂但在真实项目里会遇到各种意料之外的“不生效”情况。我按踩坑概率从高到低列几个典型。5.1 为什么勾了 GPU Instancing 依旧大量 DrawCall最常见的原因物体之间使用了不同的材质实例。场景里 2000 棵树如果每个树的材质实例是复制出来的哪怕是同一个 shaderUnity 无法把它们当作相同的材质实例化直接失效。排查用 Frame Debugger看 Material 名称后面的 ID 是否一致。另一个核心原因网格不同。比如树的 Mesh 可能经过切割顶点顺序不同、光照贴图 UV 通道不同都不能合批。实例化严格要求相同 Mesh连 UV 通道布局都得一致。还有一部分原因是物体数量太少或 LOD 切换频繁只要有一个 LOD 层级的材质或 Mesh 不同实例化就被打断。建议 LOD 每级的网格用同一个网格但不同简化版本建模软件导出多级材质保持同一个。5.2 阴影和光照贴图带来的批处理断裂使用光照贴图时每个物体需要的光照贴图索引可能不同这会破坏实例化。解决办法是把光照贴图烘焙到共享的全局纹理或者关闭实例化物体的实时接收改用手动传入的一套全局光照数据比如烘焙到顶点色/纹理数组。项目里有大量实例化植被时尤其要注意。阴影投射对批处理的影响只要所有实例在同一个阴影批次一张阴影贴图内问题不大。但如果部分物体投射阴影部分不投射或者投影距离设置不同批次可能分裂。建议统一设置。5.3 移动端为什么某些设备上实例化效果更差移动端 GPU 架构和桌面的并行方式有差异某些老旧的 Mali GPU 上实例化绘制虽然减少 CPU 负载但 GPU 侧的并行效率不如预期甚至因为常量缓冲大小受限批次容量被压得很小。建议尽量精简实例化属性每个实例属性控制在 2~4 个 float4 以内。测试覆盖中低端设备尤其是 GPU 跑不动大量实例化动画的情况。考虑使用UNITY_INSTANCING_BUFFER_START/END区块内尽量放half4而不是float4实际上 Unity 的实例化缓冲用 float4 对齐half 不会节省太多但减少属性数量有效。如果你的目标是老设备单独的实例化#pragma multi_compile_instancing变体打包进去也可以接受但历史原因会导致 shader 变体膨胀做好变体管理。5.4 颜色随机化失效检查属性是否真的在实例化缓冲区很多人在 MaterialPropertyBlock 里设置颜色打开 Frame Debugger 发现实例化还是生效的但渲染出来的所有物体颜色完全相同。这是因为 Shader 声明了该属性为普通材质属性在Properties块和普通 CBUFFER 内而 MaterialPropertyBlock 里的值虽然被传入但没有被实例化渲染管线读取。正确做法是确保该属性以UNITY_DEFINE_INSTANCED_PROP形式声明并且 Shader 代码里用UNITY_ACCESS_INSTANCED_PROP访问。如果用了 SRP 管线自写 Shader 时还要注意 SRP Batcher 和实例化的共存逻辑某些位置需要额外处理。检查这个问题的快捷方式打开 Frame Debugger点开 Batch 节点查看网格实例编号是否为一个较大的数值几百或几千并且 Shader Properties 面板里是否出现了unity_InstanceID和 per-instance properties 区域。5.5 粒子系统和 Trail Renderer 的“伪实例化”Unity 的粒子系统Particle System自带 GPU Instancing 支持在 Renderer 模块中可以勾选 Enable Mesh Instancing但前提是使用的 Shader 开启实例化变体且粒子网格必须一致。Trail Renderer 目前内置管线中不支持实例化至少到 Unity 2022 LTS 仍然如此如果做弹幕特效需要避开。生产线性的粒子效果时可以考虑用 Mesh 粒子系统配合实例化效果和性能都有保障。6. 实例化相关的性能进阶玩法当你已经吃透了 GPU Instancing 的基础用法之后接下来这几个方向能进一步拉开性能差距也是高级项目或技术美术面试中经常被问到的点。6.1 结合 CullingGroup 做分区域实例化剔除当场景超大时一个 DrawMeshInstanced 调用覆盖全图会带来无效的 GPU 处理。前面提过手动按区块拆分并做视锥剔除是标准做法。CullingGroup 是 UnityEngine 提供的高效剔除工具它可以在主线程之外帮你高效判断大量点/包围盒是否在相机视锥内并回调事件告诉你哪些区块可见。把物体按网格区块组织每帧只对可见区块调用 DrawMeshInstanced可以有效减少不可见实例的顶点处理。更进阶的方案是 GPU Driven RenderingUnity 官方在 2021.2 版本中逐步开放了Graphics.RenderMeshIndirect等接口可以配合 Compute Shader 在 GPU 端完成剔除工作CPU 完全不碰不可见物体数据。这个方案复杂度高但移动端旗舰手机上表现极佳采用者包括不少 3A 级手游。建议完成基础实例化后再研究它。6.2 与 SRP Batcher 共存别把两个“批处理”搞成互斥很多用 URP 的开发者有个误解SRP Batcher 会替代 GPU Instancing。实际上SRP Batcher 专门优化“不同材质属性但相同 Shader”这一场景它通过缓存渲染命令来减少 CPU 侧的设置开销实例化则解决“同材质同网格多实例”场景两者解决的问题不同。在 URP 下一个物体既可能被 SRP Batcher 处理也可能在特定情况下走实例化路径它们可以共存。实践上如果大量物体共用同一个材质比如草地实例化有更高收益如果只是一堆材质不同但有相同 Shader 的角色则 SRP Batcher 更合适。6.3 实例化动画带来的“蒙皮”烦恼GPU 蒙皮动画SkinnedMeshRenderer没有直接的 Instancing 支持特别是骨骼渐变动画每个角色的骨骼矩阵数组不同无法用简单的矩阵数组方式实例化。如果项目里需要大量同模型角色方案是顶点动画/纹理化骨骼动画Texture Baked Animation把骨骼动画烘焙到纹理传入实例化属性顶点着色器采样纹理获得每帧骨骼偏移。用Graphics.DrawMeshInstanced配合 LOD 和动画纹理实现同屏几百个角色。市面上成功的多人竞技游戏和割草玩法游戏常用这类方案值得深挖。但简单的“挂了 SkinnedMeshRenderer 就能实例化”是不存在的需要专门做动画管线改造。7. 常见问题速查与几个值得记住的参数整合一下之前零散提到的经验做成一个速查表格方便你在项目遇到问题时快速定位。症状可能原因快速排查/解决勾选了 Enable GPU InstancingDraw Call 不变材质实例不同/网格不同/LOD 打断Frame Debugger 看 Batch 数量与材质 ID实例化后画面颜色全部一样属性未声明为实例化属性Shader 中用 UNITY_DEFINE_INSTANCED_PROP 并正确访问实例化后阴影错乱或没有阴影自定义 Shader 缺少阴影实例化支持在阴影 Pass 中加入 multi_compile_instancing 和实例化宏移动端 FPS 不升反降实例属性过多/老 GPU 带宽受限精简属性测试多台设备必要时退回静态合批大批量物体闪烁或位置错乱矩阵数组越界/批次容量超限检查实例数是否超过单批次上限确认 batchCount 参数实例化后无法接收阴影材质设置/Shader 采样层面问题确认 Standard Shader Receive Shadows自写 Shader 检查阴影宏几个常用参数UNITY_INSTANCING_BUFFER_START(Props)和UNITY_INSTANCING_BUFFER_END声明实例化属性缓冲区。UNITY_SETUP_INSTANCE_ID在顶点/片元着色器开头获取当前实例 ID。UNITY_TRANSFER_INSTANCE_ID/UNITY_ACCESS_INSTANCED_PROP实例化属性在片元阶段的传递与访问。#pragma multi_compile_instancing生成实例化变体。UNITY_MATRIX_M实例化场景下获取正确的模型矩阵老代码里用unity_ObjectToWorld会在实例化时失效。写在最后的一个小经验我在实际项目里做 GPU Instancing 踩过几次坑之后最大的体会是这一技术本质上是在拿 GPU 的并行能力和带宽换 CPU 的串行负担所以优化目标一定要明确。如果你项目里有一万棵树但每棵树的动画都需要单独逻辑那实例化的收益会被逻辑开销吃回去如果你的瓶颈本来就在 GPU 像素填充率上那 Instancing 也救不了你。先开 Profiler 确认瓶颈在哪再决定用不用这套方案。另外一个实用建议把“哪些材质开启了实例化、哪些没开”做成项目规范写进团队的材质管理文档里。美术同学随手复制一个新材质是常有的事一个没勾选项的材质能摧毁你所有优化成果。可以在 OnValidate 或构建 CI 脚本里检查——这虽然多花点功夫但长期收益巨大尤其在多人协作的项目里。这个内容后续还可以这样扩展结合 GPU Driven Rendering 和 GPU Occlusion Culling 做一个万级物体全 GPU 交付的完整演示或者把《原神》式的大世界植被方案拆开讲解。到时候可以再写一篇详细笔记。