Unity3D學習筆記6——GPU例項化(1)

charlee44發表於2022-07-06

1. 概述

在之前的文章中說到,一種材質對應一次繪製呼叫的指令。即使是這種情況,兩個三維物體使用同一種材質,但它們使用的材質引數不一樣,那麼最終仍然會造成兩次繪製指令。原因在於,圖形工作都是一種狀態機,狀態發生了變化,就必須進行一次繪製呼叫指令。

GPU例項化用於解決這樣的問題:對於像草地、樹木這樣的物體,它們往往是資料量很大,但同時又只存在微小的差別如位置、姿態、顏色等。如果像常規物體那樣進行渲染,所使用的繪製指令必然很多,資源佔用必然很大。一個合理的策略就是,我們指定一個需要繪製物體物件,以及大量該物件不同的引數,然後根據引數在一個繪製呼叫中繪製出來——這就是所謂的GPU例項化。

2. 詳論

首先,我們建立一個空的GameObject物件,並且掛接如下指令碼:

using UnityEngine;

//例項化引數
public struct InstanceParam
{  
    public Color color;
    public Matrix4x4 instanceToObjectMatrix;        //例項化到物方矩陣
}

[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();
        }        
    }
}

這個指令碼的意思是,設定一個網格和一個材質,通過隨機獲取的例項化引數,渲染這個網格的多個例項:
imglink1

GPU例項化的關鍵介面是Graphics.DrawMeshInstancedIndirect()。Graphics物件的一系列介面是Unity的底層API,它是需要每一幀呼叫的。Graphics.DrawMeshInstanced()也可以例項繪製,但是最多隻能繪製1023個例項。所以還是Graphics.DrawMeshInstancedIndirect()比較好。

例項化引數InstanceParam和GPU緩衝區引數bufferWithArgs都是儲存於一個ComputeBuffer物件中。ComputeBuffe定義了一個GPU資料緩衝區物件,能夠對映到Unity Shader中的 StructuredBuffer中。例項化引數InstanceParam儲存了每個例項化物件的位置,姿態、縮放以及顏色資訊,通過Material.SetBuffer(),傳遞到著色器中:

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
		
			//頂點著色器輸入
			struct a2v
			{
				float4  position : POSITION;
				float3  normal: NORMAL;
				float2  texcoord : TEXCOORD0;	
 			};

			//頂點著色器輸出
			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"
}

這是一個改進自《Unity3D學習筆記3——Unity Shader的初步使用》的簡單例項化著色器。例項化繪製往往位置並不是固定的,這意味著Shader中獲取的模型矩陣UNITY_MATRIX_M一般是不正確的。因而例項化繪製的關鍵就在於對模型矩陣的重新計算,否則繪製的位置是不正確的。例項化的資料往往位置比較接近,所以可以先傳入一個基準位置(矩陣ObjectToWorld),然後例項化資料就可以只傳入於這個位置的相對矩陣(instanceToObjectMatrix)。

最終的執行結果如下,繪製了大量不同位置、不同姿態、不同大小以及不同顏色的膠囊體,並且效能基本上不受影響。

imglink2

3. 參考

  1. 《Unity3D學習筆記3——Unity Shader的初步使用》
  2. Graphics.DrawMeshInstanced

具體實現程式碼

相關文章