Unity 高效渲染方案GPU Instancing 教程适用版本Unity 2018 | 渲染管线Built-in / URP / HDRP核心用途大量同模型物体单 DrawCall 批量渲染大幅降低 CPU 开销SLG和MMORPG以及大世界游戏必备的游戏技术目录概述GPU Instancing 核心原理基础篇标准 GPU Instancing Shader进阶篇GPU Instancing 动画GPU 蒙皮实例化实战篇Demo 工程解析性能对比与收益已知限制与避坑指南总结一、概述1.1 什么是 GPU InstancingGPU Instancing 是 Unity 提供的一种批量渲染技术允许同一网格 同一材质的多个物体合并为一个 DrawCall提交给 GPU。普通模式1000 个同模型物体 → 1000 个 DrawCallInstancing 模式1000 个同模型物体 →1 个 DrawCall1.2 使用场景场景示例推荐度静态物体大批量树木、石头、草、路灯⭐⭐⭐⭐⭐带颜色/大小差异的物体不同颜色的宝箱、不同大小的石块⭐⭐⭐⭐⭐带动画的角色群集人群、军队、怪物群⭐⭐⭐⭐需特殊方案粒子替代方案大量弹幕、碎片⭐⭐⭐1.3 和传统合批的对比特性静态批处理GPU InstancingSRP Batcher合并粒度网格级别实例级别材质级别Mesh 必须相同❌✅必须相同❌材质必须相同✅✅✅支持不同颜色/属性❌✅ 实例缓冲区❌支持骨骼动画❌⚠️ 需特殊方案❌CPU 开销低极低中二、GPU Instancing 核心原理2.1 传统渲染 vs Instancing 渲染传统模式CPU 逐物体提交 → 每个物体设置 MVP 矩阵 → 调用 DrawCall→ N 个物体 N 个 DrawCallInstancing 模式CPU 一次性提交所有实例的矩阵数组 自定义属性数组 → GPU 用SV_InstanceID索引→ N 个物体 1 个 DrawCall2.2 关键机制CPU 侧 GPU 侧 ┌──────────────┐ ┌──────────────────┐ │ 实例矩阵数组 │ ───────→ │ 顶点着色器 │ │ 实例颜色数组 │ │ UNITY_SETUP_INSTANCE_ID(v) │ 实例动画参数 │ │ unity_ObjectToWorld → 自动索引实例矩阵 └──────────────┘ │ UNITY_ACCESS_INSTANCED_PROP(...) └──────────────────┘核心要点引擎自动将unity_ObjectToWorld替换为实例矩阵数组unity_ObjectToWorld在 instancing 分支下是数组自定义差异化数据通过实例缓冲区Instancing Buffer传递GPU 用UNITY_VERTEX_INPUT_INSTANCE_ID获取当前实例索引三、基础篇标准 GPU Instancing Shader3.1 标准 Instancing Shader 模板Shader Custom/InstancingBase { Properties { _MainTex (Tex, 2D) white {} _Color (Color, Color) (1,1,1,1) } SubShader { Tags { RenderType Opaque } Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_instancing // 【核心】开启 Instancing 分支编译 #include UnityCG.cginc struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; UNITY_VERTEX_INPUT_INSTANCE_ID // 【核心】实例 ID 输入 }; struct v2f { float2 uv : TEXCOORD0; float4 pos : SV_POSITION; UNITY_VERTEX_OUTPUT_STEREO // VR 支持 }; sampler2D _MainTex; float4 _MainTex_ST; // 【核心】实例缓冲区声明每个实例独立的属性 UNITY_INSTANCING_BUFFER_START(Props) UNITY_DEFINE_INSTANCED_PROP(fixed4, _Color) UNITY_INSTANCING_BUFFER_END(Props) v2f vert(appdata v) { UNITY_SETUP_INSTANCE_ID(v); // 【核心】注册实例 ID v2f o; UNITY_TRANSFER_INSTANCE_ID(v, o); UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o); o.pos UnityObjectToClipPos(v.vertex); o.uv TRANSFORM_TEX(v.uv, _MainTex); return o; } fixed4 frag(v2f i) : SV_Target { UNITY_SETUP_INSTANCE_ID(i); // 片元着色器也可访问实例属性 fixed4 col tex2D(_MainTex, i.uv) * UNITY_ACCESS_INSTANCED_PROP(Props, _Color); return col; } ENDCG } } FallBack Unlit/Texture }3.2 【核心】必须添加的三要素以下三行代码缺一不可少任何一行 GPU Instancing 都无法正常工作要素代码位置重要性① 编译指令#pragma multi_compile_instancingSubShader/Pass 内⭐⭐⭐⭐⭐② 实例 ID 输入UNITY_VERTEX_INPUT_INSTANCE_IDappdata / v2f 结构体⭐⭐⭐⭐⭐③ 注册实例 IDUNITY_SETUP_INSTANCE_ID(v)顶点着色器第一行⭐⭐⭐⭐⭐3.3 C# 调用端Graphics.DrawMeshInstancedusingUnityEngine;publicclassInstancingDemo:MonoBehaviour{publicMeshmesh;publicMaterialmaterial;publicintcount1000;privateMatrix4x4[]matrices;privateMaterialPropertyBlockpropertyBlock;voidStart(){matricesnewMatrix4x4[count];propertyBlocknewMaterialPropertyBlock();varcolorsnewColor[count];for(inti0;icount;i){Vector3posRandom.insideUnitSphere*10f;matrices[i]Matrix4x4.TRS(pos,Quaternion.identity,Vector3.one);colors[i]newColor(Random.value,Random.value,Random.value,1);}propertyBlock.SetColorArray(_Color,colors);// 或每帧单独设置性能较差但更灵活// propertyBlock.SetColor(_Color, Color.red);}voidUpdate(){// 一次性绘制所有实例Graphics.DrawMeshInstanced(mesh,0,material,matrices,count,propertyBlock);}}3.4 材质面板的变化正确编写 Instancing Shader 后材质 Inspector 面板会出现“GPU Instancing” 勾选框✅ 勾选启用 Instancing使用 instancing 变体❌ 取消关闭 Instancing使用普通变体自定义 Shader 如果没有加#pragma multi_compile_instancing不会出现这个勾选框。四、进阶篇GPU Instancing 动画GPU 蒙皮实例化4.1 为什么要特殊方案Unity 原生 SkinnedMeshRenderer GPU Instancing不兼容SkinnedMeshRenderer 每帧在 CPU 计算骨骼蒙皮每帧更新网格顶点GPU Instancing 要求网格数据在 GPU 端保持不变只变换矩阵骨骼矩阵无法通过标准实例缓冲区传递4.2 核心思路Animation Texture动画纹理烘焙把动画数据从 CPU 搬到 GPU绕过 SkinnedMeshRenderer┌─────────────────────────────────────────────────────────┐ │ 烘焙流程 │ │ │ │ 原始 FBX 动画 ─→ 逐帧采样骨骼矩阵 ─→ 写入 Texture2D │ │ │ │ │ AnimationTexture 结构: │ │ ┌──────┬──────┬──────┬──────┬──────┬──────┐ │ │ │ F0B0 │ F0B1 │ ... │ F1B0 │ F1B1 │ ... │ ← 每像素 │ │ │ row0 │ row0 │ │ row0 │ row0 │ │ 存储 │ │ ├──────┼──────┤ ├──────┼──────┤ │ float4 │ │ │ F0B0 │ F0B1 │ │ F1B0 │ F1B1 │ │ (矩阵行)│ │ │ row1 │ row1 │ │ row1 │ row1 │ │ │ │ └──────┴──────┘ └──────┴──────┘ │ │ │ ↑ 每帧每个骨骼占 3 个像素4x4 矩阵取前三行 │ └─────────────────────────────────────────────────────────┘4.3 实例动画 Shader 核心实现以下是完整的工作 Shader来自本 Demo 工程Shader AnimationGpuInstancing/Standard { Properties { _Color(Color, Color) (1,1,1,1) _MainTex(Albedo (RGB), 2D) white {} _Glossiness(Smoothness, Range(0,1)) 0.5 _Metallic(Metallic, Range(0,1)) 0.0 // 动画相关 [NoScaleOffset] _AnimTex(Animation Texture, 2D) white {} [HideInInspector] [PerRendererData] _StartFrame(, Int) 0 [HideInInspector] [PerRendererData] _EndFrame(, Int) 0 [HideInInspector] [PerRendererData] _FrameCount(, Int) 1 [HideInInspector] [PerRendererData] _OffsetSeconds(, Float) 0 [HideInInspector] _PixelCountPerFrame(, Int) 0 } SubShader { Tags { RenderType Opaque } LOD 200 CGPROGRAM #pragma surface surf Standard fullforwardshadows vertex:vert #pragma multi_compile_instancing // 【核心】GPU Instancing 编译指令 #pragma target 4.5 // 需要 tex2Dlod sampler2D _AnimTex; float4 _AnimTex_TexelSize; // 纹理尺寸信息 int _PixelCountPerFrame; // 【核心】实例缓冲区每个实例独立的动画参数 UNITY_INSTANCING_BUFFER_START(Props) UNITY_DEFINE_INSTANCED_PROP(int, _StartFrame) UNITY_DEFINE_INSTANCED_PROP(int, _EndFrame) UNITY_DEFINE_INSTANCED_PROP(int, _FrameCount) UNITY_DEFINE_INSTANCED_PROP(float, _OffsetSeconds) UNITY_DEFINE_INSTANCED_PROP(fixed4, _Color) UNITY_INSTANCING_BUFFER_END(Props) // ---- 辅助函数从动画纹理采样骨骼矩阵 ---- // 根据像素索引计算 UV 坐标 float4 GetUV(int index) { int row index / (int)_AnimTex_TexelSize.z; // 行 index / 宽度 int col index % (int)_AnimTex_TexelSize.z; // 列 index % 宽度 return float4(col / _AnimTex_TexelSize.z, row / _AnimTex_TexelSize.w, 0, 0); } // 采样 3 个像素恢复一个 float4x4 骨骼矩阵 float4x4 GetMatrix(int startIndex, float boneIndex) { int matrixIndex startIndex boneIndex * 3; float4 row0 tex2Dlod(_AnimTex, GetUV(matrixIndex)); float4 row1 tex2Dlod(_AnimTex, GetUV(matrixIndex 1)); float4 row2 tex2Dlod(_AnimTex, GetUV(matrixIndex 2)); float4 row3 float4(0, 0, 0, 1); // 第四行固定 return float4x4(row0, row1, row2, row3); } // ---- 自定义顶点结构含 bone 数据 ---- struct appdata { float4 vertex : POSITION; float3 normal : NORMAL; float4 texcoord : TEXCOORD0; float4 texcoord1 : TEXCOORD1; // 预留 half4 boneIndex : TEXCOORD2; // 骨骼索引 fixed4 boneWeight : TEXCOORD3; // 骨骼权重 UNITY_VERTEX_INPUT_INSTANCE_ID // 【核心】实例 ID 输入 }; // ---- 顶点着色器GPU 蒙皮 Instancing ---- void vert(inout appdata v, out Input o) { UNITY_SETUP_INSTANCE_ID(v); // 【核心】绑定实例 ID UNITY_TRANSFER_INSTANCE_ID(v, o); UNITY_INITIALIZE_OUTPUT(Input, o); // 【核心】读取当前实例的动画参数实例缓冲区 int startFrame UNITY_ACCESS_INSTANCED_PROP(_StartFrame_arr, _StartFrame); int endFrame UNITY_ACCESS_INSTANCED_PROP(_EndFrame_arr, _EndFrame); int frameCount UNITY_ACCESS_INSTANCED_PROP(_FrameCount_arr, _FrameCount); float offsetSeconds UNITY_ACCESS_INSTANCED_PROP(_OffsetSeconds_arr, _OffsetSeconds); // 计算当前帧基于时间 随机偏移 int offsetFrame (int)((_Time.y offsetSeconds) * 30); // 30 FPS int currentFrame startFrame offsetFrame % frameCount; // 计算该帧在纹理中的起始像素位置 int clampedIndex currentFrame * _PixelCountPerFrame; // 采样 4 根骨骼的矩阵 float4x4 bone1Matrix GetMatrix(clampedIndex, v.boneIndex.x); float4x4 bone2Matrix GetMatrix(clampedIndex, v.boneIndex.y); float4x4 bone3Matrix GetMatrix(clampedIndex, v.boneIndex.z); float4x4 bone4Matrix GetMatrix(clampedIndex, v.boneIndex.w); // 线性混合蒙皮Linear Blend Skinning float4 pos mul(bone1Matrix, v.vertex) * v.boneWeight.x mul(bone2Matrix, v.vertex) * v.boneWeight.y mul(bone3Matrix, v.vertex) * v.boneWeight.z mul(bone4Matrix, v.vertex) * v.boneWeight.w; float4 normal mul(bone1Matrix, v.normal) * v.boneWeight.x mul(bone2Matrix, v.normal) * v.boneWeight.y mul(bone3Matrix, v.normal) * v.boneWeight.z mul(bone4Matrix, v.normal) * v.boneWeight.w; v.vertex pos; v.normal normal; } ENDCG } FallBack Diffuse }4.4 C# 动画控制AnimatedMeshAnimatorusingUnityEngine;publicclassSampleAnimatedMeshObjectInitializer:MonoBehaviour{[SerializeField]MaterialPropertyBlockControllerBodyPropertyBlockController;[SerializeField]AnimatedMeshAnimatorBodyMeshAnimator;[SerializeField]AnimatedMeshAnimatorFaceMeshAnimator;privatereadonlystaticstring[]RandomAnimationNamesnewstring[]{Victory,ATK3,Jump,Idle,};privatevoidAwake(){// 每个实例随机颜色varrandomColornewColor(Random.Range(0.00f,1.00f),Random.Range(0.00f,1.00f),Random.Range(0.00f,1.00f),1);BodyPropertyBlockController.SetColor(_Color,randomColor);BodyPropertyBlockController.Apply();// 每个实例随机动画 随机时间偏移varoffsetSecondsRandom.Range(0.0f,3.0f);varrandomIndexRandom.Range(0,RandomAnimationNames.Length);varrandomAnimationNameRandomAnimationNames[randomIndex];BodyMeshAnimator.Play(randomAnimationName,offsetSeconds);FaceMeshAnimator.Play(randomAnimationName,offsetSeconds);}}核心思想每个实例只传少量参数动画名 时间偏移Shader 自己算蒙皮。4.5 GPU 动画实例化的前提条件条件说明✅ 网格完全相同顶点数、拓扑、骨骼索引顺序完全一致✅ 骨骼数量一致所有实例的骨骼数必须相等✅ 动画预烘焙所有骨骼矩阵离线烧入 Animation Texture✅ 材质统一外部差异用实例缓冲区传不新建材质✅ Shader 支持multi_compile_instancing 纹理采样蒙皮⚠️ 不支持动画过渡、分层 Blend、RootMotion 等五、实战篇Demo 工程解析5.1 工程结构Assets/ ├── Sample/ │ ├── Shaders/ │ │ └── AnimationGpuInstancing_Standard.shader ← GPU 蒙皮 Instancing │ └── Scripts/ │ └── SampleAnimatedMeshObjectInitializer.cs ← 实例初始化5.2 数据流烘焙阶段编辑器 ┌─────────────────┐ │ AnimationTexture │ ← 所有动画的骨骼矩阵 │ (RGBAFloat 图) │ └────────┬────────┘ ↓ 运行时每实例设置MaterialPropertyBlock ┌─────────────────────────────────────────────────┐ │ _StartFrame 动画在纹理中的起始帧 │ │ _EndFrame 动画结束帧 │ │ _FrameCount 总帧数 │ │ _OffsetSeconds 随机偏移让不同实例不同步 │ │ _Color 自定义颜色 │ └─────────────────────────────────────────────────┘ ↓ 顶点着色器 GPU Instancing 蒙皮 ┌───────────────────────────┐ │ UNITY_SETUP_INSTANCE_ID │ │ → 读取实例属性 │ │ → 按时间 偏移算当前帧 │ │ → 采样 Animation Texture │ │ → 线性混合蒙皮 │ │ → 输出变换后顶点 │ └───────────────────────────┘5.3 核心技巧时间同步与随机化_Time.y offsetSeconds让所有实例基于相同全局时间但加上随机偏移既共享一个 DrawCall又看起来动作不同步。帧计算currentFrame startFrame (int)(_Time.y * 30) % frameCount30 是动画烘焙帧率startFrame定位到该动画在纹理中的起始位置。PerRendererData 标记[PerRendererData]告诉 Unity 这些属性是通过 MaterialPropertyBlock 逐实例设置的不要存进材质资产。六、性能对比与收益6.1 测试场景1000 个带动画的角色方案DrawCallCPU 耗时GPU 耗时内存SkinnedMeshRenderer原生1000~8ms~3ms高GPU Instancing无动画1~0.1ms~3ms低GPU Instancing 动画纹理1~0.2ms~4ms中6.2 缩放规律DrawCall 数 vs 实例数 关系图 DrawCall │ 1000┤ ╱ SkinnedMeshRenderer │╱ │ ├───────────────── │ ╱── GPU Instancing≈1 DrawCall │ ╱ └───────────────────────── 实例数CPU 端从 O(N) 降到 O(1)一个 DrawCall 不管实例数量GPU 端略有增加需要采样纹理做蒙皮但远小于 CPU 节省6.3 实例上限模式单次 DrawMeshInstanced 上限说明标准 Instancing1023 个Unity 硬限制512x512 纹理~87,000 帧骨骼矩阵容量BatchRendererGroup无硬限制ECS 方案七、已知限制与避坑指南7.1 Instancing 通用限制限制说明❌GrabPass使用 GrabPass 的 Shader 无法 Instancing❌多 Pass只有主 Pass 支持副 Pass 失效❌不同网格所有实例必须是完全相同的 Mesh❌不同材质所有实例必须用完全相同的 Material⚠️实例属性数量每个实例缓冲区有上限约 250 个 float47.2 GPU 动画 Instancing 特定限制限制说明❌动画过渡无法在 GPU 做 Blend Tree / CrossFade❌复杂状态机Animator Controller 在 GPU 不可用❌RootMotion骨骼根运动需要额外处理❌不同骨骼数所有实例骨骼数必须一致⚠️纹理精度Animation Texture 使用 ARGBFloat精度和带宽消耗较高7.3 常见问题排查Q: 勾上 GPU Instancing 后 DrawCall 没减少→【首选检查】#pragma multi_compile_instancing是否添加→ 检查所有实例是否使用完全相同的 Material→ 检查是否有每实例不同的属性没有走实例缓冲区Q: 动画不对或闪烁→ 检查_AnimTex_TexelSize是否正确传递→ 检查 UV 计算是否溢出纹理边界→ 检查帧率30fps和纹理帧布局是否匹配Q: 颜色/属性设置无效→ 检查属性是否在UNITY_INSTANCING_BUFFER_START内声明→ 检查 C# 端是否使用MaterialPropertyBlock设置→ 检查是否用UNITY_ACCESS_INSTANCED_PROP读取Q: 性能提升不明显→ GPU Instancing 主要减少 CPU 开销如果项目是 GPU 瓶颈则收益有限→ 尝试增加实例数量到 500 观察差距八、总结适用决策树需要渲染大量相同模型 ├── 无需动画 ─→ 标准 GPU Instancing ✅ │ 1. 加 【】#pragma multi_compile_instancing │ 2. 【】实例缓冲区声明差异属性 │ 3. Graphics.DrawMeshInstanced │ └── 需要动画 ─→ GPU 蒙皮实例化 ⚠️ 1. 烘焙 Animation Texture 2. Shader 内采样纹理做蒙皮 3. 【】实例缓冲区传动画参数 4. 同模型、同骨骼、同材质关键要点回顾标准 Instancing只做矩阵变换批量不处理骨骼动画 Instancing必须用Animation Texture GPU 蒙皮【必记】三要素缺一不可#pragma multi_compile_instancingUNITY_VERTEX_INPUT_INSTANCE_IDUNITY_SETUP_INSTANCE_ID(v)每实例差异属性必须走实例缓冲区UNITY_INSTANCING_BUFFER_START/END不走全局变量同网格、同材质、同骨骼是硬前提参考资料Unity Manual: GPU InstancingUnity Manual: Animation Instancing (experimental)NVIDIA GPU Skinned Instancing 方案
Unity 高效渲染方案:GPU Instancing 教程
Unity 高效渲染方案GPU Instancing 教程适用版本Unity 2018 | 渲染管线Built-in / URP / HDRP核心用途大量同模型物体单 DrawCall 批量渲染大幅降低 CPU 开销SLG和MMORPG以及大世界游戏必备的游戏技术目录概述GPU Instancing 核心原理基础篇标准 GPU Instancing Shader进阶篇GPU Instancing 动画GPU 蒙皮实例化实战篇Demo 工程解析性能对比与收益已知限制与避坑指南总结一、概述1.1 什么是 GPU InstancingGPU Instancing 是 Unity 提供的一种批量渲染技术允许同一网格 同一材质的多个物体合并为一个 DrawCall提交给 GPU。普通模式1000 个同模型物体 → 1000 个 DrawCallInstancing 模式1000 个同模型物体 →1 个 DrawCall1.2 使用场景场景示例推荐度静态物体大批量树木、石头、草、路灯⭐⭐⭐⭐⭐带颜色/大小差异的物体不同颜色的宝箱、不同大小的石块⭐⭐⭐⭐⭐带动画的角色群集人群、军队、怪物群⭐⭐⭐⭐需特殊方案粒子替代方案大量弹幕、碎片⭐⭐⭐1.3 和传统合批的对比特性静态批处理GPU InstancingSRP Batcher合并粒度网格级别实例级别材质级别Mesh 必须相同❌✅必须相同❌材质必须相同✅✅✅支持不同颜色/属性❌✅ 实例缓冲区❌支持骨骼动画❌⚠️ 需特殊方案❌CPU 开销低极低中二、GPU Instancing 核心原理2.1 传统渲染 vs Instancing 渲染传统模式CPU 逐物体提交 → 每个物体设置 MVP 矩阵 → 调用 DrawCall→ N 个物体 N 个 DrawCallInstancing 模式CPU 一次性提交所有实例的矩阵数组 自定义属性数组 → GPU 用SV_InstanceID索引→ N 个物体 1 个 DrawCall2.2 关键机制CPU 侧 GPU 侧 ┌──────────────┐ ┌──────────────────┐ │ 实例矩阵数组 │ ───────→ │ 顶点着色器 │ │ 实例颜色数组 │ │ UNITY_SETUP_INSTANCE_ID(v) │ 实例动画参数 │ │ unity_ObjectToWorld → 自动索引实例矩阵 └──────────────┘ │ UNITY_ACCESS_INSTANCED_PROP(...) └──────────────────┘核心要点引擎自动将unity_ObjectToWorld替换为实例矩阵数组unity_ObjectToWorld在 instancing 分支下是数组自定义差异化数据通过实例缓冲区Instancing Buffer传递GPU 用UNITY_VERTEX_INPUT_INSTANCE_ID获取当前实例索引三、基础篇标准 GPU Instancing Shader3.1 标准 Instancing Shader 模板Shader Custom/InstancingBase { Properties { _MainTex (Tex, 2D) white {} _Color (Color, Color) (1,1,1,1) } SubShader { Tags { RenderType Opaque } Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_instancing // 【核心】开启 Instancing 分支编译 #include UnityCG.cginc struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; UNITY_VERTEX_INPUT_INSTANCE_ID // 【核心】实例 ID 输入 }; struct v2f { float2 uv : TEXCOORD0; float4 pos : SV_POSITION; UNITY_VERTEX_OUTPUT_STEREO // VR 支持 }; sampler2D _MainTex; float4 _MainTex_ST; // 【核心】实例缓冲区声明每个实例独立的属性 UNITY_INSTANCING_BUFFER_START(Props) UNITY_DEFINE_INSTANCED_PROP(fixed4, _Color) UNITY_INSTANCING_BUFFER_END(Props) v2f vert(appdata v) { UNITY_SETUP_INSTANCE_ID(v); // 【核心】注册实例 ID v2f o; UNITY_TRANSFER_INSTANCE_ID(v, o); UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o); o.pos UnityObjectToClipPos(v.vertex); o.uv TRANSFORM_TEX(v.uv, _MainTex); return o; } fixed4 frag(v2f i) : SV_Target { UNITY_SETUP_INSTANCE_ID(i); // 片元着色器也可访问实例属性 fixed4 col tex2D(_MainTex, i.uv) * UNITY_ACCESS_INSTANCED_PROP(Props, _Color); return col; } ENDCG } } FallBack Unlit/Texture }3.2 【核心】必须添加的三要素以下三行代码缺一不可少任何一行 GPU Instancing 都无法正常工作要素代码位置重要性① 编译指令#pragma multi_compile_instancingSubShader/Pass 内⭐⭐⭐⭐⭐② 实例 ID 输入UNITY_VERTEX_INPUT_INSTANCE_IDappdata / v2f 结构体⭐⭐⭐⭐⭐③ 注册实例 IDUNITY_SETUP_INSTANCE_ID(v)顶点着色器第一行⭐⭐⭐⭐⭐3.3 C# 调用端Graphics.DrawMeshInstancedusingUnityEngine;publicclassInstancingDemo:MonoBehaviour{publicMeshmesh;publicMaterialmaterial;publicintcount1000;privateMatrix4x4[]matrices;privateMaterialPropertyBlockpropertyBlock;voidStart(){matricesnewMatrix4x4[count];propertyBlocknewMaterialPropertyBlock();varcolorsnewColor[count];for(inti0;icount;i){Vector3posRandom.insideUnitSphere*10f;matrices[i]Matrix4x4.TRS(pos,Quaternion.identity,Vector3.one);colors[i]newColor(Random.value,Random.value,Random.value,1);}propertyBlock.SetColorArray(_Color,colors);// 或每帧单独设置性能较差但更灵活// propertyBlock.SetColor(_Color, Color.red);}voidUpdate(){// 一次性绘制所有实例Graphics.DrawMeshInstanced(mesh,0,material,matrices,count,propertyBlock);}}3.4 材质面板的变化正确编写 Instancing Shader 后材质 Inspector 面板会出现“GPU Instancing” 勾选框✅ 勾选启用 Instancing使用 instancing 变体❌ 取消关闭 Instancing使用普通变体自定义 Shader 如果没有加#pragma multi_compile_instancing不会出现这个勾选框。四、进阶篇GPU Instancing 动画GPU 蒙皮实例化4.1 为什么要特殊方案Unity 原生 SkinnedMeshRenderer GPU Instancing不兼容SkinnedMeshRenderer 每帧在 CPU 计算骨骼蒙皮每帧更新网格顶点GPU Instancing 要求网格数据在 GPU 端保持不变只变换矩阵骨骼矩阵无法通过标准实例缓冲区传递4.2 核心思路Animation Texture动画纹理烘焙把动画数据从 CPU 搬到 GPU绕过 SkinnedMeshRenderer┌─────────────────────────────────────────────────────────┐ │ 烘焙流程 │ │ │ │ 原始 FBX 动画 ─→ 逐帧采样骨骼矩阵 ─→ 写入 Texture2D │ │ │ │ │ AnimationTexture 结构: │ │ ┌──────┬──────┬──────┬──────┬──────┬──────┐ │ │ │ F0B0 │ F0B1 │ ... │ F1B0 │ F1B1 │ ... │ ← 每像素 │ │ │ row0 │ row0 │ │ row0 │ row0 │ │ 存储 │ │ ├──────┼──────┤ ├──────┼──────┤ │ float4 │ │ │ F0B0 │ F0B1 │ │ F1B0 │ F1B1 │ │ (矩阵行)│ │ │ row1 │ row1 │ │ row1 │ row1 │ │ │ │ └──────┴──────┘ └──────┴──────┘ │ │ │ ↑ 每帧每个骨骼占 3 个像素4x4 矩阵取前三行 │ └─────────────────────────────────────────────────────────┘4.3 实例动画 Shader 核心实现以下是完整的工作 Shader来自本 Demo 工程Shader AnimationGpuInstancing/Standard { Properties { _Color(Color, Color) (1,1,1,1) _MainTex(Albedo (RGB), 2D) white {} _Glossiness(Smoothness, Range(0,1)) 0.5 _Metallic(Metallic, Range(0,1)) 0.0 // 动画相关 [NoScaleOffset] _AnimTex(Animation Texture, 2D) white {} [HideInInspector] [PerRendererData] _StartFrame(, Int) 0 [HideInInspector] [PerRendererData] _EndFrame(, Int) 0 [HideInInspector] [PerRendererData] _FrameCount(, Int) 1 [HideInInspector] [PerRendererData] _OffsetSeconds(, Float) 0 [HideInInspector] _PixelCountPerFrame(, Int) 0 } SubShader { Tags { RenderType Opaque } LOD 200 CGPROGRAM #pragma surface surf Standard fullforwardshadows vertex:vert #pragma multi_compile_instancing // 【核心】GPU Instancing 编译指令 #pragma target 4.5 // 需要 tex2Dlod sampler2D _AnimTex; float4 _AnimTex_TexelSize; // 纹理尺寸信息 int _PixelCountPerFrame; // 【核心】实例缓冲区每个实例独立的动画参数 UNITY_INSTANCING_BUFFER_START(Props) UNITY_DEFINE_INSTANCED_PROP(int, _StartFrame) UNITY_DEFINE_INSTANCED_PROP(int, _EndFrame) UNITY_DEFINE_INSTANCED_PROP(int, _FrameCount) UNITY_DEFINE_INSTANCED_PROP(float, _OffsetSeconds) UNITY_DEFINE_INSTANCED_PROP(fixed4, _Color) UNITY_INSTANCING_BUFFER_END(Props) // ---- 辅助函数从动画纹理采样骨骼矩阵 ---- // 根据像素索引计算 UV 坐标 float4 GetUV(int index) { int row index / (int)_AnimTex_TexelSize.z; // 行 index / 宽度 int col index % (int)_AnimTex_TexelSize.z; // 列 index % 宽度 return float4(col / _AnimTex_TexelSize.z, row / _AnimTex_TexelSize.w, 0, 0); } // 采样 3 个像素恢复一个 float4x4 骨骼矩阵 float4x4 GetMatrix(int startIndex, float boneIndex) { int matrixIndex startIndex boneIndex * 3; float4 row0 tex2Dlod(_AnimTex, GetUV(matrixIndex)); float4 row1 tex2Dlod(_AnimTex, GetUV(matrixIndex 1)); float4 row2 tex2Dlod(_AnimTex, GetUV(matrixIndex 2)); float4 row3 float4(0, 0, 0, 1); // 第四行固定 return float4x4(row0, row1, row2, row3); } // ---- 自定义顶点结构含 bone 数据 ---- struct appdata { float4 vertex : POSITION; float3 normal : NORMAL; float4 texcoord : TEXCOORD0; float4 texcoord1 : TEXCOORD1; // 预留 half4 boneIndex : TEXCOORD2; // 骨骼索引 fixed4 boneWeight : TEXCOORD3; // 骨骼权重 UNITY_VERTEX_INPUT_INSTANCE_ID // 【核心】实例 ID 输入 }; // ---- 顶点着色器GPU 蒙皮 Instancing ---- void vert(inout appdata v, out Input o) { UNITY_SETUP_INSTANCE_ID(v); // 【核心】绑定实例 ID UNITY_TRANSFER_INSTANCE_ID(v, o); UNITY_INITIALIZE_OUTPUT(Input, o); // 【核心】读取当前实例的动画参数实例缓冲区 int startFrame UNITY_ACCESS_INSTANCED_PROP(_StartFrame_arr, _StartFrame); int endFrame UNITY_ACCESS_INSTANCED_PROP(_EndFrame_arr, _EndFrame); int frameCount UNITY_ACCESS_INSTANCED_PROP(_FrameCount_arr, _FrameCount); float offsetSeconds UNITY_ACCESS_INSTANCED_PROP(_OffsetSeconds_arr, _OffsetSeconds); // 计算当前帧基于时间 随机偏移 int offsetFrame (int)((_Time.y offsetSeconds) * 30); // 30 FPS int currentFrame startFrame offsetFrame % frameCount; // 计算该帧在纹理中的起始像素位置 int clampedIndex currentFrame * _PixelCountPerFrame; // 采样 4 根骨骼的矩阵 float4x4 bone1Matrix GetMatrix(clampedIndex, v.boneIndex.x); float4x4 bone2Matrix GetMatrix(clampedIndex, v.boneIndex.y); float4x4 bone3Matrix GetMatrix(clampedIndex, v.boneIndex.z); float4x4 bone4Matrix GetMatrix(clampedIndex, v.boneIndex.w); // 线性混合蒙皮Linear Blend Skinning float4 pos mul(bone1Matrix, v.vertex) * v.boneWeight.x mul(bone2Matrix, v.vertex) * v.boneWeight.y mul(bone3Matrix, v.vertex) * v.boneWeight.z mul(bone4Matrix, v.vertex) * v.boneWeight.w; float4 normal mul(bone1Matrix, v.normal) * v.boneWeight.x mul(bone2Matrix, v.normal) * v.boneWeight.y mul(bone3Matrix, v.normal) * v.boneWeight.z mul(bone4Matrix, v.normal) * v.boneWeight.w; v.vertex pos; v.normal normal; } ENDCG } FallBack Diffuse }4.4 C# 动画控制AnimatedMeshAnimatorusingUnityEngine;publicclassSampleAnimatedMeshObjectInitializer:MonoBehaviour{[SerializeField]MaterialPropertyBlockControllerBodyPropertyBlockController;[SerializeField]AnimatedMeshAnimatorBodyMeshAnimator;[SerializeField]AnimatedMeshAnimatorFaceMeshAnimator;privatereadonlystaticstring[]RandomAnimationNamesnewstring[]{Victory,ATK3,Jump,Idle,};privatevoidAwake(){// 每个实例随机颜色varrandomColornewColor(Random.Range(0.00f,1.00f),Random.Range(0.00f,1.00f),Random.Range(0.00f,1.00f),1);BodyPropertyBlockController.SetColor(_Color,randomColor);BodyPropertyBlockController.Apply();// 每个实例随机动画 随机时间偏移varoffsetSecondsRandom.Range(0.0f,3.0f);varrandomIndexRandom.Range(0,RandomAnimationNames.Length);varrandomAnimationNameRandomAnimationNames[randomIndex];BodyMeshAnimator.Play(randomAnimationName,offsetSeconds);FaceMeshAnimator.Play(randomAnimationName,offsetSeconds);}}核心思想每个实例只传少量参数动画名 时间偏移Shader 自己算蒙皮。4.5 GPU 动画实例化的前提条件条件说明✅ 网格完全相同顶点数、拓扑、骨骼索引顺序完全一致✅ 骨骼数量一致所有实例的骨骼数必须相等✅ 动画预烘焙所有骨骼矩阵离线烧入 Animation Texture✅ 材质统一外部差异用实例缓冲区传不新建材质✅ Shader 支持multi_compile_instancing 纹理采样蒙皮⚠️ 不支持动画过渡、分层 Blend、RootMotion 等五、实战篇Demo 工程解析5.1 工程结构Assets/ ├── Sample/ │ ├── Shaders/ │ │ └── AnimationGpuInstancing_Standard.shader ← GPU 蒙皮 Instancing │ └── Scripts/ │ └── SampleAnimatedMeshObjectInitializer.cs ← 实例初始化5.2 数据流烘焙阶段编辑器 ┌─────────────────┐ │ AnimationTexture │ ← 所有动画的骨骼矩阵 │ (RGBAFloat 图) │ └────────┬────────┘ ↓ 运行时每实例设置MaterialPropertyBlock ┌─────────────────────────────────────────────────┐ │ _StartFrame 动画在纹理中的起始帧 │ │ _EndFrame 动画结束帧 │ │ _FrameCount 总帧数 │ │ _OffsetSeconds 随机偏移让不同实例不同步 │ │ _Color 自定义颜色 │ └─────────────────────────────────────────────────┘ ↓ 顶点着色器 GPU Instancing 蒙皮 ┌───────────────────────────┐ │ UNITY_SETUP_INSTANCE_ID │ │ → 读取实例属性 │ │ → 按时间 偏移算当前帧 │ │ → 采样 Animation Texture │ │ → 线性混合蒙皮 │ │ → 输出变换后顶点 │ └───────────────────────────┘5.3 核心技巧时间同步与随机化_Time.y offsetSeconds让所有实例基于相同全局时间但加上随机偏移既共享一个 DrawCall又看起来动作不同步。帧计算currentFrame startFrame (int)(_Time.y * 30) % frameCount30 是动画烘焙帧率startFrame定位到该动画在纹理中的起始位置。PerRendererData 标记[PerRendererData]告诉 Unity 这些属性是通过 MaterialPropertyBlock 逐实例设置的不要存进材质资产。六、性能对比与收益6.1 测试场景1000 个带动画的角色方案DrawCallCPU 耗时GPU 耗时内存SkinnedMeshRenderer原生1000~8ms~3ms高GPU Instancing无动画1~0.1ms~3ms低GPU Instancing 动画纹理1~0.2ms~4ms中6.2 缩放规律DrawCall 数 vs 实例数 关系图 DrawCall │ 1000┤ ╱ SkinnedMeshRenderer │╱ │ ├───────────────── │ ╱── GPU Instancing≈1 DrawCall │ ╱ └───────────────────────── 实例数CPU 端从 O(N) 降到 O(1)一个 DrawCall 不管实例数量GPU 端略有增加需要采样纹理做蒙皮但远小于 CPU 节省6.3 实例上限模式单次 DrawMeshInstanced 上限说明标准 Instancing1023 个Unity 硬限制512x512 纹理~87,000 帧骨骼矩阵容量BatchRendererGroup无硬限制ECS 方案七、已知限制与避坑指南7.1 Instancing 通用限制限制说明❌GrabPass使用 GrabPass 的 Shader 无法 Instancing❌多 Pass只有主 Pass 支持副 Pass 失效❌不同网格所有实例必须是完全相同的 Mesh❌不同材质所有实例必须用完全相同的 Material⚠️实例属性数量每个实例缓冲区有上限约 250 个 float47.2 GPU 动画 Instancing 特定限制限制说明❌动画过渡无法在 GPU 做 Blend Tree / CrossFade❌复杂状态机Animator Controller 在 GPU 不可用❌RootMotion骨骼根运动需要额外处理❌不同骨骼数所有实例骨骼数必须一致⚠️纹理精度Animation Texture 使用 ARGBFloat精度和带宽消耗较高7.3 常见问题排查Q: 勾上 GPU Instancing 后 DrawCall 没减少→【首选检查】#pragma multi_compile_instancing是否添加→ 检查所有实例是否使用完全相同的 Material→ 检查是否有每实例不同的属性没有走实例缓冲区Q: 动画不对或闪烁→ 检查_AnimTex_TexelSize是否正确传递→ 检查 UV 计算是否溢出纹理边界→ 检查帧率30fps和纹理帧布局是否匹配Q: 颜色/属性设置无效→ 检查属性是否在UNITY_INSTANCING_BUFFER_START内声明→ 检查 C# 端是否使用MaterialPropertyBlock设置→ 检查是否用UNITY_ACCESS_INSTANCED_PROP读取Q: 性能提升不明显→ GPU Instancing 主要减少 CPU 开销如果项目是 GPU 瓶颈则收益有限→ 尝试增加实例数量到 500 观察差距八、总结适用决策树需要渲染大量相同模型 ├── 无需动画 ─→ 标准 GPU Instancing ✅ │ 1. 加 【】#pragma multi_compile_instancing │ 2. 【】实例缓冲区声明差异属性 │ 3. Graphics.DrawMeshInstanced │ └── 需要动画 ─→ GPU 蒙皮实例化 ⚠️ 1. 烘焙 Animation Texture 2. Shader 内采样纹理做蒙皮 3. 【】实例缓冲区传动画参数 4. 同模型、同骨骼、同材质关键要点回顾标准 Instancing只做矩阵变换批量不处理骨骼动画 Instancing必须用Animation Texture GPU 蒙皮【必记】三要素缺一不可#pragma multi_compile_instancingUNITY_VERTEX_INPUT_INSTANCE_IDUNITY_SETUP_INSTANCE_ID(v)每实例差异属性必须走实例缓冲区UNITY_INSTANCING_BUFFER_START/END不走全局变量同网格、同材质、同骨骼是硬前提参考资料Unity Manual: GPU InstancingUnity Manual: Animation Instancing (experimental)NVIDIA GPU Skinned Instancing 方案