Unity SRP 02 Draw Calls

黎奠發表於2020-10-03

這是有關建立自定義指令碼渲染管道的系列教程的第二部分。 本次會講著色器的編寫以及如何高效的繪製多個物件。

Shader

為了繪製幾何體,CPU需要告訴GPU繪製什麼東西,以及如何繪製。所繪製的東西通常都是網格(mesh)。如何繪製可以通過Shader來控制。Shader就是一些GPU指令的集合。除了要繪製的網格,Shader還需要其他的資訊,其中包括變換矩陣,材質資訊。
Unity的LW(或稱Universal)RP和HDRP 允許你用ShaderGraph包來設計Shader,Shader Graph可以幫你生成Shader程式碼。但是我們的自定義的RP是不支援這個的,所以我們只能自己寫Shader。這也可以讓我們對Shader具體做了什麼有更好的理解。

Unlit Shader

我們的第一個shader就是簡單的完全用一個顏色去畫一個網格,不包括任何光照效果。一個shader資產可以通過Assets / Create / Shader 來建立。Unlit Shader 是最合適的,但是我們還是會刪掉裡面所有預製的程式碼。將新的Shader檔案命名為Unlit並放到Shaders資料夾下,Shaders資料夾在Runtime同層級的資料夾中,如圖。

原圖

Shader的程式碼大部分看起來很像C#程式碼,但是它包含多種不同的樣式,其中包括一些已經過時了的。
Shader 的定義類似一個類的定義,但是是通過用Shader關鍵字後跟隨一個字串來宣告的。該字串表示該Shader在選擇Shader的下拉選單中的位置。我們這裡用"Custom RP/Unlit"。然後會跟隨一對大括號,大括號裡面還會有很多前面帶著關鍵字的大括號。Properties塊定義了材質的屬性,然後是SubShader塊,該塊的內部需要有一個Pass塊,這個pass塊就描述了渲染物體的方法。

Shader "Custom RP/Unlit" {
	
	Properties {}
	
	SubShader {
		
		Pass {}
	}
}

這樣我們定義了一個可以用來建立材質球的,能通過編譯的最簡單的shader

原圖
預設材質會將材質渲染成純白色。材質會有一個預設的引數來控制RenderQueue,預設是2000,也就是不透明物體的渲染佇列。還有一個勾選框來啟用雙面的全域性照明,不過我們不關心這個。

本節做了:

  1. 通過選單建立shader檔案,命名為Unlit,放在“Custom RP/Shaders”資料夾下
  2. shader命名為 “Custom RP/Unlit”
  3. 包括兩個塊:PropertiesSubShader
  4. SubShader塊下包括一個Pass

HLSL程式碼

我們寫Shader所用的語言是 High-Level Shading Language,簡寫為HLSL。我們需要將這部分程式碼放在Pass塊中,並且被HLSLPROGRAMENDHLSL關鍵字包住。我們之所以這樣做是因為還可以將其他型別的shader語言放在Pass塊中。

		Pass {
			HLSLPROGRAM
			ENDHLSL
		}

為了繪製網格,GPU必須把連續的三角形(網格由很多三角形構成)變成一個個畫素點(稱為光柵化)。為了完成這件事,首先需要把每個頂點的座標從3D空間變換到2D空間,然後對被三角形覆蓋住的畫素進行填充。這兩步被不同的,我們可以編寫的shader程式控制。控制第一步的shader程式被稱為vertex shader,控制第二步的稱為fragment shader。每個片元(fragment)對應與螢幕上的一個畫素,或者紋理中的一個畫素(即紋素 texel),fragment並不是最終的畫素,因為它可能被其他的繪製在它上面的其他東西(比如其他fragment)蓋住。
我們需要為這兩個程式起個名字,通過編譯指令。這是一行由#pragma開頭,跟著vertexfragment,再跟著著色器的名字,我們這裡叫做UnlitPassVertexUnlitPassFragment

			HLSLPROGRAM
			#pragma vertex UnlitPassVertex
			#pragma fragment UnlitPassFragment
			ENDHLSL

shader 的編譯器現在會報錯找不到宣告的shader程式碼(UnlitPassVertex和UnilitPassFragment)我們需要實現兩個同名的HLSL函式,來定義這兩個著色器。我們可以直接把程式碼寫在這裡,但是我們並不這樣做。我們將這些HLSL程式碼放到另一個檔案中。我們把這個檔案命名為“UnlitPass.hlsl”在同一個資料夾下。我們可以用#include將這個檔案通過相對路徑新增進來。

			HLSLPROGRAM
			#pragma vertex UnlitPassVertex
			#pragma fragment UnlitPassFragment
			#include "UnlitPass.hlsl"
			ENDHLSL

Unity並沒有建立HLSL檔案的選單選項,所以我們自己來建立一個空檔案。

原圖

本節做了:

  1. HLSLPROGRAMENDHLSL包住Pass中的所有程式碼
  2. 指定頂點著色器,片元著色器,通過#pragma vertex UnlitPassVertex,和#pragma fragment UnlitPassFragment
  3. 在同資料夾中建立一個“UnlitPass.hlsl”檔案
  4. 在Pass中包含這個檔案

包含防護(Include Guard)

HLSL對檔案的分組類似於C#的類,雖然HLSL並沒有類的概念。除了程式碼塊的區域性作用域,HLSL只有一個全域性的作用域。所以所有的東西在任何地方都是可以訪問到的。將檔案包含(include)進來與引入(using)一個名稱空間也是不同的。這會將檔案中所有的程式碼拷貝到包含指令的位置上。所以如果包含了同一個檔案很多次,就會由重複的程式碼,並很有可能編譯出錯。為了防止這個問題,我們為UnilitPass.hlsl新增包含防護(Include Guard)。
我們可以用#define來完成上面的目的。我們在檔案的最開頭定義“CUSTOM_UNLIT_PASS_INCLUDED”

#define CUSTOM_UNLIT_PASS_INCLUDED

這是一個巨集的簡單的例子,它只定義了一個識別符號。如果這個識別符號以及存在那麼就說明我們的檔案已經被匯入了。所以這時我們不希望再次匯入。換句話說,我們只希望在沒有定義這個識別符號的時候插入程式碼。我們可以用#ifndef來檢查這個這件事情。在定義之前做這件事情。

#ifndef CUSTOM_UNLIT_PASS_INCLUDED
#define CUSTOM_UNLIT_PASS_INCLUDED

如果我們我們已經定義了這個識別符號,那麼在ifndef後面的程式碼就會被跳過。我們還需要用#endif來結束#ifndef。我們把它放在檔案的結尾。

#ifndef CUSTOM_UNLIT_PASS_INCLUDED
#define CUSTOM_UNLIT_PASS_INCLUDED
#endif

我們現在就可以保證檔案中的程式碼不會被插入多次,即使我們對其進行了多次包含操作。

本節做了:

  1. 在“UnlitPass.hlsl”檔案中進行巨集操作:
  2. 如果沒定義CUSTOM_UNLIT_PASS_INCLUDED,就定義這個標誌
  3. 在檔案結尾結束判斷
  4. 所有的程式碼寫在這個判斷範圍之內。保證程式碼只被包含一次

Shader函式

我們在保護的範圍內定義自己的shader函式。他們的語法格式就類似於C#的方法。

#ifndef CUSTOM_UNLIT_PASS_INCLUDED
#define CUSTOM_UNLIT_PASS_INCLUDED

void UnlitPassVertex () {}

void UnlitPassFragment () {}

#endif

現在我們的shader就已經可以編譯通過了。結果是一個青色的材質

原圖
通過修改片元函式的返回值,我們可以改變這個顏色。顏色被定義為有4個分量的float4型別的向量,這4個分量分別代表了紅綠藍透明度。我們可以只返回一個0,會自動擴充套件到整個向量。透明度在這裡不起作用,因為我們現在建立的是不透明shader

float4 UnlitPassFragment () {
	return 0.0;
}

我們應當用float還是half型別
大多數的GPU支援這兩種型別。而half會更高效。所以如果你在為移動裝置做優化,儘可能的用half是沒問題的。一個經驗法則是float只用在位置和紋理座標上,其他的地方都用half。
當不是面向移動平臺的時候,經度並不是問題,因為GPU總會用float無論你寫了什麼。我在本套教程中會用float。
還有一種fixed型別,不過這種型別只被舊的硬體支援。通常情況下等價於half

現在我們的shader還是會編譯失敗,因為這個函式沒有語義。我們必須指明返回值到底代表了什麼,因為有些情況可能會產生很多資料,並且有不同的作用。在這裡我們用系統定義的渲染目標 ,通過在宣告的後面加上冒號,再加上“SV_TARGET”。

float4 UnlitPassFragment () : SV_TARGET {
	return 0.0;
}

“UnlitPassVertex”負責處理頂點的座標,所以我們會返回一個位置,也是float4型別的,因為這個座標必須是其次裁剪座標系下的,我們會在後面說明。目前我們先用零向量來代表,並且把語義宣告為“SV_POSITION”

float4 UnlitPassVertex () : SV_POSITION {
	return 0.0;
}

本節做了:

  1. 在UnilitPass.hlsl檔案中定義兩個函式 UnlitPassFragment和UnlitPassVertex
  2. 注意定義返回的語義。

空間座標變換

當所有頂點的座標都設為0後,網格會塌陷成一個點,所以沒有任何東西被渲染出來。vertex函式的主要職責就是把頂點的位置變換到正確的座標系下。
在這個函式被呼叫的時候,會傳入我們需要的引數。我們通過給函式新增參照來做到這一點。我們需要頂點的區域性座標系中的位置,可以命名為“positionOS”,在Unity的新渲染管線下,也用這個名字。型別是float3,因為是一個3維空間的點。我們目前先直接返回這個數,並把第四個分量設定為1。

float4 UnlitPassVertex (float3 positionOS) : SV_POSITION {
	return float4(positionOS, 1.0);
}

對於這個輸入也需要新增語義,因為頂點資料不只有位置。在這裡需要在引數的後面加上冒號再加上“POSITION”

float4 UnlitPassVertex (float4 positionOS : POSITION) : SV_POSITION {
	return float4(positionOS, 1.0);
}


原圖
網格再次顯示出來了,但是由於我們輸出的座標並沒有被轉換到正確的座標系下,所以結果是不對的。座標轉換需要用到矩陣,這些矩陣會在GPU渲染東西的時候被傳入。我們需要新增這個矩陣到我們的shader中,但是由於這些矩陣總是相同的,所以我們將把Unity提供的標準輸入放到另一個HLSL檔案中,這樣既可以保持程式碼的整潔,也可以讓其他程式碼複用。新建一個“UnityInput.hlsl”檔案,並放到“ShaderLibrary”資料夾中,這個資料夾在“Custom RP”中,這與Unity的RP有相同的檔案結構。

原圖
我們用“CUSTOM_UNITY_INPUT_INCLUDED”作為包含保護的識別符號。然後我們在全域性作用域中定義一個float4x4型別的矩陣,叫做“unity_ObjectToWorld”。在c#類中,這類似於定義了一個欄位,但是在這裡一般被稱為是通用變數(uniform value)。它由GPU每次繪製設定一次,在每次繪製的期間,這個變數的值對所有頂點和片元函式的呼叫保持不變(統一)。

#ifndef CUSTOM_UNITY_INPUT_INCLUDED
#define CUSTOM_UNITY_INPUT_INCLUDED

float4x4 unity_ObjectToWorld;

#endif

你可以用矩陣進行座標空間到世界空間的轉換。由於這是個通用的功能,所以我們為他建立一個函式,並放到另一個檔案中,叫做“Common.hlsl”同樣在“ShaderLibrary”資料夾下。我們在這個資料夾中包含“UnityInput.hlsl”,然後宣告一個“TransformObjectToWorld”函式,並用float3作為返回值,和引數。

#ifndef CUSTOM_COMMON_INCLUDED
#define CUSTOM_COMMON_INCLUDED

#include "UnityInput.hlsl"

float3 TransformObjectToWorld (float3 positionOS) {
	return 0.0;
}
	
#endif

座標系的轉換通過呼叫mul函式將矩陣與向量相乘做到。在這裡,我們需要一個4維的向量,但是第四維的值永遠是1,我們可以通過float4(positionOS, 1.0)得到。返回值依然是4維的,我們可以通過.xyz提取出前三維。

float3 TransformObjectToWorld (float3 positionOS) {
	return mul(unity_ObjectToWorld, float4(positionOS, 1.0)).xyz;
}

我們先在UnlitPassVertex中將位置轉換到世界座標系下。首先包含“Common.hlsl”。由於不再同一個資料夾中,所以我們需要用相對路徑“…/ShaderLibrary/Common.hlsl”找到他。然後用“TransformObjectToWorld”計算得到一個新變數“positionWS”,並用其在返回的地方替換掉物體空間的位置。

#include "../ShaderLibrary/Common.hlsl"

float4 UnlitPassVertex (float3 positionOS : POSITION) : SV_POSITION {
	float3 positionWS = TransformObjectToWorld(positionOS.xyz);
	return float4(positionWS, 1.0);
}

結果仍然是錯誤的,因為我們需要齊次剪輯空間中的位置。 該空間定義了一個立方體,其中包含攝像機所看到的所有內容,如果是透視攝像機,則它會變形為梯形。 從世界空間到該空間的變換可以通過與檢視投影矩陣相乘來完成,該檢視投影矩陣考慮了相機的位置,方向,投影,視野和遠近裁剪平面。 將其新增到UnityInput.hlsl。

float4x4 unity_ObjectToWorld;

float4x4 unity_MatrixVP;

在“Common.hlsl”中新增“TransformWorldToHClip”函式,這與“TransformObjectToWorld”類似,只不過輸入的是世界空間的座標。最後會返回float4。

float3 TransformObjectToWorld (float3 positionOS) {
	return mul(unity_ObjectToWorld, float4(positionOS, 1.0)).xyz;
}

float4 TransformWorldToHClip (float3 positionWS) {
	return mul(unity_MatrixVP, float4(positionWS, 1.0));
}

然後我們在UnlitPassVertex 中呼叫這個函式,得到正確的座標

float4 UnlitPassVertex (float3 positionOS : POSITION) : SV_POSITION {
	float3 positionWS = TransformObjectToWorld(positionOS.xyz);
	return TransformWorldToHClip(positionWS);
}


原圖

本節做了:

  1. 建立一個“UnityInput.hlsl”檔案在“Custom RP/ShaderLibrary”資料夾下
  2. CUSTOM_UNITY_INPUT_INCLUDED做包含保護
  3. 宣告變數:float4x4 unity_ObjectToWorld;
  4. 宣告變數:float4x4 unity_MatrixVP;
  5. 建立一個“Common.hlsl”檔案,在相同的資料夾下。
  6. CUSTOM_COMMON_INCLUDED做包含保護
  7. 包含“UnityInput.hlsl”
  8. 建立函式 TransformObjectToWorld 入參和返回值都是float3型別
  9. 返回 mul(unity_ObjectToWorld, float4(positionOS, 1.0)).xyz
  10. 建立函式 TransformWorldToHClip 入參是float3,返回flaot4
  11. 返回 mul(unity_MatrixVP, float4(positionWS, 1.0))
  12. 在 “UnlitPass.hlsl”中修改頂點著色器:
  13. 增加float3型別的入參,語義定義為POSITION
  14. 依次呼叫上面建立的兩個函式,然後返回結果。

核心庫

剛剛我們寫的那兩個函式由於太通用了,所以在“Core RP Pipeline”中也有他們的定義。核心庫有很多有用的基礎的東西,所以我們安裝這個包,然後刪掉我們自己的定義,幷包含相關的檔案,在這個路徑中“Packages/com.unity.render-pipelines.core/ShaderLibrary/SpaceTransforms.hlsl”

//float3 TransformObjectToWorld (float3 positionOS) {
//	return mul(unity_ObjectToWorld, float4(positionOS, 1.0)).xyz;
//}

//float4 TransformWorldToHClip (float3 positionWS) {
//	return mul(unity_MatrixVP, float4(positionWS, 1.0));
//}

#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/SpaceTransforms.hlsl"

這樣會編譯失敗,這是由於“SpaceTransforms.hls”並沒有假設已經有unity_ObjectToWorld矩陣了,檔案中需要用到這個矩陣的地方通過標識“UNITY_MATRIX_M”來代替。所以我們可以通過巨集定義這個標識,即在包含這個檔案之前寫上#define UNITY_MATRIX_M unity_ObjectToWorld。後面所有的“UNITY_MATRIX_M”都會被unity_ObjectToWorld代替。這樣做的原因在後面會講到。

#define UNITY_MATRIX_M unity_ObjectToWorld

#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/SpaceTransforms.hlsl"

對於逆矩陣unity_WorldToObject也是同樣的,通過標識“UNITY_MATRIX_I_M”來代表。unity_MatrixV 通過UNITY_MATRIX_V代表;unity_MatrixVP通過UNITY_MATRIX_VP代表;glstate_matrix_projection通過UNITY_MATRIX_P代表。我們不需要這些矩陣,但是如果不定義這些,就無法編譯通過。

#define UNITY_MATRIX_M unity_ObjectToWorld
#define UNITY_MATRIX_I_M unity_WorldToObject
#define UNITY_MATRIX_V unity_MatrixV
#define UNITY_MATRIX_VP unity_MatrixVP
#define UNITY_MATRIX_P glstate_matrix_projection

然後我們在“UnityInput”中加入這些額外的變數

float4x4 unity_ObjectToWorld;
float4x4 unity_WorldToObject;

float4x4 unity_MatrixVP;
float4x4 unity_MatrixV;
float4x4 glstate_matrix_projection;

我們還剩下一些與矩陣無關的事情。就是unity_WorldTransformParams,包含了一些變換資訊,但是在這裡我們並不需要。這是個real4型別的變數。real4只是個別名,具體可能是float4half4,這取決於目標平臺。

float4x4 unity_ObjectToWorld;
float4x4 unity_WorldToObject;
real4 unity_WorldTransformParams;

這個別名和許多其他的基礎的巨集都是按照圖形API定義的,我們可以通過包含“ Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl”得到。所以在我們的“Common.hlsl”中,在包含“UnityInput.hlsl”之前我們可以包含這個檔案。如果您對這些檔案的內容感興趣,可以看看包中的這些檔案。

#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
#include "UnityInput.hlsl"

本節做了:

  1. 安裝“Core RP Pipeline”包
  2. 在“Common.hlsl”檔案中刪掉自己寫的函式,按順序做如下操作
  3. 包含 “Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl”
  4. 包含 “UnityInput.hlsl”
  5. 對用巨集對UNITY_MATRIX_M,UNITY_MATRIX_I_M,UNITY_MATRIX_V,UNITY_MATRIX_VP,UNITY_MATRIX_P進行定義,上面有程式碼
  6. 包含 “Packages/com.unity.render-pipelines.core/ShaderLibrary/SpaceTransforms.hlsl”
  7. 在 “UnityInput.hlsl”中新增宣告,上面同樣有程式碼
  8. 新增宣告 real4 unity_WorldTransformParams;

顏色

物體的顏色可以通過UnlitPassFragment來改變。例如,我們可以通過返回float4(1.0, 1.0, 0.0, 1.0)來讓物體變成黃色。

float4 UnlitPassFragment () : SV_TARGET {
	return float4(1.0, 1.0, 0.0, 1.0);
}

為了能夠配置每種材料的顏色,我們必須將其定義為統一的值。 在UnlitPassVertex函式之前,在include指令下執行此操作。 我們需要一個float4並將其命名為_BaseColor。 前劃線是表明這個變數是材料特性的命名規範。 返回此值,而不是UnlitPassFragment中的硬編碼顏色。

#include "../ShaderLibrary/Common.hlsl"

float4 _BaseColor;

float4 UnlitPassVertex (float3 positionOS : POSITION) : SV_POSITION {
	float3 positionWS = TransformObjectToWorld(positionOS);
	return TransformWorldToHClip(positionWS);
}

float4 UnlitPassFragment () : SV_TARGET {
	return _BaseColor;
}

這又變回了黑色,因為預設值是0。為了讓材質與我們新增的“_BaseColor”聯絡起來,我們需要在“Unlit”shader檔案中的 Properties塊中新增“_BaseColor”屬性。

	Properties {
		_BaseColor
	}

屬性後面會跟著一個小括號,其中第一個字串是在Inspector皮膚中顯示的名字,第二個代表了這個值的型別

		_BaseColor("Color", Color)

最後我們提供一個預設值,用一組數字組成

		_BaseColor("Color", Color) = (1.0, 1.0, 1.0, 1.0)


原圖

本節做了

  1. 在“UnlitPass.hlsl”中宣告變數float4 _BaseColor;
  2. 在shader檔案“Unlit”中的Properties中做如下宣告:_BaseColor("Color", Color) = (1.0, 1.0, 1.0, 1.0)

分批

每個DrawCall需要在CPU和GPU之間傳遞資料。如果有很多資料需要傳到GPU,則GPU有可能會浪費時間在等待上。而且當CPU在傳送資料的時候,是沒辦法做其他事情的。這兩個問題都會降低幀率。現在我們用了很直接的辦法,每個物體都有自己的DrawCall。這樣很浪費資源,只是因為我們現在的資料量不大,所以看不到影響。
我做了一個場景,裡面有76個球,用來四個不同顏色的材質:紅色,綠色,黃色,藍色。這需要78次drawcall來渲染,76個是球的,1個是天空盒,1個是清理渲染目標

原圖
如果在Game皮膚下開啟Stats皮膚,就可以看到當前幀的資料概覽。這裡顯示有77個batch(忽略了清理渲染目標),以及沒有被batch儲存的資料(Saved by batching)
Batching是Drawcall的合併,可減少CPU和GPU之間進行通訊的時間。 最簡單的方法是啟用SRP批處理程式。 但是,這僅適用於相容的著色器,而對我們的Unlit著色器無效。 您可以選擇它,在Inspector中看到這個資訊。 有一個SRP Batcher行指示不相容,並給出了原因。

原圖
SRP batch不會減少draw call的數量,而是使其更精簡。 它在GPU上快取了材質屬性,因此不必在每次繪製呼叫時都將其傳送出去。 這樣既減少了必須傳送的資料量,又減少了每個draw call,CPU必須完成的工作。 但這僅在著色器遵守用於統一的嚴格的資料結構時才有效。
所有材料屬性都必須在具體的儲存緩衝區內定義,而不是在全域性級別上定義。 這是通過將_BaseColor宣告包裝在帶有UnityPerMaterial名稱的cbuffer塊中來完成的。 這就像一個結構體的宣告,但必須以分號終止。 它會將將_BaseColor放入特定的常量記憶體緩衝區,不過仍可在全域性級別訪問。

cbuffer UnityPerMaterial {
	float _BaseColor;
};

並非所有平臺(例如OpenGL ES 2.0)都支援常量緩衝區,因此,我們可以使用核心RP庫中包含的CBUFFER_START和CBUFFER_END巨集,而不是直接使用cbuffer。 第一個將緩衝區名稱作為引數,就好像它是一個函式一樣。 在這種情況下,我們得到的結果與之前完全相同,只是不支援cbuffer的平臺不存在cbuffer程式碼。

CBUFFER_START(UnityPerMaterial)
	float4 _BaseColor;
CBUFFER_END

我們還必須對unity_ObjectToWorld,unity_WorldToObject和unity_WorldTransformParams執行此操作,只不過他們要分組在UnityPerDraw緩衝區中。

CBUFFER_START(UnityPerDraw)
	float4x4 unity_ObjectToWorld;
	float4x4 unity_WorldToObject;
	real4 unity_WorldTransformParams;
CBUFFER_END

在這種情況下,如果我們使用某個值,就需要將這個值所在的組整個都進行定義。 對於變換組,即使我們不使用它,我們也需要包括float4 unity_LODFade。 順序無關緊要,但是Unity會將其直接放在unity_WorldToObject之後,因此我們也要這樣做。

CBUFFER_START(UnityPerDraw)
	float4x4 unity_ObjectToWorld;
	float4x4 unity_WorldToObject;
	float4 unity_LODFade;
	real4 unity_WorldTransformParams;
CBUFFER_END


原圖
現在我們的著色器就相容了,下一步是啟用SRP批處理程式,這是通過將GraphicsSettings.useScriptableRenderPipelineBatching設定為true來完成的。 我們只需要執行一次,因此在建立管道例項時來執行此操作。

	public CustomRenderPipeline () {
		GraphicsSettings.useScriptableRenderPipelineBatching = true;
	}


原圖
“統計”皮膚顯示儲存了76個批次,雖然顯示為負數。 frame debugger現在在RenderLoopNewBatcher.Draw下顯示一個SRP Batch條目,但是請記住,它不是單個draw call,而是對它們的序列的優化。

原圖

本節做了:

  1. CBUFFER_STARTCBUFFER_END 封裝 shader中的全域性變數
  2. 將 材質屬性_BaseColor封裝在UnityPerMaterial
  3. 將 下面四個屬性封裝在UnityPerDraw中 注意,多了一條LOD相關的
    1. float4x4 unity_ObjectToWorld;
    2. float4x4 unity_WorldToObject;
    3. float4 unity_LODFade;
    4. real4 unity_WorldTransformParams;
  4. 在構造CustomRP的時候, 啟用SPR的Batch: GraphicsSettings.useScriptableRenderPipelineBatching = true;

更多的顏色

即使我們使用四種材料,也只用一個批。 之所以可行,是因為它們的所有資料都快取在GPU上,並且每個繪製呼叫僅需包含一個指向正確記憶體位置的偏移量。 唯一的限制是每種材料的記憶體佈局必須相同,這是因為我們對所有材料都使用相同的著色器,每個著色器僅包含一個顏色屬性。 Unity不會比較材質的確切記憶體佈局,它只是會對使用相同的著色器的材質採用批處理。
如果我們需要幾種不同的顏色,則可以很好地工作,但是如果我們要為每個球體賦予自己的顏色,那麼我們就必須建立更多的材料。 如果我們可以對每個物件都更改顏色(只使用一個材質),則會更加方便。 預設情況下這是不可能的,但是我們可以通過建立自定義元件型別來支援它。 將其命名為PerObjectMaterialProperties。 作為一個示例,我將其放在“ Custom RP”下的“ Examples”資料夾中。
這個想法是,一個遊戲物件可以附加一個PerObjectMaterialProperties元件,該元件具有“基礎顏色”配置選項,該選項將用於為其設定_BaseColor材質屬性。 它需要知道shader屬性的識別符號,可以通過Shader.PropertyToID檢索該識別符號並將其儲存在靜態變數中,就像在CameraRenderer中為shader pass識別符號所做的那樣,儘管在這種情況下,它是int型別的。

using UnityEngine;

[DisallowMultipleComponent]
public class PerObjectMaterialProperties : MonoBehaviour {
	
	static int baseColorId = Shader.PropertyToID("_BaseColor");
	
	[SerializeField]
	Color baseColor = Color.white;
}


原圖
設定每個物件的材質屬性是通過MaterialPropertyBlock物件完成的。 我們只需要一個例項,因為所有PerObjectMaterialProperties例項都可以重用該例項,因此可以為其宣告一個靜態欄位。

	static MaterialPropertyBlock block;

如果該欄位還沒有被賦值,請建立一個新例項,然後在其上呼叫SetColor,傳入shader屬性的id和顏色,然後通過SetPropertyBlock將塊應用於遊戲物件的Renderer元件,該元件複製其設定。 在OnValidate中執行此操作,以便結果立即顯示在編輯器中。

	void OnValidate () {
		if (block == null) {
			block = new MaterialPropertyBlock();
		}
		block.SetColor(baseColorId, baseColor);
		GetComponent<Renderer>().SetPropertyBlock(block);
	}

我將元件新增到24個任意球體中,併為其賦予了不同的顏色。

原圖
不幸的是,SRP的batch無法處理這種情況。 因此,這24個球體每個都屬於一次常規的draw call,由於排序,也可能將其他球體分成多個批次。

原圖
另外,OnValidate不會在構建中被呼叫。 為了使各個顏色在那裡出現,我們還必須在Awake中應用它們,我們可以通過簡單地在此處呼叫OnValidate來實現。

	void Awake () {
		OnValidate();
	}

本節做了:

  1. 宣告一個元件, 叫做PerObjectMaterialProperties
  2. Shader.PropertyToID("_BaseColor")得到shader中屬性的id, 儲存在靜態欄位中.
  3. 有一個負責控制顏色的欄位.
  4. 有一個靜態的, MaterialPropertyBlock型別的欄位. 用來設定材質的屬性
  5. 在OnValidate中呼叫 MaterialPropertyBlock欄位的SetColor, 傳入shader的屬性id和值
  6. 呼叫Renderer元件的SetPropertyBlock, 將 MaterialPropertyBlock欄位傳入.
  7. 在OnAwake中呼叫OnValidate. 這是因為OnValidate在釋出之後是無效的.

GPU例項

還有一種合併繪製呼叫的方法,該方法就可以處理上面的情況。 這就是所謂的GPU例項化,其工作原理是一次對具有相同網格物體的多個物件發出一次繪圖呼叫。 CPU收集所有每個物件的變換和材質屬性,並將它們放入陣列中,然後傳送給GPU。 然後,GPU遍歷所有條目,並按提供順序對其進行渲染。
因為GPU例項需要通過陣列提供資料,而我們的著色器當前不支援。 進行此工作的第一步是在著色器的Pass塊的頂點和片元設定的上方新增#pragma multi_compile_instancing指令。

			#pragma multi_compile_instancing
			#pragma vertex UnlitPassVertex
			#pragma fragment UnlitPassFragment

這個會讓Unity生成該Shader的兩個變體, 一個帶有GPU例項,一個沒有. 在材質的屬性皮膚上會多出一個勾選框, 來代表應用哪個版本的Shader

為了支援GPU例項化,程式碼會有一些變換,為此,我們必須包括來自核心著色器庫的UnityInstancing.hlsl檔案。 在定義UNITY_MATRIX_M和其他巨集之後並在包含SpaceTransforms.hlsl之前完成此操作。

#define UNITY_MATRIX_P glstate_matrix_projection

#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/UnityInstancing.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/SpaceTransforms.hlsl"

UnityInstancing.hlsl的作用是重新定義這些巨集來訪問例項資料陣列。 但是要進行這項工作,需要知道當前正在渲染的物件的索引。 索引是通過頂點資料提供的,因此我們必須使其可用。 UnityInstancing.hlsl定義了巨集來簡化此過程,但是它們假定我們的頂點著色器函式是以一個結構體作為引數的。
可以宣告一個結構體(語法的結構類似cbuffer)並將其用作函式的輸入引數。 我們還可以在結構體內部定義語義。 這種方法的優點是,它比長引數列表更清晰易讀。 因此,將UnlitPassVertex的positionOS引數包裝在新的Attributes結構中,以表示頂點輸入資料。

struct Attributes {
	float3 positionOS : POSITION;
};

float4 UnlitPassVertex (Attributes input) : SV_POSITION {
	float3 positionWS = TransformObjectToWorld(input.positionOS);
	return TransformWorldToHClip(positionWS);
}

使用GPU例項化時,物件索引也可用作頂點屬性。 我們可以在適當的時候通過簡單地將UNITY_VERTEX_INPUT_INSTANCE_ID放在屬性中來新增它。

struct Attributes {
	float3 positionOS : POSITION;
	UNITY_VERTEX_INPUT_INSTANCE_ID
};

接下來,新增UNITY_SETUP_INSTANCE_ID(input); 在UnlitPassVertex的開頭。 這將從輸入中提取索引,並將其儲存在全域性靜態變數中, 其他GPU例項巨集會依賴這些變數。

float4 UnlitPassVertex (Attributes input) : SV_POSITION {
	UNITY_SETUP_INSTANCE_ID(input);
	float3 positionWS = TransformObjectToWorld(input.positionOS);
	return TransformWorldToHClip(positionWS);
}

這足以使GPU例項化工作,但是由於SRP的批處理有更高的優先順序,因此我們現在沒有得到不同的結果。 但是我們還不能得到每個例項的材質資料。 為此我們要用陣列引用替換_BaseColor。 這是通過用UNITY_INSTANCING_BUFFER_START替換CBUFFER_START以及用UNITY_INSTANCING_BUFFER_END替換CBUFFER_END來完成的。這個巨集也需要一個引數, 這個引數不必與CBUFFER_START的一樣,但是也沒必要使它們有所不同。

//CBUFFER_START(UnityPerMaterial)
//	float4 _BaseColor;
//CBUFFER_END

UNITY_INSTANCING_BUFFER_START(UnityPerMaterial)
	float4 _BaseColor;
UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)

然後用UNITY_DEFINE_INSTANCED_PROP(float4,_BaseColor)替換_BaseColor的定義。

UNITY_INSTANCING_BUFFER_START(UnityPerMaterial)
	//	float4 _BaseColor;
	UNITY_DEFINE_INSTANCED_PROP(float4, _BaseColor)
UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)

使用例項化時,我們現在還必須在UnlitPassFragment中提供例項索引。 為了簡單起見,我們將使用一個結構,通過UNITY_TRANSFER_INSTANCE_ID(input,output)來獲取到UnlitPassVertex輸出位置和索引。 複製索引(如果存在)。 我們像Unity一樣用Varying命名此結構,因為它包含的資料在同一三角形的片段之間可能會有所不同。

struct Varyings {
	float4 positionCS : SV_POSITION;
	UNITY_VERTEX_INPUT_INSTANCE_ID
};

Varyings UnlitPassVertex (Attributes input) { //: SV_POSITION {
	Varyings output;
	UNITY_SETUP_INSTANCE_ID(input);
	UNITY_TRANSFER_INSTANCE_ID(input, output);
	float3 positionWS = TransformObjectToWorld(input.positionOS);
	output.positionCS = TransformWorldToHClip(positionWS);
	return output;
}

將此結構作為UnlitPassFragment的引數。 然後像以前一樣使用UNITY_SETUP_INSTANCE_ID來使索引可用。 現在必須通過UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial,_BaseColor)訪問material屬性。

float4 UnlitPassFragment (Varyings input) : SV_TARGET {
	UNITY_SETUP_INSTANCE_ID(input);
	return UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _BaseColor);
}


原圖
現在,Unity可以將24個球體與每個物件的顏色組合在一起,從而減少了draw call的次數。 我最後進行了四個例項化的繪製呼叫,因為這些球體仍使用其中的四種材料。 GPU例項化僅適用於所有共享相同材質的物件。 當我們只更改材料顏色時,它們可以使用相同的材質,然後就可以在一個batch中繪製。

原圖
請注意,基於目標平臺以及每個例項必須提供多少資料,批處理大小是有限制的。 如果超過此限制,那麼最終將導致一批以上。 此外,如果使用多種材質,排序仍可以拆分批次。(沒看懂)

本節做了:

  1. 在shader程式碼宣告頂點片元函式之前加一句:#pragma multi_compile_instancing
  2. 將材質上多出來的 “啟用GPU例項” 打勾
  3. 在定義了 核心包SpaceTransforms中所需變數名 之後, 但是在包含這個檔案之前, 包含Packages/com.unity.render-pipelines.core/ShaderLibrary/UnityInstancing.hlsl
  4. 將 UnlitPassVertex 函式的入參改為結構體, 叫做Attributes. 並在結構體中加入巨集UNITY_VERTEX_INPUT_INSTANCE_ID
  5. 在頂點著色器中用UNITY_SETUP_INSTANCE_ID(input);來初始化其他的巨集, 其中input是vertex的入參(即帶有UNITY_VERTEX_INPUT_INSTANCE_ID的結構體).
  6. 將頂點著色器函式的返回值和片元著色器函式的入參都變為結構體, 叫做Varyings , 同樣在結構體中加入UNITY_VERTEX_INPUT_INSTANCE_ID.
  7. 在頂點著色器中用UNITY_TRANSFER_INSTANCE_ID(input, output);將頂點資料從輸入的Attributes結構體傳送到輸出的Varyings結構體中
  8. 將 shader中材質屬性的變數用UNITY_INSTANCING_BUFFER_STARTUNITY_INSTANCING_BUFFER_END封起來(而不是原來的cbuffer), 傳入的名字不變(可以是任意的)
  9. UNITY_DEFINE_INSTANCED_PROP(float4, _BaseColor)來宣告材質屬性
  10. 在片元著色器中, 同樣用UNITY_SETUP_INSTANCE_ID(input)初始化GPU例項化相關的巨集
  11. 在呼叫材質屬性的時候, 需要用如下形式 UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _BaseColor). 第一個引數就是塊的名字

更多參考:
官方GPU例項化的介紹

多個例項網格的繪製

手動編輯場景中的許多物件是不切實際的。 因此,讓我們隨機生成一些物體。 建立一個MeshBall示例元件,該元件在 Awake 中會生成許多物件。 讓它快取著色器的_BaseColor屬性,並新增兩個用來配置的成員: 網格和支援例項化的材質。

using UnityEngine;

public class MeshBall : MonoBehaviour {

	static int baseColorId = Shader.PropertyToID("_BaseColor");

	[SerializeField]
	Mesh mesh = default;

	[SerializeField]
	Material material = default;
}

建立一個遊戲物件, 新增這個元件。我這裡用預設的求球模型。

原圖
我們可以生成許多新的遊戲物件,但不必這樣做。 相反,我們將填充變換矩陣和顏色的陣列,並告訴GPU用它們渲染網格。 這是GPU例項化最有用的地方。 我們最多可以一次提供1023個例項,因此讓我們新增一個長為1023的陣列欄位,以及需要傳遞顏色資料的MaterialPropertyBlock。 在這種情況下,顏色陣列的元素型別必須為Vector4。

	Matrix4x4[] matrices = new Matrix4x4[1023];
	Vector4[] baseColors = new Vector4[1023];

	MaterialPropertyBlock block;

建立一個Awake方法,該方法會隨機位置和顏色, 去填充陣列.

	void Awake () {
		for (int i = 0; i < matrices.Length; i++) {
			matrices[i] = Matrix4x4.TRS(
				Random.insideUnitSphere * 10f, Quaternion.identity, Vector3.one
			);
			baseColors[i] =
				new Vector4(Random.value, Random.value, Random.value, 1f);
		}
	}

在Update中,如果不存在新塊,則建立一個新塊,並在其上呼叫SetVectorArray來配置顏色。 之後,呼叫Graphics.DrawMeshInstanced。 我們在此處設定塊,以便球網格能夠承受熱過載We set up the block here so the mesh ball survives hot reloads.(沒看懂)。

	void Update () {
		if (block == null) {
			block = new MaterialPropertyBlock();
			block.SetVectorArray(baseColorId, baseColors);
		}
		Graphics.DrawMeshInstanced(mesh, 0, material, matrices, 1023, block);
	}

現在進入遊戲模式將產生很多球。 由於每個Draw Call的最大緩衝區大小不同,因此需要多少次繪圖呼叫取決於平臺。 在作者的情況下,需要進行三個繪製呼叫才能進行渲染。
請注意,各個網格的繪製順序與我們提供資料的順序相同。 除此之外,沒有任何排序或剔除的方法,儘管一旦整個批處理在視錐範圍內消失,整個批處理都將消失。

本節做了:

  • 建立1023個隨機的變換矩陣(物體空間到世界空間), 通過Matrix4x4.TRS, 並儲存起來
  • 建立1023個隨機顏色, 這裡要用Vector4型別的陣列, 因為後面要傳給shader
  • 建立MaterialPropertyBlock型別的物件block
  • 取得shader屬性"_BaseColor"的ID.
  • 每幀呼叫block.SetVectorArray, 通過ID來設定一個屬性在不同物體上的值. 這裡設定"_BaseColor", 值就是前面的顏色陣列.
  • Graphics.DrawMeshInstanced(網格, sub_mesh, 材質, 變換矩陣陣列, 數量, 材質屬性block)來繪製. 其中網格,材質是共享的. 變換矩陣和材質屬性是每個物件獨有的. 因此變換矩陣是陣列, 而材質屬性block本身具有儲存多組資料的功能.

動態分批

還有一種降低 Draw call 的方法. 這是一種很老的技術, 它會將一些應用相同材質的小網格合併到一個大網格中. 這種技術也沒有辦法為每個小網格設定不同的屬性.
大網格要根據需要生成, 這隻適用於一些小網格. 對於球來說就已經過大了. 但是對於cube來說是可以的. 為了看到效果, 我們關掉GPU Instancing. 然後設定enableDynamicBatchingtrue.

		var drawingSettings = new DrawingSettings(
			unlitShaderTagId, sortingSettings
		) {
			enableDynamicBatching = true,
			enableInstancing = false
		};

禁用SPR的批處理.

		GraphicsSettings.useScriptableRenderPipelineBatching = false;


原圖

總的來說,GPU例項化比動態批處理工作得更好。該方法也有一些注意事項,例如,如果縮放不同,那麼大網格的法向量不能保證是單位長度。同時,繪製順序也會改變,因為它現在是一個單一的網格而不是多個。
還有靜態批處理,它的工作方式與此類似,但它會提前對標記為批處理靜態的物件進行處理。除了需要更多的記憶體和儲存,它不會產生其他的錯誤( it has no caveats 不確定是不是這個意思) 。RP不知道這一點,所以我們不用擔心。

配置批處理

上面那種技術更好, 對於不同情況是不同的. 所以我們讓其可以進行配置. 首先, 我們用函式的引數來控制, 而不是硬編碼:

	void DrawVisibleGeometry (bool useDynamicBatching, bool useGPUInstancing) {
		var sortingSettings = new SortingSettings(camera) {
			criteria = SortingCriteria.CommonOpaque
		};
		var drawingSettings = new DrawingSettings(
			unlitShaderTagId, sortingSettings
		) {
			enableuseDynamicBatching = useDynamicBatching,
			enableInstancing = useGPUInstancing
		};}

Render函式也需要這些引數:

	public void Render (
		ScriptableRenderContext context, Camera camera,
		bool useDynamicBatching, bool useGPUInstancing
	) {DrawVisibleGeometry(useDynamicBatching, useGPUInstancing);}

在 RenderPipeline 中增加相應選項

	bool useDynamicBatching, useGPUInstancing;

	public CustomRenderPipeline (
		bool useDynamicBatching, bool useGPUInstancing, bool useSRPBatcher
	) {
		this.useDynamicBatching = useDynamicBatching;
		this.useGPUInstancing = useGPUInstancing;
		GraphicsSettings.useScriptableRenderPipelineBatching = useSRPBatcher;
	}

	protected override void Render (
		ScriptableRenderContext context, Camera[] cameras
	) {
		foreach (Camera camera in cameras) {
			renderer.Render(
				context, camera, useDynamicBatching, useGPUInstancing
			);
		}
	}

在 RenderPipelineAsset 中增加相應選項

	[SerializeField]
	bool useDynamicBatching = true, useGPUInstancing = true, useSRPBatcher = true;

	protected override RenderPipeline CreatePipeline () {
		return new CustomRenderPipeline(
			useDynamicBatching, useGPUInstancing, useSRPBatcher
		);
	}

透明物體

我們的著色器無法用於透明的材質。當我們改變顏色的 alpha 通道時,通常代表著透明度, 但是現在沒有效果。我們可以設定渲染佇列到 Transparent, 但是這只是改變了物體的繪製順序,而不是如何繪製。
在這裡插入圖片描述
我們並不需要再另外寫一個shader去支援透明材質。只要再多做一些事情, 我們的Unlit材質就可以同時支援透明材質和不透明材質。

混合模式

渲染透明物體與渲染不透明物體之間的主要區別就是,我們是選擇把原來繪製的東西全都用新的顏色代替,還是把新顏色與原來的顏色混合一下產生透視的效果。我們可以通過設定 源與目標的混合模式( source and destination blend modes)來控制這件事。source 代表現在將要畫的顏色,destination 代表前面已經畫上的顏色,以及最終要展示的顏色。我們用兩個 Shader 屬性來控制:_SrcBlend, _DstBlend :

	Properties {
		_BaseColor("Color", Color) = (1.0, 1.0, 1.0, 1.0)
		_SrcBlend ("Src Blend", Float) = 1
		_DstBlend ("Dst Blend", Float) = 0
	}

為了使編輯更容易, 我們把引數變成列舉形式.

		[Enum(UnityEngine.Rendering.BlendMode)] _SrcBlend ("Src Blend", Float) = 1
		[Enum(UnityEngine.Rendering.BlendMode)] _DstBlend ("Dst Blend", Float) = 0

在這裡插入圖片描述

預設值表示我們已經使用的不透明混合配置。源被設定為1,這意味著它將被完全新增,而目標被設定為0,這意味著它將被忽略。標準透明度的源混合模式是SrcAlpha,alpha越低,它就越弱。然後將目標混合模式設定為反向:OneMinusSrcAlpha,以使總權重為1。
在這裡插入圖片描述
混合模式可以在Pass塊中定義,後面跟著Blend語句。我們想要使用著色器屬性,我們可以通過把它們放在方括號中來訪問它們。這是在可程式設計著色器之前的舊語法。

		Pass {
			Blend [_SrcBlend] [_DstBlend]

			HLSLPROGRAM
			…
			ENDHLSL
		}

在這裡插入圖片描述

不進行深度寫入

透明渲染通常不會寫入深度緩衝區,因為它無法從中受益,甚至可能會產生不良結果。 我們可以通過ZWrite語句控制是否寫入深度。 同樣,我們可以使用著色器屬性,這次使用_ZWrite。

			Blend [_SrcBlend] [_DstBlend]
			ZWrite [_ZWrite]

使用自定義的Enum(Off,0,On,1)屬性定義著色器屬性,以建立預設值為on和off的on-off開關,值分別為0和1。

		[Enum(UnityEngine.Rendering.BlendMode)] _SrcBlend ("Src Blend", Float) = 1
		[Enum(UnityEngine.Rendering.BlendMode)] _DstBlend ("Dst Blend", Float) = 0
		[Enum(Off, 0, On, 1)] _ZWrite ("Z Write", Float) = 1

在這裡插入圖片描述

貼圖

以前,我們使用Alpha貼圖來建立不均勻的半透明材質。 通過向著色器新增_BaseMap紋理屬性,我們也對此進行支援。 在這種情況下,型別為2D,我們將使用Unity的標準白色紋理作為預設設定,並以白色字串表示。 另外,我們必須以空程式碼塊結束texture屬性。 它很早以前就用於控制紋理設定,但今天仍應包括在內,以防止某些情況下的怪異錯誤。

		_BaseMap("Texture", 2D) = "white" {}
		_BaseColor("Color", Color) = (1.0, 1.0, 1.0, 1.0)

在這裡插入圖片描述
紋理必須上傳到GPU記憶體,Unity會為我們做。 著色器需要一個相關紋理的控制程式碼,我們可以定義一個 uniform 值, 不過我們使用TEXTURE2D巨集。 我們還需要為紋理定義一個取樣器狀態,以控制其取樣方式,並考慮其環繞模式和過濾模式。 這是通過SAMPLER巨集完成的, 並且以"sampler"作為字首. 這與Unity自動提供的取樣器狀態的名稱相同。
紋理和取樣器狀態是著色器資源。 不能按例項提供,必須在全域性範圍內宣告。 在UnlitPass.hlsl中的著色器屬性之前執行此操作。

TEXTURE2D(_BaseMap);
SAMPLER(sampler_BaseMap);

UNITY_INSTANCING_BUFFER_START(UnityPerMaterial)
	UNITY_DEFINE_INSTANCED_PROP(float4, _BaseColor)
UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)

除此之外,Unity還可以通過float4來提供紋理的平鋪和偏移,該float4與texture屬性同名,但附加了_ST,代表縮放和平移等。 該屬性應該是UnityPerMaterial緩衝區的一部分,因此可以按例項設定

UNITY_INSTANCING_BUFFER_START(UnityPerMaterial)
	UNITY_DEFINE_INSTANCED_PROP(float4, _BaseMap_ST)
	UNITY_DEFINE_INSTANCED_PROP(float4, _BaseColor)
UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)

要取樣紋理,我們需要紋理座標,它是頂點屬性的一部分。 具體來說,我們需要第一對座標,因為可能還要更多。 這是通過將具有TEXCOORD0語義的float2欄位新增到Attributes 結構體來完成的。我們將其命名為baseUV。

struct Attributes {
	float3 positionOS : POSITION;
	float2 baseUV : TEXCOORD0;
	UNITY_VERTEX_INPUT_INSTANCE_ID
};

我們需要將座標傳遞給片段函式,因為在那裡對紋理進行了取樣。 因此也將float2 baseUV新增到Varyings中。 這次我們不需要新增特殊含義,只是我們傳遞的資料不需要GPU的特別注意。 但是,我們仍然必須賦予它語義。 我們可以應用任何未使用的識別符號,讓我們簡單地使用VAR_BASE_UV。

struct Varyings {
	float4 positionCS : SV_POSITION;
	float2 baseUV : VAR_BASE_UV;
	UNITY_VERTEX_INPUT_INSTANCE_ID
};

當我們在UnlitPassVertex中複製座標時,我們還可以應用儲存在_BaseMap_ST中的比例尺和偏移量。 這樣,我們就按頂點而不是按片段進行處理。 比例尺儲存在XY中,偏移量儲存在ZW中,我們可以通過swizzle屬性訪問它們。

Varyings UnlitPassVertex (Attributes input) {
	…

	float4 baseST = UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _BaseMap_ST);
	output.baseUV = input.baseUV * baseST.xy + baseST.zw;
	return output;
}

現在,UV座標可用於UnlitPassFragment,而且已經做了插值。 在這裡,通過使用SAMPLE_TEXTURE2D巨集進行取樣。 最終的顏色是紋理顏色與基礎顏色相乘。 將兩個相同大小的向量相乘會導致所有匹配分量相乘,因此在這種情況下,紅色乘以紅色,綠色乘以綠色,依此類推。

float4 UnlitPassFragment (Varyings input) : SV_TARGET {
	UNITY_SETUP_INSTANCE_ID(input);
	float4 baseMap = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.baseUV);
	float4 baseColor = UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _BaseColor);
	return baseMap * baseColor;
}

在這裡插入圖片描述

Alpha 裁剪

透視表面的另一種方法是在表面上挖洞。 著色器也可以通過丟棄通常會渲染的某些片段來做到這一點。 這樣會產生硬邊,而不是我們當前看到的平滑過渡。 這種技術稱為alpha裁剪。 完成此操作的通常方法是定義一個截止閾值。 alpha值低於此閾值的片段將被丟棄,而所有其他片段將保留。

		_BaseColor("Color", Color) = (1.0, 1.0, 1.0, 1.0)
		_Cutoff ("Alpha Cutoff", Range(0.0, 1.0)) = 0.5

將屬性加入到 UnlitPass.hlsl 中

	UNITY_DEFINE_INSTANCED_PROP(float4, _BaseColor)
	UNITY_DEFINE_INSTANCED_PROP(float, _Cutoff)

我們可以通過呼叫UnlitPassFragment中的clip函式來丟棄片段。 如果我們傳遞的值為零或更小,它將中止並丟棄該片段。 因此,將最終的alpha值(可通過a或w屬性訪問)減去截止閾值傳遞給它。

	float4 baseMap = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.baseUV);
	float4 baseColor = UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _BaseColor);
	float4 base = baseMap * baseColor;
	clip(base.a - UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _Cutoff));
	return base;

在這裡插入圖片描述
在這裡插入圖片描述

材質通常使用透明混合或Alpha裁剪,而不同時使用兩者。 除丟棄的片段外,典型的剪輯材料是完全不透明的,並且確實會寫入深度緩衝區。 它使用AlphaTest渲染佇列,這意味著它將在所有完全不透明的物件之後進行渲染。 這樣做是因為丟棄片段使某些GPU優化無法實現,因為不再假定三角形完全覆蓋了它們後面的內容。 通過首先繪製完全不透明的物件,它們可能最終覆蓋了部分alpha剪下物件,然後無需處理其隱藏片段。
在這裡插入圖片描述
在這裡插入圖片描述
但是,要使此優化工作有效,我們必須確保僅在需要時才使用剪輯。 我們將通過新增功能切換著色器屬性來做到這一點。 這是一個Float屬性,預設情況下設定為零,具有一個控制著色器關鍵字的Toggle屬性,我們將使用_CLIPPING作為引數。 屬性本身的名稱無關緊要,因此只需使用_Clipping。

		_Cutoff ("Alpha Cutoff", Range(0.0, 1.0)) = 0.5
		[Toggle(_CLIPPING)] _Clipping ("Alpha Clipping", Float) = 0

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-R3g4NT0s-1601711835860)(https://catlikecoding.com/unity/tutorials/custom-srp/draw-calls/transparency/alpha-clipping-off.png#pic_center)]

Shader Features

啟用切換功能會將_CLIPPING關鍵字新增到材質的活動關鍵字列表中,而禁用則將其刪除。 但這並不能單獨做任何事情。 我們必須告訴Unity根據關鍵字是否已定義來編譯著色器的不同版本。 為此,我們將#pragma shader_feature _CLIPPING新增到其Pass中的指令中。

			#pragma shader_feature _CLIPPING
			#pragma multi_compile_instancing

現在,無論是否定義了_CLIPPING,Unity都將編譯我們的著色器程式碼。 它將生成一個或兩個變體,具體取決於我們如何配置材料。 我們可以為此使用#ifdef _CLIPPING,但我更喜歡#if defined(_CLIPPING)

	#if defined(_CLIPPING)
		clip(base.a - UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _Cutoff));
	#endif

對於每個物體分別進行 Cutoff

由於截止值是UnityPerMaterial緩衝區的一部分,因此可以按例項進行配置。 因此,讓我們將該功能新增到PerObjectMaterialProperties中。 除了需要在屬性塊上呼叫SetFloat而不是SetColor之外,它的作用與顏色相同。

	static int baseColorId = Shader.PropertyToID("_BaseColor");
	static int cutoffId = Shader.PropertyToID("_Cutoff");

	static MaterialPropertyBlock block;

	[SerializeField]
	Color baseColor = Color.white;

	[SerializeField, Range(0f, 1f)]
	float cutoff = 0.5f;void OnValidate () {
		…
		block.SetColor(baseColorId, baseColor);
		block.SetFloat(cutoffId, cutoff);
		GetComponent<Renderer>().SetPropertyBlock(block);
	}

Ball of Alpha-Clipped Spheres

MeshBall也是如此。 現在,我們可以使用 裁剪材質,但是所有例項最終都具有完全相同的洞。
在這裡插入圖片描述
讓我們通過給每個例項一個隨機的旋轉,加上一個在0.5-1.5範圍內的隨機均勻比例,來增加一些變化。 但是,與其設定每個例項的截止值,不如將它們的顏色的Alpha通道更改為0.5–1範圍。 這給我們帶來了不太精確的控制,但是無論如何這是一個隨機的例子。

			matrices[i] = Matrix4x4.TRS(
				Random.insideUnitSphere * 10f,
				Quaternion.Euler(
					Random.value * 360f, Random.value * 360f, Random.value * 360f
				),
				Vector3.one * Random.Range(0.5f, 1.5f)
			);
			baseColors[i] =
				new Vector4(
					Random.value, Random.value, Random.value,
					Random.Range(0.5f, 1f)
				);

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-VSUOPQXq-1601713266925)(https://catlikecoding.com/unity/tutorials/custom-srp/draw-calls/transparency/more-varied-ball.png#pic_center)]

請注意,Unity仍然最終會向GPU傳送一個截止值陣列,每個例項一個,即使它們都是相同的。 該值是材料的副本,因此,通過更改它可以一次更改所有球體的孔,即使它們仍然不同。

相關文章