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

Unity性能优化-渲染模块(1)-CPU侧(2)-DrawCall优化(2)GPUInstancing

一.GPUInstancing是什么?

        GPU Instancing本质上是为了解决“重复渲染相同网格的多次 DrawCall”这一性能瓶颈的问题,其核心思想是:一次绘制多个变体,只传一次 Mesh 数据,但多次使用,仅改变每个实例的少量属性。极大减少了 CPU 到 GPU 的通信开销(DrawCall 数),释放 CPU,性能飙升。

        GPU Instancing 是“让 GPU 重复绘制同一个物体多个版本”的一种超高效绘制方式,是一种可以降低DrawCall的性能优化方式。

二.关于GPUInstancing的冷知识

1.在URP下,没有显式公开UNITY_GET_INSTANCE_ID宏

        记得当时问AI的时候经常提及可以在Shader内获取实例ID,像UNITY_GET_INSTANCE_ID和unity_instanceid 还以为能靠宏或全局变量方便直接获取到ID,但是Shader里却找不到,现在看来是一场乌龙!但我们可以使用底层的 SV_InstanceID 语义直接访问实例 ID

直接在顶点着色器的输入函数中写 uint id: SV_InstanceID; 可以绕过 Unity 的宏封装。

2.GPUInstancing与Batching(合批)的关系

        虽然GPUInstancing可以起到合批的作用,但是一般都叫Batching和GPUInstancing区分开来。这里我习惯将Batching(包括静态和动态批处理)理解为Unity自动帮我们做合批优化,特指CPU端的合批。而GPUInsatncing是我们手动提交DrawCall,可以理解为GPU端的合批(实例化),不是Unity自动合批的结果。

(*)CPU合批(静态/动态批处理):CPU把多个网格合成一个DrawCall,这叫合批,减少CPU Draw Call数量,Unity会在Profiler里把它归为“Saved by Batching”。

(*)GPU Instancing:一次DrawCall绘制多个实例,是GPU端的实例化技术,CPU只发一次DrawCall,GPU按实例渲染。Unity有时会把它也算进“Saved by Batching”,但统计机制和CPU合批不一样,特别是使用DrawMeshInstancedIndirect时,这个统计可能不会显示。

三.如何应用GPUInsatncing?

前提:Shader内定义预编译宏,在材质面板开启EnableGPUInstancing。

           #pragma multi_compile_instancing

一.Shader内定义实例对象属性

        在Shader内定义实例对象的自定义属性主要有两种方式:使用UnityInsatcneBuffer和StructureBuffer,下面分别介绍。

1.方案一(不推荐):使用InstancingBuffer

        如果选择使用UNITY_INSTANCING_BUFFER来定义实例属性,必须依靠Unity的实例宏封装机制:

(1)UNITY_VERTEX_INPUT_INSTANCE_ID

作用:用于在Vertex Shader输入 / 输出结构中定义一个语义为SV_InstanceID的元素。

使用场景:顶点着色器输入/输出结构体中

(1)必须在顶点着色器的输入结构体中声明

 struct appdata{float4 vertex : POSITION;UNITY_VERTEX_INPUT_INSTANCE_ID};

(2)仅当需要在片元着色器中的实例属性时,需要在顶点着色器输出结构体中声明

 struct v2f{float4 pos : SV_POSITION;//仅当您想访问片段着色器中的实例属性时才有必要//UNITY_VERTEX_INPUT_INSTANCE_ID};
(2)UNITY_INSTANCING_CBUFFER_START(name) / UNITY_INSTANCING_CBUFFER_END
        每个Instance独有的属性必须定义在一个遵循特殊命名规则的Constant Buffer中。使用这对宏来定义这些Constant Buffer。“name”参数可以是任意字符串。
(3)UNITY_DEFINE_INSTANCED_PROP(float4, _name)
        定义一个具有特定类型和名字的每个Instance独有的Shader属性。这个宏实际会定义一个Uniform数组。

参数1为类型,支持float及floatx向量类型,参数2为实例属性的名称。

在Pass内定义INSTANCE_CBUFFER

 UNITY_INSTANCING_BUFFER_START(Props)UNITY_DEFINE_INSTANCED_PROP(float, _ShellIndex)UNITY_INSTANCING_BUFFER_END(Props)
(4)UNITY_SETUP_INSTANCE_ID(v)

        这个宏必须在Vertex Shader的最开始调用,如果你需要在Fragment Shader里访问Instanced属性,则需要在Fragment Shader的开始也用一下。这个宏的目的在于让Instance IDShader函数里也能够被访问到。

(5)UNITY_TRANSFER_INSTANCE_ID(v, o)

        在Vertex Shader中把Instance ID从输入结构拷贝至输出结构中。只有当你需要在Fragment Shader中访问每个Instance独有的属性时才需要写这个宏。

(6)UNITY_ACCESS_INSTANCED_PROP(_name)

        可以说最重要最核心的一个,前面的所有宏最终目的都是使用UNITY_ACCESS_INSTANCED_PROP(_name)来获取实例的自定义数据。

        可以访问声明在InstanceBuffer中的每个Instance独有的属性。这个宏会使用Instance ID作为索引到Uniform数组中去取当前Instance对应的数据。

使用前必须先使用UNITY_SETUP_INSTANCE_ID(v);访问到实例ID!

Vert/Frag(){
UNITY_SETUP_INSTANCE_ID(v);float shellIndex =   UNITY_ACCESS_INSTANCED_PROP(Props, _ShellIndex);
}

(*)注意误区

以一个“常见错误用法”举例说明:

控制脚本中这样写:

for (int i = 0; i < shellCount; i++)
{var mpb = new MaterialPropertyBlock();mpb.SetFloat("_ShellIndex", i);Graphics.DrawMesh(mesh,matrix,material,0,null,0,mpb,ShadowCastingMode.Off,false);
}

Shader中这样写:

UNITY_INSTANCING_BUFFER_START(Props)UNITY_DEFINE_INSTANCED_PROP(float, _ShellIndex)
UNITY_INSTANCING_BUFFER_END(Props)float shellIndex = UNITY_ACCESS_INSTANCED_PROP(Props, _ShellIndex);

你会以为:“我用了 UNITY_ACCESS_INSTANCED_PROP,那就是 GPU Instancing 了吧?”

        错!这种写法其实就是 N 个 DrawCall!(一次DrawMesh就是一次DrawCall,循环n次就是n次DrawCall)

        因为每个 MPB 不一样,Unity 根本无法合批,它就当你是单独画的 N 个物体。即使 Shader 看起来用了 GPU Instancing,也完全没有实际效果Batch 不减少性能没提升,反而更糟

2.方案二(推荐):使用StructuredBuffer

        如果你选择StructureBuffer来传递实例自定义数据,恭喜你,你可以忘记上面的有关Unity内置的实例封装机制相关的所有宏!

        StructuredBuffer是一种 只读的 GPU 缓冲区,可在 Shader 中访问由 CPU(或 ComputeShader)传入的结构化数组。它不像 CBUFFER 那样局限于固定大小或不能索引。

Shader内的StructuredBuffer需要C#中的ComputerBuffer搭配使用完成实例属性数据的传递!

Shader内声明和使用语法:

struct appdata
{float4 vertex : POSITION;uint id: SV_InstanceID;
}StructuredBuffer<float> _MyBuffer;v2f vert(appdata v)
{//在Shader内StructuredBuffer常搭配实例的id属性使用。float shellIndex = _ShellIndexBuffer[v.id];
}

3.优劣对比

这里直接说,本人推荐使用StructuredBuffer,不推荐使用InstnceBuffer,观点如下:

1.InsatnceBuffer适用场景狭窄且使用繁琐

基本只适合以下情况:

1.只想通过DrawMeshInstanced()绘制;(优中选优,我选性能天花板DrawMeshInstancedIndirect()) 

2.每个实例只需要少量简单属性(仅支持flaotx类型);

3.只传一个 MPB,统一绘制。

仅适合极其简单但不灵活的 Instancing 场景。

一旦你想实现复杂动画,实例数据动态扩容/更新,使用 ComputeShader 写入属性,用 Indirect 绘制

,每帧动态控制实例显示状态,传结构体数组或 float3[]、int[],那InstanceBuffer立刻崩溃,必须用 StructuredBuffer

2.StructuredBuffer适用场景灵活且使用方便

(1)完全不依赖 Unity 的 Instancing 宏;

(2)DrawMeshInstanced()或DrawMeshInstancedIndirect()都能用;

(3)不必考虑破坏合批;

(4)属性数量、类型自由扩展。

真正做到了想传什么就传什么。

总结:InstanceBuffer这种 Unity 宏方案越来越显得尴尬:不够灵活、难扩展、还容易破坏合批,

而StructuredBuffer是真正现代化、可控、灵活、SRP 兼容的解决方案

二.控制脚本中传递实例数据

        一般选择使用GPUInstancing都是使用Graphics类中的实例绘制API的,这里就不考虑Graphics.DrawMesh()了,而是以Graphics.DrawMeshInstanced()为例。

1.方案一:MPB

        如果不考虑GPUInstancing,一般有多MPB配合多次DrawMesh()的用法,但由于GPUInsatncing的思想是一次绘制多个实例,也就是只调用一次Graphics.DrawMeshInstanced(),这要求使用同一个MPB,所以在使用MPB传递实例数据时,我们可以靠MPB传递一个数组传递自定义实例数据。

Shader内定义数组变量:

float _ShellIndexArray[128]; // 注意最多128个,Unity 限制struct appdata
{float4 vertex : POSITION;uint id : SV_InstanceID; // 必须获取实例ID
};struct v2f
{float4 pos : SV_POSITION;float shellIndex : TEXCOORD0;
};v2f vert(appdata v)
{v2f o;float shellIndex = _ShellIndexArray[v.id]; 
}

C#控制脚本内传递对应数组属性:

        float[] shellIndexArray = new float[instanceCount];for (int i = 0; i < instanceCount; i++){xxxshellIndexArray[i] = i; // 自定义 per-instance 数据}mpb = new MaterialPropertyBlock();mpb.SetFloatArray("_ShellIndexArray", shellIndexArray);

2.方案二(推荐):ComputeBuffer

ComputeBuffer 是 Unity 提供的一种 GPU 侧的内存容器,用于在 CPU 和 GPU 间传递结构化数据。它可以被:

(1)Shader内读取;(如 vertex/fragment shader)

(2)Compute Shader 中读写;

(3)也可用于 GPU Instancing 自定义每实例数据。

Shader内定义StructedBuffer变量:

StructuredBuffer<T> _MyBuffer;

C#控制脚本内定义并绑定数据到ComputerBuffer中:

 private ComputeBuffer shellIndexBuffer;float[] shellIndices;shellIndexBuffer = new ComputeBuffer(shellCount, sizeof(float));shellIndexBuffer.SetData(shellIndices);material.SetBuffer("_ShellIndexBuffer", shellIndexBuffer);

3.对比优劣

特性MaterialPropertyBlockComputeBuffer
数据量少量参数(几十以内)大量数据(上万条都行)
数据结构简单标量、数组任意结构体(float3、matrix、struct等)
访问方式通过SetFloat/SetVector/SetMatrix通过 GPU buffer 或 ComputeShader 直接访问
性能(小批量)非常轻量、CPU快创建稍慢,占用显存,访问灵活
实例化支持支持每个实例传入不同值可替代 Unity Instancing,支持间接绘制
动态更新需要频繁调用 SetXXX()可高效一次性传入大批数据
支持结构不支持复杂结构(无 struct)支持 StructuredBuffer<T>
GPU 计算不支持支持(支持 ComputeShader)

使用 MPB 更好:

只需要传一两个float给每个实例。

实例数少(几十个以内)。

使用 ComputeBuffer 更好:

每个实例要传多个复杂数据:如 float3 velocity, float4 color, float4x4 matrix

实现 DrawMeshInstancedIndirect(),也就是“完全由 GPU 控制绘制”。

要通过 ComputeShader 动态修改实例数据。

实例数非常多(几千~几万级别)。

要共享数据给多个 Pass 或 Shader。

三.Shader+C#控制脚本使用搭配

在使用GPUInsatncing情况下主要有两种搭配方式,下面以Graphics.DrawMeshInstanced()为例。

1.方案一:若干float[ ] +MPB

Shader内直接定义float[ ]属性,依靠实例id取值:

float _ShellIndexArray[128]; // 注意最多128个,Unity 限制struct appdata
{float4 vertex : POSITION;uint id : SV_InstanceID; // 必须获取实例ID
};struct v2f
{float4 pos : SV_POSITION;float shellIndex : TEXCOORD0;
};v2f vert(appdata v)
{v2f o;float shellIndex = _ShellIndexArray[v.id]; 
}

C#控制脚本中使用MPB绑定若干个数组传递不同实例数据:

Matrix4x4[] matrices = new Matrix4x4[shellCount];
MaterialPropertyBlock mpb = new MaterialPropertyBlock();
float[] shellIndices = new float[shellCount];for (int i = 0; i < shellCount; i++)
{matrices[i] = transform.localToWorldMatrix;shellIndices[i] = i;
}
mpb.SetFloatArray("_ShellIndex", shellIndices);// 一次 DrawCall 才是真正 Instanced
Graphics.DrawMeshInstanced(mesh,0,material,matrices,shellCount,mpb
);

2.方案二: StructureBuffer+ComputeBuffer

Shader内定义StructedBuffer变量:

注意:Shader中的StructedBuffer必须搭配C#脚本中的ComputerBuffer使用!

struct MyData
{float3 position;float4 color;
};StructuredBuffer<MyData> _MyBuffer; //MyData也可以是float,取决于用途可以额外不自定义struct appdata
{float4 vertex : POSITION;uint id : SV_InstanceID; // 必须获取实例ID
};struct v2f
{float4 pos : SV_POSITION;float shellIndex : TEXCOORD0;
};v2f vert(appdata v)
{v2f o;float shellIndex = _MyBuffer[v.id]; 
}

C#控制脚本中定义并绑定数据到ComputeBuffer内:

private ComputeBuffer shellIndexBuffer;
float[] shellIndices;void Start(){  shellIndices = new float[shellCount];for (int i = 0; i < shellCount; i++){shellIndices[i] = i;}shellIndexBuffer = new ComputeBuffer(shellCount, sizeof(float));shellIndexBuffer.SetData(shellIndices);material.SetBuffer("_ShellIndexBuffer", shellIndexBuffer);
}

本篇完

http://www.dtcms.com/a/263489.html

相关文章:

  • StackGAN(堆叠生成对抗网络)
  • Qt Hello World 程序
  • js代码02
  • NVCC编译以及Triton编译介绍
  • 攻防世界-MISC-red_green
  • 【Python使用】嘿马python运维开发全体系教程第2篇:日志管理,Linux概述【附代码文档】
  • 查看CPU支持的指令集和特性
  • 计算机网络中那些常见的路径搜索算法(一)——DFS、BFS、Dijkstra
  • leetcode:693. 交替位二进制数(数学相关算法题,python3解法)
  • 集群【运维】麒麟V10挂载本地yum源
  • 一套非常完整的复古传奇源码测试
  • LLaMA-Factory框架之参数详解
  • 【机器学习】决策树(Decision Tree)
  • 字节跳动 C++ QT PC客户端面试
  • 设计模式-访问者模式
  • Prompt:提示词工程
  • postgresql增量备份系列二 pg_probackup
  • Linux云计算基础篇(2)
  • ADP3120AJRZ-RL 【ADI】 6A高速MOSFET驱动器,让电源效率飙升!
  • Python-Word文档、PPT、PDF以及Pillow处理图像详解
  • Prompt Enginering
  • django 数据表外键 删除时 对应表的数据不删除如何设置
  • 随笔 | 写在六月的最后一天,也写在2025年上半年的最后一天
  • 2025年6月个人工作生活总结
  • 深入 ARM-Linux 的系统调用世界
  • vue常见问题:
  • 手机APP预约心理咨询师指南
  • 服务器上设置了代理之后,服务器可以访问外网,但是不能访问服务器本地。如何解决
  • CentOS 7 8 安装 madam
  • Android 中 使用 ProgressBar 实现进度显示