Unity3D學(xué)習(xí)筆記6——GPU實(shí)例化(1)
1. 概述
在之前的文章中說(shuō)到,一種材質(zhì)對(duì)應(yīng)一次繪制調(diào)用的指令。即使是這種情況,兩個(gè)三維物體使用同一種材質(zhì),但它們使用的材質(zhì)參數(shù)不一樣,那么最終仍然會(huì)造成兩次繪制指令。原因在于,圖形工作都是一種狀態(tài)機(jī),狀態(tài)發(fā)生了變化,就必須進(jìn)行一次繪制調(diào)用指令。
GPU實(shí)例化用于解決這樣的問(wèn)題:對(duì)于像草地、樹(shù)木這樣的物體,它們往往是數(shù)據(jù)量很大,但同時(shí)又只存在微小的差別如位置、姿態(tài)、顏色等。如果像常規(guī)物體那樣進(jìn)行渲染,所使用的繪制指令必然很多,資源占用必然很大。一個(gè)合理的策略就是,我們指定一個(gè)需要繪制物體對(duì)象,以及大量該對(duì)象不同的參數(shù),然后根據(jù)參數(shù)在一個(gè)繪制調(diào)用中繪制出來(lái)——這就是所謂的GPU實(shí)例化。
2. 詳論
首先,我們創(chuàng)建一個(gè)空的GameObject對(duì)象,并且掛接如下腳本:
using UnityEngine;
//實(shí)例化參數(shù)
public struct InstanceParam
{
public Color color;
public Matrix4x4 instanceToObjectMatrix; //實(shí)例化到物方矩陣
}
[ExecuteInEditMode]
public class Note6Main : MonoBehaviour
{
public Mesh mesh;
public Material material;
int instanceCount = 200;
Bounds instanceBounds;
ComputeBuffer bufferWithArgs = null;
ComputeBuffer instanceParamBufferData = null;
// Start is called before the first frame update
void Start()
{
instanceBounds = new Bounds(new Vector3(0, 0, 0), new Vector3(100, 100, 100));
uint[] args = new uint[5] { 0, 0, 0, 0, 0 };
bufferWithArgs = new ComputeBuffer(1, args.Length * sizeof(uint), ComputeBufferType.IndirectArguments);
int subMeshIndex = 0;
args[0] = mesh.GetIndexCount(subMeshIndex);
args[1] = (uint)instanceCount;
args[2] = mesh.GetIndexStart(subMeshIndex);
args[3] = mesh.GetBaseVertex(subMeshIndex);
bufferWithArgs.SetData(args);
InstanceParam[] instanceParam = new InstanceParam[instanceCount];
for (int i = 0; i < instanceCount; i++)
{
Vector3 position = Random.insideUnitSphere * 5;
Quaternion q = Quaternion.Euler(Random.Range(0.0f, 90.0f), Random.Range(0.0f, 90.0f), Random.Range(0.0f, 90.0f));
float s = Random.value;
Vector3 scale = new Vector3(s, s, s);
instanceParam[i].instanceToObjectMatrix = Matrix4x4.TRS(position, q, scale);
instanceParam[i].color = Random.ColorHSV();
}
int stride = System.Runtime.InteropServices.Marshal.SizeOf(typeof(InstanceParam));
instanceParamBufferData = new ComputeBuffer(instanceCount, stride);
instanceParamBufferData.SetData(instanceParam);
material.SetBuffer("dataBuffer", instanceParamBufferData);
material.SetMatrix("ObjectToWorld", Matrix4x4.identity);
}
// Update is called once per frame
void Update()
{
if(bufferWithArgs != null)
{
Graphics.DrawMeshInstancedIndirect(mesh, 0, material, instanceBounds, bufferWithArgs, 0);
}
}
private void OnDestroy()
{
if (bufferWithArgs != null)
{
bufferWithArgs.Release();
}
if(instanceParamBufferData != null)
{
instanceParamBufferData.Release();
}
}
}
這個(gè)腳本的意思是,設(shè)置一個(gè)網(wǎng)格和一個(gè)材質(zhì),通過(guò)隨機(jī)獲取的實(shí)例化參數(shù),渲染這個(gè)網(wǎng)格的多個(gè)實(shí)例:

GPU實(shí)例化的關(guān)鍵接口是Graphics.DrawMeshInstancedIndirect()。Graphics對(duì)象的一系列接口是Unity的底層API,它是需要每一幀調(diào)用的。Graphics.DrawMeshInstanced()也可以實(shí)例繪制,但是最多只能繪制1023個(gè)實(shí)例。所以還是Graphics.DrawMeshInstancedIndirect()比較好。
實(shí)例化參數(shù)InstanceParam和GPU緩沖區(qū)參數(shù)bufferWithArgs都是存儲(chǔ)于一個(gè)ComputeBuffer對(duì)象中。ComputeBuffe定義了一個(gè)GPU數(shù)據(jù)緩沖區(qū)對(duì)象,能夠映射到Unity Shader中的 StructuredBuffer
Shader "Custom/SimpleInstanceShader"
{
Properties
{
}
SubShader
{
Tags{"Queue" = "Geometry"}
Pass
{
CGPROGRAM
#include "UnityCG.cginc"
#pragma vertex vert
#pragma fragment frag
#pragma target 4.5
sampler2D _MainTex;
float4x4 ObjectToWorld;
struct InstanceParam
{
float4 color;
float4x4 instanceToObjectMatrix;
};
#if SHADER_TARGET >= 45
StructuredBuffer<InstanceParam> dataBuffer;
#endif
//頂點(diǎn)著色器輸入
struct a2v
{
float4 position : POSITION;
float3 normal: NORMAL;
float2 texcoord : TEXCOORD0;
};
//頂點(diǎn)著色器輸出
struct v2f
{
float4 position: SV_POSITION;
float2 texcoord: TEXCOORD0;
float4 color: COLOR;
};
v2f vert(a2v v, uint instanceID : SV_InstanceID)
{
#if SHADER_TARGET >= 45
float4x4 instanceToObjectMatrix = dataBuffer[instanceID].instanceToObjectMatrix;
float4 color = dataBuffer[instanceID].color;
#else
float4x4 instanceToObjectMatrix = float4x4(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
float4 color = float4(1.0f, 1.0f, 1.0f, 1.0f);
#endif
float4 localPosition = mul(instanceToObjectMatrix, v.position);
//float4 localPosition = v.position;
float4 worldPosition = mul(ObjectToWorld, localPosition);
v2f o;
//o.position = UnityObjectToClipPos(v.position);
o.position = mul(UNITY_MATRIX_VP, worldPosition);
o.texcoord = v.texcoord;
o.color = color;
return o;
}
fixed4 frag(v2f i) : SV_Target
{
return i.color;
}
ENDCG
}
}
Fallback "Diffuse"
}
這是一個(gè)改進(jìn)自《Unity3D學(xué)習(xí)筆記3——Unity Shader的初步使用》的簡(jiǎn)單實(shí)例化著色器。實(shí)例化繪制往往位置并不是固定的,這意味著Shader中獲取的模型矩陣UNITY_MATRIX_M一般是不正確的。因而實(shí)例化繪制的關(guān)鍵就在于對(duì)模型矩陣的重新計(jì)算,否則繪制的位置是不正確的。實(shí)例化的數(shù)據(jù)往往位置比較接近,所以可以先傳入一個(gè)基準(zhǔn)位置(矩陣ObjectToWorld),然后實(shí)例化數(shù)據(jù)就可以只傳入于這個(gè)位置的相對(duì)矩陣(instanceToObjectMatrix)。
最終的運(yùn)行結(jié)果如下,繪制了大量不同位置、不同姿態(tài)、不同大小以及不同顏色的膠囊體,并且性能基本上不受影響。


浙公網(wǎng)安備 33010602011771號(hào)