结论: 用Graphics.DrawMeshInstanced绘制出来的实例,ShaderGraph里的节点Instance ID 是正常工作的。 而用Graphics.DrawMeshInstancedIndirect绘制的实例,ShaderGraph里的节点Instance ID 一直都是0。
unity3d版本:2022.3.62f1
shaderGraph版本:14.0.12
用Graphics.DrawMeshInstanced绘制
using UnityEngine;
public class InstancedCubes : MonoBehaviour
{
public Mesh mesh;
public Material material;
private Matrix4x4[] matrices;
void Start()
{
matrices = new Matrix4x4[3];
for (int i = 0; i < 3; i++)
{
matrices[i] = Matrix4x4.TRS(new Vector3(i * 2, 0, 0), Quaternion.identity, Vector3.one);
}
Graphics.DrawMeshInstanced(mesh, 0, material, matrices, 3);
}
void Update()
{
Graphics.DrawMeshInstanced(mesh, 0, material, matrices, 3);
}
}
shaderGraph节点连接:直接拖出Instance ID 节点,输出给Frag 的BaseColor,理论上来讲,Instance ID 递增,那么,用DrawMeshInstanced绘制出来的实例,第一个颜色应该是黑的,之后的实例,都是白色的了。
运行后,结果确实如此,ShaderGraph的Instance ID是正常工作的。
接下来,当我换成Graphics.DrawMeshInstancedIndirect来绘制实例,这时候,需要用到ComputeBuffer 来存储每个实例的变换矩阵(Matrix4x4),以及一个 args buffer 来告诉 GPU 需要绘制多少个实例。
using UnityEngine;
public class IndirectInstancingDemo : MonoBehaviour
{
public Mesh mesh;
public Material material;
public int instanceCount = 3;
ComputeBuffer argsBuffer;
ComputeBuffer matrixBuffer;
void Start()
{
// 每个实例的变换矩阵
Matrix4x4[] matrices = new Matrix4x4[instanceCount];
for (int i = 0; i < instanceCount; i++)
{
matrices[i] = Matrix4x4.TRS(new Vector3(i * 2, 0, 0), Quaternion.identity, Vector3.one);
}
// 创建并上传矩阵buffer
matrixBuffer = new ComputeBuffer(instanceCount, 64); // 4x4矩阵=16个float=64字节
matrixBuffer.SetData(matrices);
material.SetBuffer("_PerInstanceData", matrixBuffer);
// 设置DrawMeshInstancedIndirect参数
uint[] args = new uint[5] { mesh.GetIndexCount(0), (uint)instanceCount, 0, 0, 0 };
argsBuffer = new ComputeBuffer(1, args.Length * sizeof(uint), ComputeBufferType.IndirectArguments);
argsBuffer.SetData(args);
}
void Update()
{
Graphics.DrawMeshInstancedIndirect(mesh, 0, material, new Bounds(Vector3.zero, Vector3.one * 100), argsBuffer);
}
void OnDestroy()
{
if (matrixBuffer != null) matrixBuffer.Release();
if (argsBuffer != null) argsBuffer.Release();
}
}
注意事项:
Shader 需要支持 StructuredBuffer(或 Buffer)来读取 _PerInstanceData,并在 vert 阶段用它来变换顶点。此时,需要一段hlsl代码片段(InstanceTransform.hlsl):
#ifndef INSTANCE_TRANSFORM_INCLUDED
#define INSTANCE_TRANSFORM_INCLUDED
StructuredBuffer<float4x4> _PerInstanceData;
void ApplyInstanceMatrix_float(float3 Position, out float3 Out)
{
Out = Position;
#ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED
#if defined(UNITY_ANY_INSTANCING_ENABLED)
uint id = unity_InstanceID;
float4x4 m = _PerInstanceData[id];
Out = mul(m, float4(Position, 1.0)).xyz;
#endif
#endif
}
#endif // INSTANCE_TRANSFORM_INCLUDED
运行后,只看到一个实例经过排查,就是unity_InstanceID的问题,既然确定了是它的问题,那接下来,就是想办法,让它生效就行了。
如果用Graphics.DrawMeshInstancedIndirect绘制实例,想使用ShaderGraph里的Instance ID,需要一些手段技巧,保证ShaderGraph 里的Instance ID 在编译成Shader代码后,是可用的。
直接上ShaderGraph的配置:
- BlackBoard:
- 新增一个这个节点,并用string模式,调用一个函数
- 在hlsl代码里,定义vertInstancingSetup函数
#ifndef INSTANCE_TRANSFORM_INCLUDED #define INSTANCE_TRANSFORM_INCLUDED StructuredBuffer<float4x4> _PerInstanceData; // 1. 定义并实现 vertInstancingSetup void vertInstancingSetup() { #ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED // 获取当前实例的矩阵 float4x4 data = _PerInstanceData[unity_InstanceID]; // 设置 Unity 的内置矩阵 unity_ObjectToWorld = data; unity_WorldToObject = unity_ObjectToWorld; unity_WorldToObject._14_24_34 *= -1; unity_WorldToObject._11_22_33 = unity_WorldToObject._11_22_33; #endif } void ApplyInstanceMatrix_float(float3 Position, out float3 Out) { Out = Position; #ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED #if defined(UNITY_ANY_INSTANCING_ENABLED) uint id = unity_InstanceID; float4x4 m = _PerInstanceData[id]; Out = mul(m, float4(Position, 1.0)).xyz; #endif #endif } #endif // INSTANCE_TRANSFORM_INCLUDED
下面是我提问AI,AI给的回答
为什么要加 PROCEDURAL_INSTANCING_ON,还要加 #pragma instancing_options procedural:vertInstancingSetup,还要实现 vertInstancingSetup?这些到底是干什么的?是不是都是为了解决 unity_InstanceID 的问题?
1. PROCEDURAL_INSTANCING_ON 是什么?
这是一个多编译关键字(multi_compile keyword),用来告诉Unity:“我要用自定义的实例化(Instancing)流程”。
它本身不会让 unity_InstanceID 生效,但它会让Unity编译出一套支持自定义实例化的Shader变体。
2. #pragma instancing_options procedural:vertInstancingSetup 是什么?
这是Unity Shader的一个特殊编译指令。
它告诉Unity:“我不想用Unity默认的实例化数据(比如MaterialPropertyBlock),我要用我自己定义的实例化数据,并且用我指定的函数(这里是vertInstancingSetup)来设置每个实例的数据。”
没有这句,Unity不会自动生成和绑定 unity_InstanceID 相关的代码,unity_InstanceID 也不会被定义。
3. vertInstancingSetup 是什么?
这是你自定义的实例化数据设置函数。
Unity在渲染每个实例时,会自动调用这个函数,让你有机会用 unity_InstanceID 去读取你自己的Buffer(比如StructuredBuffer),并把数据写到Shader里(比如变换矩阵、颜色等)。
你可以在这里做任何你想做的“每实例初始化”操作。
4. unity_InstanceID 到底怎么来的?
只有在Shader里用到了自定义实例化流程(即上面三步都做了),Unity才会在底层为你生成和传递 unity_InstanceID。
如果你只是用普通的Instancing(比如材质勾选Enable GPU Instancing),Unity只会传递最基础的实例ID,且你无法自定义每实例数据。
只有用Procedural Instancing,你才能用 unity_InstanceID 作为索引,去StructuredBuffer里取你自己传的数据。
5. 总结一句话
> 这三步(Keyword、Pragma、Setup函数)是Unity Procedural Instancing的“开关+入口+实现”,只有都做了,unity_InstanceID才会在Shader里生效,你才能用它做真正的每实例自定义数据。
6. 类比理解
PROCEDURAL_INSTANCING_ON:告诉Unity“我要用自定义实例化”
#pragma instancing_options procedural:vertInstancingSetup:告诉Unity“我的自定义实例化入口函数叫vertInstancingSetup”
vertInstancingSetup:你自己写的“每实例初始化”函数
unity_InstanceID:Unity帮你传进来的“当前实例编号”,只有上面都配好才有。