Unity 渲染教程(二):着色器基礎

原文出處:http://gad.qq.com/program/translateview/7173930


這是關於渲染基礎的系列教程的第二部分。這個渲染基礎的系列教程的第一部分是有關矩陣的內容。在這篇文章中我們將編寫我們的第一個着色器代碼並導入紋理。


這個系列教程是使用Unity 5.4.0開發的,這個版本目前還是開放測試版本。我使用的是build 5.4.0b10版本。

Unity 渲染教程(二):着色器基礎
對球使用紋理。


1. 默認的場景

當你在Unity中創建新的場景的時候,你將使用默認的相機和定向的光源。  通過GameObject / 3D Object / Sphere這個菜單項來創建一個簡單的球體,然後把這個簡單的球體放在原點,並把相機放在它的前面。

Unity 渲染教程(二):着色器基礎
默認場景中的默認球體。

這是一個非常簡單的場景,但是已經有很多複雜的渲染內容了。爲了更好地抓住渲染過程,擺脫所有奇怪的東西是非常有幫助的,首先讓我們只關心渲染的基礎部分。


1.1 剝離那些和渲染無關的內容

通過菜單項Window / Lighting來查看場景的光照設置。 這將打開一個具有三個選項卡的光照窗口。我們只對默認情況下處於激活狀態的「場景」選項卡感興趣。

Unity 渲染教程(二):着色器基礎
默認的光照設置。

這是一個關於環境光照的部分,你可以在其中選擇天空盒。這個天空盒目前用於場景背景、環境光照和反射。讓我們將其設置爲none進行關閉。

在你進行設置的時候,你還可以關閉預計算和實時全局光照的面板。我們不會很快使用到這些東西。

Unity 渲染教程(二):着色器基礎
不再使用天空盒了。

在沒有天空盒的情況下,環境光源會自動切換爲純色。環境光源的默認顏色爲深灰色,具有非常淺的藍色色調。而反射變爲純黑色,如警告框所示。

正如你可能期望的那樣,球體會變得更暗,背景會是純色。但是,得到的結果卻是背景是深藍色。那麼這個顏色來自哪裏?

Unity 渲染教程(二):着色器基礎
簡化後的光照

背景顏色是根據攝像機來定義的。它在默認情況下會渲染天空盒,但是它也會回落到純色狀態。

Unity 渲染教程(二):着色器基礎
默認的相機設置。

 爲什麼背景顏色的透明通道值爲5而不是255?

要進一步簡化渲染的話,請取消方向光源對象的激活或將其刪除。這將擺脫場景中的直接光照,以及由直接光照所投射的陰影。剩下的就是背景,會用環境顏色顯示球體的輪廓。

Unity 渲染教程(二):着色器基礎
球體處於黑暗之中。



2. 從物體到二維圖像

我們這個非常簡單的場景是用兩個步驟繪製出來的。 首先,圖像用相機的背景顏色進行填充。然後將我們的球體的輪廓繪製在填充顏色的上面。

Unity怎麼知道它必須畫一個球體? 我們有一個球體對象,這個對象有一個網格渲染器組件。如果此對象位於相機的視圖內,那麼就應該出現在最終的圖像中。 Unity通過檢查對象的包圍盒否與相機的視錐體相交來驗證這一點。


Unity 渲染教程(二):着色器基礎
默認的球體。

變換組件用於改變網格和包圍盒的位置、方向和大小。實際上,整個變換層次都會被用到,正如第1部分「矩陣」中所描述的那樣。如果對象會出現在相機的視圖中,則這個物體會被安排進行渲染。

最後,圖形處理器負責渲染對象的網格。 特定的渲染指令由對象的材質定義。 材質引用了着色器 - 這是一個圖形處理器程序,加上它可能有的任何設置。

Unity 渲染教程(二):着色器基礎
每個組件控制着渲染哪些內容。

我們的對象目前有默認材質,它使用Unity的標準着色器。我們要用我們自己的着色器來代替它,我們將從頭開始構建它。



2.1 你的第一個着色器程序

通過Assets / Create / Shader / Unlit Shader創建一個新的着色器,並且將它命名爲類似My First Shader這樣的名字。

Unity 渲染教程(二):着色器基礎
你的第一個着色器程序。

打開着色器文件並刪除其內容,所以我們可以從頭開始。

着色器代碼用Shader關鍵字定義。它後面是一個字符串,描述可用於選擇此着色器的着色器菜單項。它不需要匹配文件名。然後跟着的是填充了着色器內容的塊。
1
2
3
Shader "Custom/My First Shader" {
 
}
保存文件。 你將收到不支持這個着色器的警告,因爲它沒有子着色器或是備選着色器。這是因爲它是空的緣故。

雖然這個着色器沒有什麼功能,但是我們可以將它分配給一個材質。因此,通過Assets / Create / Material來創建一個新材質,並從着色器菜單中選擇我們的着色器。

Unity 渲染教程(二):着色器基礎 Unity 渲染教程(二):着色器基礎
使用了你的着色器的材質。

更改我們的球體對象,使我們的球體對象使用我們自己的材質,而不是默認材質。 球體將變爲洋紅色。發生這種情況是因爲Unity會切換到一個錯誤的着色器,它使用這種顏色來引起你對問題的注意。

Unity 渲染教程(二):着色器基礎 Unity 渲染教程(二):着色器基礎
使用了你的着色器的材質。

着色器的錯誤提示信息中提到子着色器。你可以使用這些子着色器將多個着色器變量組合在一起。這允許你爲不同的構建平臺或者LOD值不同的情況下提供不同的子着色器。讓我們舉個簡單的例子來說,你可以爲桌面電腦上運行的那個應用使用一個子着色器,而爲移動設備上運行的應用使用另一個子着色器。
1
2
3
4
5
6
Shader "Custom/My First Shader" {
 
    SubShader {
         
    }
}
子着色器裏面必須包含至少一個通道。着色器通道是對象實際被渲染的地方。 我們將使用一個通道,但着色器裏面可以有多個。具有多個通道意味着對象被多次渲染,這是很多效果所需要的。
1
2
3
4
5
6
7
8
9
Shader "Custom/My First Shader" {
 
    SubShader {
 
        Pass {
 
        }
    }
}
我們的球體現在可能變成白色,因爲我們使用的是一個空通道的默認行爲。如果發生這種情況,這意味着我們不再收到任何着色器錯誤的提示信息。但是,你可能仍然在控制檯中看到舊的錯誤提示信息。編輯器傾向於堅持提示錯誤信息,因爲當着色器重新編譯而沒有錯誤的時候,這些錯誤提示信息是不會被清除的。

Unity 渲染教程(二):着色器基礎
一個白色的球體。



2.2 着色器程序

現在是時候來編寫我們自己的着色器程序了。我們用Unity的着色語言來做這個功能,這是HLSL和CG着色語言的變體。我們必須用CGPROGRAM關鍵字指示我們的代碼開始。我們必須以ENDCG關鍵字來指示我們的代碼終止。
1
2
3
4
5
Pass {
    CGPROGRAM
 
    ENDCG
}

着色器編譯器現在會發出警告,警告我們的着色器裏面沒有頂點程序和片段程序。着色器由兩個程序組成,也就是頂點程序和片段程序。頂點程序負責處理網格的頂點數據。 這包括從對象空間到顯示空間的轉換,就像我們在第1部分「矩陣」中所做的那樣。片段程序負責對位於網格三角形內的單個像素進行渲染。

Unity 渲染教程(二):着色器基礎
頂點程序和片段程序。

 我們必須通過pragma指令告訴編譯器使用哪些程序。
1
2
3
4
5
6
CGPROGRAM
 
#pragma vertex MyVertexProgram
#pragma fragment MyFragmentProgram
 
ENDCG

編譯器會再次發出警告,這次因爲它找不到我們指定的程序。這是因爲我們還沒有定義這些程序的緣故。

定義頂點程序和片段程序就像定義方法一樣,非常像C#裏面的做法,雖然它們通常被稱爲函數。讓我們簡單地創建兩個空的返回void的方法,並給它們適當的名稱。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
CGPROGRAM
 
#pragma vertex MyVertexProgram
#pragma fragment MyFragmentProgram
 
void MyVertexProgram () {
 
}
 
void MyFragmentProgram () {
 
}
 
ENDCG
此時,着色器將編譯,球體將消失。或者你仍然會得到錯誤信息提示。這取決於你的編輯器使用的是哪個渲染平臺。如果你使用的是Direct3D 9渲染平臺,你可能會得到錯誤信息提示。



2.3 着色器的編譯

Unity的着色器編譯器接受我們的代碼,並將其轉換爲不同的程序,具體如何轉換取決於目標平臺。不同的平臺需要不同的解決方案。 例如,如果是Windows 平臺的話,需要的是Direct3D,如果是Mac平臺的話,需要的是OpenGL ,如果是移動平臺的話,需要的是OpenGL ES,等等。 我們不是在這裏處理單個編譯器,而是處理多個編譯器。

你最終使用哪個編譯器取決於你的定位。由於這些編譯器並不相同,因此每個平臺可能會產生不同的結果。舉個簡單的例子來說,我們的空程序使用OpenGL和Direct3D 11的話,就能正常工作,但在如果使用的是Direct3D 9,就會失敗。

在編輯器中選擇着色器,並查看檢查器窗口。它會顯示有關着色器的一些信息,包括當前的編譯器錯誤。還有一個帶有「編譯和顯示代碼」按鈕和下拉菜單的「編譯代碼」項。 如果單擊「編譯和顯示代碼」按鈕,Unity將編譯着色器代碼並在編輯器中打開着色器代碼的輸出,因此你可以檢查生成的代碼具體是什麼。

Unity 渲染教程(二):着色器基礎
着色器檢查器,會顯示在所有平臺上的錯誤信息。

 你可以通過下拉菜單手動選擇編譯着色器的平臺。默認情況下是使用編輯器所使用的圖形設備進行編譯。你可以手動選擇其他平臺進行編譯,無論是你當前的構建平臺,還是你有許可證的所有平臺,或是其他的自定義選擇。這使你能夠快速確保你的着色器在多個平臺上能夠正常的編譯,而不必進行完整的構建。

Unity 渲染教程(二):着色器基礎
選擇OpenGLCore。

要編譯所選的程序,請關閉彈出窗口,然後單擊「編譯並顯示代碼」按鈕。單擊彈出窗口中的小的「顯示」按鈕將顯示使用的着色器變量,但是這在現在沒有用。

舉個簡單的例子來說明,這裏當我們的着色器是爲OpenGlCore平臺編譯的時候得到的代碼。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// Compiled shader for custom platforms, uncompressed size: 0.5KB
 
// Skipping shader variants that would not be included into build of current scene.
 
Shader "Custom/My First Shader" {
SubShader {
 Pass {
  GpuProgramID 16807
Program "vp" {
SubProgram "glcore " {
"#ifdef VERTEX
#version 150
#extension GL_ARB_explicit_attrib_location : require
#extension GL_ARB_shader_bit_encoding : enable
void main()
{
    return;
}
#endif
#ifdef FRAGMENT
#version 150
#extension GL_ARB_explicit_attrib_location : require
#extension GL_ARB_shader_bit_encoding : enable
void main()
{
    return;
}
#endif
"
}
}
Program "fp" {
SubProgram "glcore " {
"// shader disassembly not supported on glcore"
}
}
 }
}
}
 生成的代碼被分割爲兩個塊,vp和fp,分別用於頂點程序和片段程序。然而,在OpenGL的情況下,兩個程序都在vp塊中。兩個主要函數對應於兩個我們的空方法。 所以讓我們關注這兩個主要函數並忽略其他代碼。
1
2
3
4
5
6
7
8
9
10
11
12
#ifdef VERTEX
void main()
{
    return;
}
#endif
#ifdef FRAGMENT
void main()
{
    return;
}
#endif
 這裏是爲Direct3D 11生成的代碼,讓我們只剝離出那些有趣的部分。它看起來很不同,但很明顯,代碼沒有做太多的工作。
1
2
3
4
5
6
7
8
9
10
11
12
Program "vp" {
SubProgram "d3d11 " {
      vs_4_0
   0: ret
}
}
Program "fp" {
SubProgram "d3d11 " {
      ps_4_0
   0: ret
}
}
當我們處理我們的程序時,我會經常顯示OpenGL Core和D3D11平臺的編譯代碼,所以就可以對具體內部發生了什麼有一個比較明確的認識。



2.4 導入其他文件

要生成具有功能的着色器代碼,你會需要很多模板代碼。比如定義公共變量、函數和其他東西的代碼。 如果這是一個C#程序的話,我們會將這些代碼放在其他的類中。但是着色器沒有類的概念。它們只是一個包含所有代碼的大文件,沒有類或命名空間提供的分組功能。

幸運的是,我們可以將代碼拆分成多個文件。你可以使用#include指令將不同文件的內容加載到當前文件中。一個典型的文件包括UnityCG.cginc,所以讓我們這樣做一下看看效果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CGPROGRAM
 
#pragma vertex MyVertexProgram
#pragma fragment MyFragmentProgram
 
#include "UnityCG.cginc"
 
void MyVertexProgram () {
 
}
 
void MyFragmentProgram () {
 
}
 
ENDCG
 UnityCG.cginc是與Unity捆綁在一起的着色器導入文件之一。它包括一些其他必要的文件,幷包含一些通用的功能。

Unity 渲染教程(二):着色器基礎
導入文件的層次結構,從UnityCG開始。

UnityShaderVariables.cginc 定義了渲染所需要的一大堆着色器變量,例如變換、相機和光照數據。 這些都是在需要的情況下,在Unity編輯器裏面進行設置的。

HLSLSupport.cginc 設置的是那些無論你的目標平臺是什麼,你都可以使用相同的代碼的東西。所以你不需要擔心使用特定於某個平臺的數據類型等事情。

UnityInstancing.cginc 是專門用於實例化支持的,這是一種減少繪製調用的特定渲染技術。雖然它不直接導入文件,它取決於UnityShaderVariables的信息。

請注意,這些文件的內容被有效地複製到你自己的文件中,替換了導入指令。這個過程發生在預處理步驟的期間,預處理步驟會執行所有的預處理指示。這些指令都是以哈希開頭的語句,例如#include和#pragma。在預處理步驟完成之後,着色器代碼被再次處理,並且被實際編譯。




2.5  生成輸出

爲了渲染某些東西,我們的着色器程序必須能夠輸出結果。頂點程序必須返回頂點的最終座標。 一共會有多少個座標? 四個,因爲我們使用的是4×4變換矩陣,正如這個系列的第1部分《矩陣》中所描述的那樣。

將函數的類型從void更改爲float4。float4只是四個浮點數的集合。但是現在,讓我們只返回0。
1
2
3
float4 MyVertexProgram () {
    return 0;
}
我們現在得到的錯誤信息提示是關於缺少語義。着色器的編譯器看到我們返回一個四個浮點數的集合,但是它不知道這個四個浮點數的集合代表着什麼。所以它不知道圖形處理器應該用它做什麼。我們必須非常具體地瞭解我們的程序的輸出。
在這種情況下,我們試圖輸出頂點的位置。 我們必須通過將SV_POSITION語義附加到我們的方法來指明這一點。 SV表示系統值,而POSITION表示最終頂點位置。
1
2
3
float4 MyVertexProgram () : SV_POSITION {
    return 0;
}
片段程序應該輸出一個像素的RGBA顏色值。 我們可以使用float4 類型。返回0將爲這個像素使用一個固定的顏色。
1
2
3
float4 MyFragmentProgram () {
    return 0;
}
片段程序也需要語義。 在這種情況下,我們必須指出最終的顏色應該寫在哪裏。 我們使用SV_TARGET,它是默認的着色器目標,也就是幀緩衝區,其中包含着我們正在生成的圖像。
1
2
3
float4 MyFragmentProgram () : SV_TARGET {
    return 0;
}
 但是,等等,頂點程序的輸出會被用作片段程序的輸入。這表明片段程序應該得到一個與頂點程序的輸出相匹配的參數。
1
2
3
float4 MyFragmentProgram (float4 position) : SV_TARGET {
    return 0;
}
 不管我們給參數起什麼名字,我們都必須確保使用正確的語義。
1
2
3
4
5
float4 MyFragmentProgram (
    float4 position : SV_POSITION
) : SV_TARGET {
    return 0;
}
我們的着色器再次編譯,沒有錯誤信息提示,但是球體消失了。這不應該令人感到驚訝的,因爲我們將球體所有的頂點摺疊到了一個點。

如果你看下編譯後的OpenGL核心程序,你會看到他們現在在寫入輸出值。我們的單顏色值確實已被四分量向量所代替。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#ifdef VERTEX
void main()
{
    gl_Position = vec4(0.0, 0.0, 0.0, 0.0);
    return;
}
#endif
#ifdef FRAGMENT
layout(location = 0) out vec4 SV_TARGET0;
void main()
{
    SV_TARGET0 = vec4(0.0, 0.0, 0.0, 0.0);
    return;
}
#endif
 D3D11程序也是如此,儘管語法是不同的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Program "vp" {
SubProgram "d3d11 " {
      vs_4_0
      dcl_output_siv o0.xyzw, position
   0: mov o0.xyzw, l(0,0,0,0)
   1: ret
}
}
Program "fp" {
SubProgram "d3d11 " {
      ps_4_0
      dcl_output o0.xyzw
   0: mov o0.xyzw, l(0,0,0,0)
   1: ret
}
}



2.6  對頂點進行變換

爲了能夠讓我們的球再次顯示出來,我們的頂點程序必須產生一個正確的頂點位置。 爲此,我們需要知道頂點在物體空間中的位置。我們可以通過向我們的函數添加一個帶POSITION語義的變量來訪問它。然後將該位置提供爲齊次座標的形式  ,所以它的類型是float4。
1
2
3
float4 MyVertexProgram (float4 position : POSITION) : SV_POSITION {
    return 0;
}
 讓我們從直接返回這個位置信息開始。
1
2
3
float4 MyVertexProgram (float4 position : POSITION) : SV_POSITION {
    return position;
}
 編譯後的頂點程序現在有一個頂點的輸入信息並將其複製到其輸出裏面去。
1
2
3
4
5
6
in  vec4 in_POSITION0;
void main()
{
    gl_Position = in_POSITION0;
    return;
}
1
2
3
4
5
6
Bind "vertex" Vertex
      vs_4_0
      dcl_input v0.xyzw
      dcl_output_siv o0.xyzw, position
   0: mov o0.xyzw, v0.xyzw
   1: ret

Unity 渲染教程(二):着色器基礎
原始頂點位置。

黑色球體將變得可見,但它的位置被扭曲。這是因爲我們使用的是物體空間的位置,我們把球在物體空間的位置當做了球的顯示位置。因此,移動球體將在視覺上不會產生差別。

我們必須將原始的頂點位置乘以模型-視圖-投影矩陣。模型-視圖-投影矩陣將對象的變換層次與相機變換和投影相結合,就像我們在這個系列的第1部分《矩陣》中做的那樣。

4 x 4的模型-視圖-投影矩陣在UnityShaderVariables中被定義爲UNITY_MATRIX_MVP。我們可以使用mul函數將它與頂點的位置相乘。這將把我們的球體正確地投影到顯示器上去。你還可以移動、旋轉和縮放它,並且圖像將按照預期改變。
1
2
3
float4 MyVertexProgram (float4 position : POSITION) : SV_POSITION {
    return mul(UNITY_MATRIX_MVP, position);
}

Unity 渲染教程(二):着色器基礎
位置投影正確的球體。

 如果你檢查OpenGLCore平臺上編譯出來的頂點程序,你會注意到一個統一的變量突然出現在代碼裏面。即使它們沒有被代碼使用並且將被忽略,訪問矩陣這個事情觸發了編譯器將整個塊都導入進來了。

 你還將看到矩陣乘法被編碼爲一堆乘法和加法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
uniform     vec4 _Time;
uniform     vec4 _SinTime;
uniform     vec4 _CosTime;
uniform     vec4 unity_DeltaTime;
uniform     vec3 _WorldSpaceCameraPos;
in  vec4 in_POSITION0;
vec4 t0;
void main()
{
    t0 = in_POSITION0.yyyy * glstate_matrix_mvp[1];
    t0 = glstate_matrix_mvp[0] * in_POSITION0.xxxx + t0;
    t0 = glstate_matrix_mvp[2] * in_POSITION0.zzzz + t0;
    gl_Position = glstate_matrix_mvp[3] * in_POSITION0.wwww + t0;
    return;
}
 D3D11的編譯器不會包含未使用的變量。 它用一個mul和三個mad指令對矩陣乘法進行編碼。mad指令表示加法之後緊跟着乘法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Bind "vertex" Vertex
ConstBuffer "UnityPerDraw" 352
Matrix 0 [glstate_matrix_mvp]
BindCB  "UnityPerDraw" 0
      vs_4_0
      dcl_constantbuffer cb0[4], immediateIndexed
      dcl_input v0.xyzw
      dcl_output_siv o0.xyzw, position
      dcl_temps 1
   0: mul r0.xyzw, v0.yyyy, cb0[1].xyzw
   1: mad r0.xyzw, cb0[0].xyzw, v0.xxxx, r0.xyzw
   2: mad r0.xyzw, cb0[2].xyzw, v0.zzzz, r0.xyzw
   3: mad o0.xyzw, cb0[3].xyzw, v0.wwww, r0.xyzw
   4: ret



3.  給像素顏色

現在我們得到了正確的形狀,讓我們來添加一些顏色。最簡單的是使用固定的顏色,例如黃色。
1
2
3
4
5
float4 MyFragmentProgram (
    float4 position : SV_POSITION
) : SV_TARGET {
    return float4(1, 1, 0, 1);
}

Unity 渲染教程(二):着色器基礎
黃顏色的球體。

 當然,你不總是想要黃色的對象。在理想情況下,我們的着色器將支持任何的顏色。然後,你可以使用該材質配置你要應用的顏色。這是通過着色器的屬性完成的。




3.1 着色器的屬性

着色器屬性在單獨的塊中聲明。讓我們將它添加到着色器代碼的頂部。
1
2
3
4
5
6
7
8
9
Shader "Custom/My First Shader" {
 
    Properties {
    }
 
    SubShader {
        
    }
}
 在新的塊中放入一個名爲_Tint的屬性。你可以給它任何名稱,但通常的約定是以下劃線開始,後面跟一個大寫字母,然後是小寫字母。這麼做是確保沒有什麼別的地方會使用這個名字,以防止意外的重複名稱。
1
2
3
Properties {
    _Tint
}
 屬性的名稱後面必須跟着一個字符串和一個類型,放在圓括號中,就像調用一個方法一樣。該字符串用於標記材質檢查器中的屬性。 在這種情況下,類型是isColor。
1
2
3
Properties {
    _Tint ("Tint", Color)
}
 屬性聲明的最後一部分是給一個默認值賦值。讓我們將這個默認值設置爲白色。
1
2
3
Properties {
    _Tint ("Tint", Color) = (1, 1, 1, 1)
}
 我們的tint屬性不應該出現在我們着色器檢查器的屬性部分。

Unity 渲染教程(二):着色器基礎
着色器屬性。

 當你選擇材質的時候,你將看到新的Tint屬性,被設置爲白色。你可以將Tint屬性更改爲任何你喜歡的顏色,比如說是綠色。

Unity 渲染教程(二):着色器基礎
 材質的屬性。




3.2 訪問屬性

 要實際使用屬性,我們向着色器代碼添加了一個變量。它的名稱必須完全匹配屬性名稱,因此它的名稱將是_Tint。然後我們可以在我們的片段程序中簡單地返回這個變量。
1
2
3
4
5
6
7
8
9
10
11
12
13
#include "UnityCG.cginc"
 
float4 _Tint;
 
float4 MyVertexProgram (float4 position : POSITION) : SV_POSITION {
    return mul(UNITY_MATRIX_MVP, position);
}
 
float4 MyFragmentProgram (
    float4 position : SV_POSITION
) : SV_TARGET {
    return _Tint;
}
請注意,變量必須在使用之前進行定義。雖然你可以改變C#類中的字段和方法的順序,這在C#中沒有問題,但是對於着色器不是這樣的。着色器的編譯器是從上到下工作的。它不會往後看一下。

編譯號的片段程序現在包括tint變量。
1
2
3
4
5
6
7
8
9
10
11
12
13
uniform     vec4 _Time;
uniform     vec4 _SinTime;
uniform     vec4 _CosTime;
uniform     vec4 unity_DeltaTime;
uniform     vec3 _WorldSpaceCameraPos;
uniform     vec4 _Tint;
layout(location = 0) out vec4 SV_TARGET0;
void main()
{
    SV_TARGET0 = _Tint;
    return;
}
1
2
3
4
5
6
7
8
ConstBuffer "$Globals" 112
Vector 96 [_Tint]
BindCB  "$Globals" 0
      ps_4_0
      dcl_constantbuffer cb0[7], immediateIndexed
      dcl_output o0.xyzw
   0: mov o0.xyzw, cb0[6].xyzw
   1: ret

Unity 渲染教程(二):着色器基礎
顏色爲綠色的球。




3.3 從頂點程序傳遞到片段程序

到目前爲止,我們給了所有的像素相同的顏色,但是這是相當受限的情況。通常情況下,頂點數據起着很大的作用。舉個例子來說,我們可以將位置解釋爲顏色。然而,變換後的位置不是非常有用的。因此,讓我們使用網格中的局部位置信息作爲顏色。我們該如何將額外的數據從頂點程序傳遞給片段程序?

GPU通過光柵化三角形來創建圖像。它需要三個經過處理的頂點並在它們之間進行插值。對於由三角形覆蓋的每個像素,它會調用片段程序,並傳遞內插值後的數據。

Unity 渲染教程(二):着色器基礎
對頂點數據進行插值。

因此,頂點程序的輸出並不是直接用作片段程序的輸入。插值過程位於兩者之間。 在這個圖裏面SV_POSITION數據被進行內插值,但是其他數據也可以進行內插值。

要訪問插值後的局部位置信息,請向片段程序中添加參數。 因爲我們只需要X、Y和Z組件,我們用float3就足夠了。然後我們可以輸出位置信息,就像它是一種顏色一樣。我們必須提供第四個顏色分量,可以簡單的只保留爲1。
1
2
3
4
5
6
float4 MyFragmentProgram (
    float4 position : SV_POSITION,
    float3 localPosition
) : SV_TARGET {
    return float4(localPosition, 1);
}
 再次提醒下,我們必須使用語義來告訴編譯器該如何解釋這些數據。這一次我們將使用TEXCOORD0。
1
2
3
4
5
6
float4 MyFragmentProgram (
    float4 position : SV_POSITION,
    float3 localPosition : TEXCOORD0
) : SV_TARGET {
    return float4(localPosition, 1);
}
 編譯好的片段着色器現在將使用內插值後的數據而不是使用統一的顏色。
1
2
3
4
5
6
7
8
in  vec3 vs_TEXCOORD0;
layout(location = 0) out vec4 SV_TARGET0;
void main()
{
    SV_TARGET0.xyz = vs_TEXCOORD0.xyz;
    SV_TARGET0.w = 1.0;
    return;
}
1
2
3
4
5
6
  ps_4_0
   dcl_input_ps linear v0.xyz
   dcl_output o0.xyzw
0: mov o0.xyz, v0.xyzx
1: mov o0.w, l(1.000000)
2: ret
 當然頂點程序必須輸出局部位置信息才能正常工作。我們可以通過向它添加一個輸出參數,使用相同的TEXCOORD0語義來做到這一點。 頂點和片段函數的參數名稱不需要匹配。這是通過語義進行匹配的。
1
2
3
4
5
6
float4 MyVertexProgram (
    float4 position : POSITION,
    out float3 localPosition : TEXCOORD0
) : SV_POSITION {
    return mul(UNITY_MATRIX_MVP, position);
}
 要通過頂點程序傳遞數據,從位置數據localPosition裏面複製X、Y和Z分量。
1
2
3
4
5
6
7
float4 MyVertexProgram (
    float4 position : POSITION,
    out float3 localPosition : TEXCOORD0
) : SV_POSITION {
    localPosition = position.xyz;
    return mul(UNITY_MATRIX_MVP, position);
}
 額外的頂點程序輸出包含在編譯器着色器中,這樣我們將看到我們的球體被正確的渲染。
1
2
3
4
5
6
7
8
9
10
11
12
in  vec4 in_POSITION0;
out vec3 vs_TEXCOORD0;
vec4 t0;
void main()
{
    t0 = in_POSITION0.yyyy * glstate_matrix_mvp[1];
    t0 = glstate_matrix_mvp[0] * in_POSITION0.xxxx + t0;
    t0 = glstate_matrix_mvp[2] * in_POSITION0.zzzz + t0;
    gl_Position = glstate_matrix_mvp[3] * in_POSITION0.wwww + t0;
    vs_TEXCOORD0.xyz = in_POSITION0.xyz;
    return;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Bind "vertex" Vertex
ConstBuffer "UnityPerDraw" 352
Matrix 0 [glstate_matrix_mvp]
BindCB  "UnityPerDraw" 0
      vs_4_0
      dcl_constantbuffer cb0[4], immediateIndexed
      dcl_input v0.xyzw
      dcl_output_siv o0.xyzw, position
      dcl_output o1.xyz
      dcl_temps 1
   0: mul r0.xyzw, v0.yyyy, cb0[1].xyzw
   1: mad r0.xyzw, cb0[0].xyzw, v0.xxxx, r0.xyzw
   2: mad r0.xyzw, cb0[2].xyzw, v0.zzzz, r0.xyzw
   3: mad o0.xyzw, cb0[3].xyzw, v0.wwww, r0.xyzw
   4: mov o1.xyz, v0.xyzx
   5: ret

Unity 渲染教程(二):着色器基礎
將局部位置信息解釋爲顏色。




3.4 使用結構

你是否認爲我們的程序的參數列表看起來很亂?其實它只會變得更糟,因爲我們要在頂點程序和片段程序之間傳遞越來越多的數據。因爲頂點程序的輸出應該匹配片段程序的輸入,所以如果我們可以在一個地方定義參數列表將是非常方便的。 幸運的是,我們可以這樣做。

我們可以定義數據結構,它們只是變量的集合。除了語法有點不同以外,它們類似於C#中的結構。這裏是一個結構體,定義了我們正在內插值的數據。請注意要在定義後使用分號。
1
2
3
4
struct Interpolators {
    float4 position : SV_POSITION;
    float3 localPosition : TEXCOORD0;
};
 使用這個結構來讓我們的代碼更整潔。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
float4 _Tint;
 
struct Interpolators {
    float4 position : SV_POSITION;
    float3 localPosition : TEXCOORD0;
};
 
Interpolators MyVertexProgram (float4 position : POSITION) {
    Interpolators i;
    i.localPosition = position.xyz;
    i.position = mul(UNITY_MATRIX_MVP, position);
    return i;
}
 
float4 MyFragmentProgram (Interpolators i) : SV_TARGET {
    return float4(i.localPosition, 1);
}




3.5 調整顏色
因爲顏色值爲負的情況被修正爲零,我們的球體在渲染的時候看起來相當的暗。 因爲默認球體在物體空間中的半徑爲1/2,所以顏色通道的值最終位於-1/2和1/2之間。 我們想將它們移動到0-1範圍,我們可以通過給所有通道的值加上1/2來改變這一點。
相關文章
相關標籤/搜索