一、多例化技術概述
假設需要繪製有很多模型的場景,而大部分模型使用的是同一個模型,即使用同一組頂點數據在渲染時會給它們指定不同的世界座標,繪製在不同位置上。比如草,可能由幾個三角形構成,渲染一株草沒有渲染壓力,但是成千上萬株就會調用相應次數繪製調用(draw call)就會極大影響性能。
1.1 不使用GPU多例化技術繪製多個相同模型
如果這樣繪製同一模型大量實例(instance),很快就會因爲繪製調用過多而達到性能瓶頸,與GPU執行繪製執行繪製頂點的操作本身相比,準備模型的頂點數據和世界變換等用到的着色器的操作會消耗更多性能,因爲這些操作都是在相對緩慢的CPU到GPU總線(bus)上進行的。因此即便GPU執行渲染頂點非常快,命令GPU去準備渲染的操作卻不一定快。
如果能將數據一次性發送給GPU,然後使用一個繪製函數讓渲染流水線利用這些數據繪製多個相同的物體將會大大提升性能。這種技術就是GPU多例化(GPU Instancing)技術。
使用GPU多例化技術,能夠在一個繪製調用中渲染多個相同的物體。Direct3D和OpenGL等渲染流水線實作目前已經實現了這個功能,Unity3D引擎在此基礎上進行包裝,使得在每個平臺上都能用同一套代碼使用GPU多例化技術。
GPU多例化的思想,就是把每個實例的不同信息存儲在緩衝區(可能是頂點緩衝區,可能是存儲着色器uniform變量的常量緩衝區)中, 然後直接操作緩衝區中的數據來設置。
以上一小節爲例,僅僅因爲每次模型的世界變換矩陣不同,就需要調用代價昂貴的繪製調用。而其實每次的調用都是很相似的,在設置世界矩陣時,無非就是更新着色器常量緩衝區中的某個uniform變量。這裏又是一次CPU到GPU的傳遞數據操作。既然可以在定義頂點信息結構體時指定每個頂點的法線、切線和紋理映射座標等信息,那完全也可以爲每個頂點增加一個描述世界矩陣的屬性,無非就是在頂點信息結構體中多加一個float4X4類型的屬性變量而已。
假設需要渲染100個相同的模型,每個模型有256個三角形,那麼需要兩個緩衝區,一個是用來描述模型的頂點信息,因爲待渲染的模型是相同的,所以這個緩衝區只存儲了256個三角形(如果不存在任何的優化組織方式,則有768個頂點);另一個就是用來描述模型在世界座標下的位置信息。例如不考慮旋轉和縮放,100個模型即佔用100個float3類型的存儲空間。
以Direct3D 11平臺爲例,當準備好頂點數據、設置好頂點緩衝區之後,接下來進入輸入組裝階段。輸入組裝階段是使用硬件實現的。此階段根據用戶輸入的頂點緩衝區信息、圖元拓撲結構信息和描述頂點佈局格式信息,把頂點組裝成圖元,然後發送給頂點緩衝區。設置好組裝的相關設置後,對應的頂點着色器和片元着色器也要做好對應的設置才能使用多例化技術。
下面我們看在Unity3D中如何包裝在各平臺下此技術的實現。
二、如何在材質中啓用多例化技術
要在Unity3D中啓用GPU多例化技術,首先應在材質文件的Inspector面板中選中Enable Instancing複選框:
注意,只有材質文件使用的着色器代碼文件中聲明瞭支持GPU多例化技術,材質文件的Inspector面板中才會出現Enable GPU Instancing複選框。引擎提供的Standard着色器、StandardSpecular着色器及所有的外觀着色器都支持GPU多例化技術。
三、添加逐實例數據
每一次多例化繪製調用時,默認的僅對同一網格材質,但是有着不同的位置變換信息的遊戲對象進行批次化。爲了能讓GPU多例化技術不僅應用在只有不同的位置變換信息的遊戲對象,如位置變換信息相同,但材質顏色不同的遊戲對象,也可以使用GPU多例化技術,可以在自定義着色器代碼中添加逐多例化屬性。
3.1 在surface shader中給材質顏色增加GPU多例化支持的代碼
在定義的屬性的時候,要加上:
UNITY_INSTANCING_BUFFER_START(Props)
UNITY_DEFINE_INSTANCED_PROP(fixed4,_Color)
UNITY_INSTANCING_BUFFER_END(Props)
然後在surf函數中我對原本的_Color屬性進行計算操作時,換成這種寫法:
fixed4 c=tex2D(_MainTex,IN.uv_MainTex)*UNITY_ACCESS_INSTANCED_PROP(Props,_Color);
o.Albedo=c.rgb;
上面代碼使用UNITY_INSTANCING_BUFFER_START宏宣告要使用GPU多例化技術的變量,使用UNITY_DEFINE_INSTANCED_PROP宏聲明_Color變量使用技術,使用UNITY_INSTANCING_BUFFER_END宏結束使用技術的宣告。
3.2 在C#層改變game object中的多例化材質顏色屬性
在C#代碼段中,通過使用MaterialPropertyBlock類設置每一個多例化實例的不同顏色,在一個繪製調用中渲染不同顏色的多例化遊戲對象,如下代碼:
MaterialPropertyBlock props=new MaterialPropertyBlock();
MeshRenderer renderer;
foreach(GameObject obj in objects){
float r=Random.Range(0.0f,1.0f);
float g=Random.Range(0.0f,1.0f);
float b=Random.Range(0.0f,1.0f);
props.SetColor("_Color",new Color(r,g,b));
renderer=obj.GetComponent<MeshRender>();
renderer.SetPropertyBlock(props);
}
四、在頂點着色器和片元着色器中使用多例化技術
默認的surface 着色器是開啓了多例化技術支持的,對於普通的頂點着色器和片元着色器也可以使用此項技術,但要在代碼中添加一些關鍵語句。
4.1 在頂點着色器和片元着色器中啓用GPU多例化技術
Shader "Unlit/testShader"
{
Properties
{
_Color("Color",Color)=(1,1,1,1)
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
//第一步,必須要使用這個編譯指示符宣告使用GPU多例化技術
#pragma multi_compile_instancing
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct v2f
{
float2 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
//第2步,如果要訪問片元着色器的多例化屬性變量,需要使用此宏
UNITY_VERTEX_INPUT_INSTANCE_ID
};
sampler2D _MainTex;
float4 _MainTex_ST;
//第3步,使用和外觀着色器相同的宣告多例化屬性變量語句
UNITY_INSTANCING_BUFFER_START(Props)
UNITY_DEFINE_INSTANCED_PROP(fixed4, _Color)
UNITY_INSTANCING_BUFFER_END(Props)
v2f vert (appdata v)
{
v2f o;
//第4步,如果要訪問片元着色器中的多例化變量,需要使用此宏
UNITY_SETUP_INSTANCE_ID(v);
UNITY_TRANSFER_INSTANCE_ID(v, o);
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
//第5步,如果要訪問片元着色器中的多例化屬性變量,需要使用此宏
UNITY_SETUP_INSTANCE_ID(i);
// sample the texture
fixed4 col = tex2D(_MainTex, i.uv)*UNITY_ACCESS_INSTANCED_PROP(_Color);
// apply fog
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
從上面的代碼可以看到,要在頂點着色器和片元着色器使用GPU多例化技術,需要在聲明傳遞給頂點着色器的頂點數據結構體appdata中,以及聲明頂點着色器傳遞給片元着色器的數據結構體v2f中加入UNITY_VERTEX_INPUT_INSTANCE_ID宏,在聲明材質變量處使用UNITY_INSTANCING_BUFFER_START、UNITY_DEFINE_INSTANCED_PROP和UNITY_INSTANCING_BUFFER_END宏;在頂點着色器的主入口函數處使用UNITY_SETUP_INSTANCE_ID和UNITY_TRAMSFER_INSTANCE_ID宏進行多例化處理;在片元着色器的主入口函數處使用UNITY_SETUP_INSTANCE_ID和UNITY_ACCESS_INSTANCED_PROP宏處理和訪問多例化屬性變量。
4.2 UNITY_VERTEX_INPUT_INSTANCE_ID宏的定義
#if !defined(UNITY_VERTEX_INPUT_INSTANCE_ID)
# define UNITY_VERTEX_INPUT_INSTANCE_ID DEFAULT_UNITY_VERTEX_INPUT_INSTANCE_ID
#endif
UNITY_VERTEX_INPUT_INSTANCE_ID 宏就是DEFAULT_UNITY_VERTEX_INPUT_INSTANCE_ID宏。
4.3 DEFAULT_UNITY_VERTEX_INPUT_INSTANCE_ID宏的定義
// basic instancing setups
// - UNITY_VERTEX_INPUT_INSTANCE_ID Declare instance ID field in vertex shader input / output struct.
// - UNITY_GET_INSTANCE_ID (Internal) Get the instance ID from input struct.
#if defined(UNITY_INSTANCING_ENABLED) || defined(UNITY_PROCEDURAL_INSTANCING_ENABLED) || defined(UNITY_STEREO_INSTANCING_ENABLED)
//每一個多例化對象對應的instance id
// A global instance ID variable that functions can directly access.
static uint unity_InstanceID;
// Don't make UnityDrawCallInfo an actual CB on GL
#if !defined(SHADER_API_GLES3) && !defined(SHADER_API_GLCORE)
//着色器常量緩衝區UnityDrawCallInfo
UNITY_INSTANCING_CBUFFER_SCOPE_BEGIN(UnityDrawCallInfo)
#endif
//當前渲染批次中是在多例化對象數組中的起始位置
int unity_BaseInstanceID;
//當前渲染批次中的多例化對象的個數,如果是立體渲染,則以雙次渲染之前的個數爲準
int unity_InstanceCount;
#if !defined(SHADER_API_GLES3) && !defined(SHADER_API_GLCORE)
UNITY_INSTANCING_CBUFFER_SCOPE_END
#endif
#ifdef SHADER_API_PSSL
#define DEFAULT_UNITY_VERTEX_INPUT_INSTANCE_ID uint instanceID;
#define UNITY_GET_INSTANCE_ID(input) _GETINSTANCEID(input)
#else
#define DEFAULT_UNITY_VERTEX_INPUT_INSTANCE_ID uint instanceID : SV_InstanceID;
#define UNITY_GET_INSTANCE_ID(input) input.instanceID
#endif
#else
#define DEFAULT_UNITY_VERTEX_INPUT_INSTANCE_ID
#endif // UNITY_INSTANCING_ENABLED || UNITY_PROCEDURAL_INSTANCING_ENABLED || UNITY_STEREO_INSTANCING_ENABLED
可見,假如以非PlayStation平臺的Direct3D爲例,UNITY_VERTEX_INPUT_INSTANCE_ID宏本質上就是一行代碼:
unit instanceID:SV_InstanceID; //當前渲染批次下的頂點實例id
上一小節中struct appdata展開就是:
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
unit instanceID:SV_InstanceID; //當前渲染批次下的頂點實例id
};
以Direct3D 11爲例,該渲染批次下的實例id實質上就對應於在上節定義的uInstanceID,是一個32位的無符號整數,用於每一個着色器階段中識別當前正在被處理的幾何體實例,只要聲明瞭綁定SV_InstanceID語義的實例id變量,在輸入組裝渲染流水線會爲其賦值上一個實例id,然後在每一次繪製調用時,實例id變量的值將會加1,當此值超過了2的32次方減1後,將會重置爲0。
4.4 UNITY_INSTANCING_BUFFER_START及另外兩個配套的宏的定義
#if defined(UNITY_INSTANCING_ENABLED)
#define UNITY_INSTANCING_BUFFER_START(buf) UNITY_INSTANCING_CBUFFER_SCOPE_BEGIN(UnityInstancing_##buf) struct {
#define UNITY_INSTANCING_BUFFER_END(arr) } arr##Array[UNITY_INSTANCED_ARRAY_SIZE]; UNITY_INSTANCING_CBUFFER_SCOPE_END
#define UNITY_DEFINE_INSTANCED_PROP(type, var) type var;
#define UNITY_ACCESS_INSTANCED_PROP(arr, var) arr##Array[unity_InstanceID].var
4.5 UNITY_INSTANCED_APPLY_SIZE宏的定義
4.4節中的宏UNITY_INSTANCED_ARRAY_SIZE的定義如下:
#if defined(UNITY_INSTANCING_ENABLED)
#ifdef UNITY_FORCE_MAX_INSTANCE_COUNT
#define UNITY_INSTANCED_ARRAY_SIZE UNITY_FORCE_MAX_INSTANCE_COUNT
#elif defined(UNITY_INSTANCING_SUPPORT_FLEXIBLE_ARRAY_SIZE)
#define UNITY_INSTANCED_ARRAY_SIZE 2 // minimum array size that ensures dynamic indexing
#elif defined(UNITY_MAX_INSTANCE_COUNT)
#define UNITY_INSTANCED_ARRAY_SIZE UNITY_MAX_INSTANCE_COUNT
#else
#if defined(SHADER_API_VULKAN) && defined(SHADER_API_MOBILE)
#define UNITY_INSTANCED_ARRAY_SIZE 250
#else
#define UNITY_INSTANCED_ARRAY_SIZE 500
#endif
#endif
可以看出,UNITY_INSTANCING_BUFFER_START等三個宏的功能是定義一個着色器常量緩衝區,並對應定義一些着色器要使用的變量。4.1節中這三句話宏的定義在Direct3D 11平臺上可以展開爲
cbuffer UnityInstancingProps{
float4 _Color[500];
}
4.6 UNITY_SETUP_INSTANCE_ID宏的定義
#if !defined(UNITY_SETUP_INSTANCE_ID)
# define UNITY_SETUP_INSTANCE_ID(input) DEFAULT_UNITY_SETUP_INSTANCE_ID(input)
#endif
4.7 DEFAULT_UNITY_SETUP_INSTANCE_ID和UNITY_TRANSFER_INSTANCE_ID宏的定義
UNITY_SETUP_INSTANCE_ID宏轉包裝了DEFAULT_UNITY_SETUP_INSTANCE_ID宏,該宏的定義如下:
////////////////////////////////////////////////////////
// - UNITY_SETUP_INSTANCE_ID Should be used at the very beginning of the vertex shader / fragment shader,
// so that succeeding code can have access to the global unity_InstanceID.
// Also procedural function is called to setup instance data.
// - UNITY_TRANSFER_INSTANCE_ID Copy instance ID from input struct to output struct. Used in vertex shader.
#if defined(UNITY_INSTANCING_ENABLED) || defined(UNITY_PROCEDURAL_INSTANCING_ENABLED) || defined(UNITY_STEREO_INSTANCING_ENABLED)
void UnitySetupInstanceID(uint inputInstanceID)
{
#ifdef UNITY_STEREO_INSTANCING_ENABLED
#if defined(SHADER_API_GLES3)
// We must calculate the stereo eye index differently for GLES3
// because otherwise, the unity shader compiler will emit a bitfieldInsert function.
// bitfieldInsert requires support for glsl version 400 or later. Therefore the
// generated glsl code will fail to compile on lower end devices. By changing the
// way we calculate the stereo eye index, we can help the shader compiler to avoid
// emitting the bitfieldInsert function and thereby increase the number of devices we
// can run stereo instancing on.
unity_StereoEyeIndex = round(fmod(inputInstanceID, 2.0));
unity_InstanceID = unity_BaseInstanceID + (inputInstanceID >> 1);
#else
// stereo eye index is automatically figured out from the instance ID
unity_StereoEyeIndex = inputInstanceID & 0x01;
unity_InstanceID = unity_BaseInstanceID + (inputInstanceID >> 1);
#endif
#else
unity_InstanceID = inputInstanceID + unity_BaseInstanceID;
#endif
}
void UnitySetupCompoundMatrices();
#ifdef UNITY_PROCEDURAL_INSTANCING_ENABLED
#ifndef UNITY_INSTANCING_PROCEDURAL_FUNC
#error "UNITY_INSTANCING_PROCEDURAL_FUNC must be defined."
#else
//過程函數的前置聲明
void UNITY_INSTANCING_PROCEDURAL_FUNC(); // forward declaration of the procedural function
//第一步
#define DEFAULT_UNITY_SETUP_INSTANCE_ID(input) { UnitySetupInstanceID(UNITY_GET_INSTANCE_ID(input)); UNITY_INSTANCING_PROCEDURAL_FUNC(); UnitySetupCompoundMatrices(); }
#endif
#else
#define DEFAULT_UNITY_SETUP_INSTANCE_ID(input) { UnitySetupInstanceID(UNITY_GET_INSTANCE_ID(input)); UnitySetupCompoundMatrices(); }
#endif
#define UNITY_TRANSFER_INSTANCE_ID(input, output) output.instanceID = UNITY_GET_INSTANCE_ID(input)
#else
#define DEFAULT_UNITY_SETUP_INSTANCE_ID(input)
#define UNITY_TRANSFER_INSTANCE_ID(input, output)
#endif
UNITY_PROCEDURAL_INSTANCING_ENABLED宏表示,如果啓用自定義程序處理頂點實例化,那就需要使用者自行提供一個名爲UNITY_INSTANCING_PROCEDURAL_FUNC的宏,以設置頂點的實例ID。如代碼中的“第一步”註釋所標註的語句提示。如果UNITY_PROCEDURAL_INSTANCING_ENABLED宏未定義,則DEFAULT_UNITY_SETUP_INSTANCE_ID宏定義爲調用UnitySetupInstancingID函數,Unity3D定義了一個名爲unity_InstanceID的着色器變量,此變量的定義如下:
static uint unity_InstanceID;
unity_InstanceID就是用來索引某一個由輸入組裝生成的用戶定義的頂點屬性實例,即示例中的_Color屬性的某一個示例。而爲了訪問這些屬性的具體某一個實例值,在UnityInstancing.cginc文件中定義了一個UNITY_ACCESS_INSTANCED_PROP宏。此宏的功能是利用unity_InstanceID取得實例值,具體代碼如下:
#define UNITY_ACCESS_INSTANCED_PROP(arr, var) arr##Array[unity_InstanceID].var
unity_InstanceID的具體計算操作在UnitySetupInstanceID函數中執行。如上面代碼所示,如果在非立體渲染的情況下,unity_InstanceID爲當前輸入的頂點實例id與變量unity_BaseInstanceID之和。unity_BaseInstanceID的定義如下。
4.8 着色器常量緩衝區 UnityDrawCallInfo的定義
#if !defined(SHADER_API_GLES3) && !defined(SHADER_API_GLCORE)
//着色器常量緩衝區UnityDrawCallInfo
UNITY_INSTANCING_CBUFFER_SCOPE_BEGIN(UnityDrawCallInfo)
#endif
//當前渲染批次中是在多例化對象數組中的起始位置
int unity_BaseInstanceID;
//當前渲染批次中的多例化對象的個數,如果是立體渲染,則以雙次渲染之前的個數爲準
int unity_InstanceCount;
Unity引入了用來提高渲染效率的批次化渲染(batch render)技術。此技術的基本原理就是通過將一些渲染狀態一致的待渲染對象組成一組,一次性提交給GPU進行繪製,而不需要來回地設置渲染狀態,這可以顯著地節省繪製調用。使用了GPU多例化技術的待渲染對象的渲染狀態基本是一致的。因此對多例化的頂點進行分批次時,引擎底層會指明該批次的起始實例id及該批次有多少個實例id。着色器常量緩衝區中的兩個變量便記錄了這兩個信息。而UNITY_GET_INSTANCE_ID宏則是獲取到UNITY_VERTEX_INPUT_INSTANCE_ID宏所表示的當前渲染批次下的實例id。
綜上,4.1節中標註的UNITY_SETUP_INSTANCE_ID和UNITY_TRANSFER_INSTANCE_ID宏在Direct3D 11平臺上展開後的代碼如下:
unity_InstanceID=v.instanceID+unity_BaseInstanceID;
o.instanceID=v.instanceID;
4.9 4.1節展開多例化相關的宏之後的代碼
Shader "Unlit/testShader"
{
Properties
{
_Color("Color",Color)=(1,1,1,1)
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
//第一步,必須要使用這個編譯指示符宣告使用GPU多例化技術
#pragma multi_compile_instancing
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
//UNITY_VERTEX_INPUT_INSTANCE_ID
uint instanceID:SV_INSTANCE_ID; //當前渲染批次下的頂點實例id
};
struct v2f
{
float2 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
//第2步,如果要訪問片元着色器的多例化屬性變量,需要使用此宏
uint instanceID:SV_INSTANCE_ID;
//UNITY_VERTEX_INPUT_INSTANCE_ID
};
sampler2D _MainTex;
float4 _MainTex_ST;
//第3步,使用和外觀着色器相同的宣告多例化屬性變量語句
cbuffer UnityInstancingProps {
float4 _Color[500];
};
/*UNITY_INSTANCING_BUFFER_START(Props)
UNITY_DEFINE_INSTANCED_PROP(fixed4, _Color)
UNITY_INSTANCING_BUFFER_END(Props)*/
v2f vert (appdata v)
{
v2f o;
//第4步,如果要訪問片元着色器中的多例化變量,需要使用此宏
unity_InstanceID = v.instanceID + unity_BaseInstanceID;
o.instanceID = v.instanceID;
/*UNITY_SETUP_INSTANCE_ID(v);
UNITY_TRANSFER_INSTANCE_ID(v, o);*/
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
//第5步,如果要訪問片元着色器中的多例化屬性變量,需要使用此宏
unity_InstanceID = v.instanceID + unity_BaseInstanceID;
fixed4 col = tex2D(_MainTex, i.uv)*_Color[unity_InstanceID];
//UNITY_SETUP_INSTANCE_ID(i);
//fixed4 col = tex2D(_MainTex, i.uv)*UNITY_ACCESS_INSTANCED_PROP(_Color_arr, _Color);
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}