天天看點

Unity3D Shader之路 寫Shader前必須要知道的事情 渲染流水線的概括

版本:unity 5.4.1  語言:Unity Shader

總起:

    最近花了一個月的時間把《Unity Shader 入門精要》看完了,沒怎麼寫博文,因為寫得太好了,看得有點廢寝忘食了,再次強烈推薦。

 今天把寫Shader前必須要知道的渲染流水線給概括一下,然後簡單結合頂點\片元着色器Shader,說說各個代碼在流水線的位置,以及職責功能。

渲染流水線:

 在寫Unity腳本的時候,不管是C#也好還是js也好,都是在跟CPU打交道,做算術運算、調用類成員、控制程式流程。而寫Shader不同,他是跟GPU打交道。

 我們首先來看看腳本的一個調用流程:(百度上有中文翻譯,我這邊直接上官方網站了)https://docs.unity3d.com/Manual/ExecutionOrder.html

 從這裡看出遊戲中的一幀首先是初始化,也就是Awake、OnEnable、Start這些函數的執行,接着實體、輸入、遊戲邏輯的執行,在Unity中寫代碼的一天到晚都是跟這些函數打交道吧?接下來我們看到了Scene Rendering,這裡就是一幀最重要的渲染部分了:由CPU發出指令(也就是平常所說的drawcall),GPU響應指令進行渲染,執行的就是一個渲染流水線。

 而Shader在其中對一個渲染流水線進行控制,告訴GPU,CPU傳過來的這個物體該如何渲染。粗淺的來說Shader之于GPU,就如同C#之于CPU。

 那麼渲染流水線是怎樣的一個流程呢?簡單的來說就是:“頂曲幾裁屏,三三片逐屏”。

    這是一種簡單的記憶方法,感覺要還蠻好用的。

 第一行所說的是幾何階段:頂點着色器 -> 曲面細分着色器 -> 幾何着色器 -> 裁剪 -> 螢幕映射。主要的工作就是将模型的各個頂點變換到螢幕坐标的一個空間中,為真正的渲染做準備。

 第二行說的是光栅化階段:三角形設定 -> 三角形周遊 -> 片元着色器 -> 逐片操作 -> 螢幕圖像。這一個階段就是真正的在螢幕顯示像素。

一個簡單的Shader:

    這邊就通過書上的例子簡單的說說流水線各個部分的作用。

Shader "Unity Shaders Book/Chapter 5/Simple Shader" 
{
	// 顯示在Unity Inspector上的屬性,給使用者提供控制
	// 形式:屬性名("Inspector顯示的标簽", 屬性類型) = 初始化資料
	Properties 
	{
		// 這是一個Color,初始化為(1,1,1,1),在Inspector上顯示Color Tint,而在Shader中使用_Color進行調用
		_Color("Color Tint", Color) = (1, 1, 1, 1)	
	}

	// Shader的控制代碼都在Pass中,一個SubShader可能包含多個Pass
	// 根據控制代碼的不同可能會調用所有的Pass,也可能隻調用一個
	// 現在我們隻有一個,進行渲染就會調用這個Pass,暫時不用多管其他的情況
	SubShader 
	{
		// 标簽,SubShader下的标簽對所有Pass都有效,而Pass中的隻對本身有效
		// Queue:說明渲染所在的隊列,Geometry表明大多數不透明物體是在該隊列中渲染的
		// RenderType Opaque:表明這個物體是不透明的,與隊列的差別是,Queue隻是渲染順序,即使設定成其他的也沒問題,
		//		              但你需要這個物體是不透明的RenderType就必須設定為Opaque
		Tags { "Queue" = "Geometry" "RenderType" = "Opaque" }
        Pass 
        {
        	// LightMode:光照模式
        	Tags { "LightMode" = "ForwardBase" }

        	// 設定,控制Shader的一些預設處理,深度測試和深度寫入總是開啟
        	ZTest On ZWrite On

            // CGPROGRAM和ENDCG成對出現,兩行内部的代碼就是用cg語言(c語言的變種)寫的控制代碼
            CGPROGRAM

            // vertex:表明控制頂點着色器的函數是vert
            // fragment:表明控制片元着色器的函數是frag
            #pragma vertex vert
            #pragma fragment frag
            
            // 申明Properties中的屬性,以便在vert和frag函數中調用
            uniform fixed4 _Color;

            // Unity給vert函數提供的輸入
			struct a2v 
			{
				// vertex必須,POSITION表明他是一個模型的頂點坐标
                float4 vertex : POSITION;
                // normal,NORMAL表明需要的是法線
				float3 normal : NORMAL;
				// texcoord,TEXCOORD0表明需要的是目前的模型的uv坐标
				float4 texcoord : TEXCOORD0;
            };
            
            // vert函數的輸出,frag函數的輸入
            // SV_開頭的兩個輸出是必須的,而且必須這麼寫,這是流水線過程中必須擷取的資料
            struct v2f 
            {
            	// pos必須,SV_POSITION表明需要輸出一個螢幕坐标
                float4 pos : SV_POSITION;
                // color,顔色,其他屬性,在frag函數中使用,不過一般冒号後面的辨別為TEXCOORD0、TEXCOORD1、TEXCOORD2...
                //        隻要不重複就行了,其他屬性标明什麼無所謂
                fixed3 color : COLOR0;
            };
            
            // 頂點着色器控制的函數
            v2f vert(a2v v) 
            {
            	// 先初始化輸出結構
            	v2f o;

            	// UNITY_MATRIX_M:從模型空間到世界空間
            	// UNITY_MATRIX_V:從世界空間到相機的觀察空間
            	// UNITY_MATRIX_P:從觀察空間到螢幕空間
            	// o.pos:需要輸出一個螢幕空間的坐标,是以很簡單,UNITY_MATRIX_MVP是模型空間到螢幕空間的矩陣,然後乘以v.vertex就能得到
            	o.pos = mul(UNITY_MATRIX_MVP, v.vertex);

            	// 根據法線計算顔色
            	o.color = v.normal * 0.5 + fixed3(0.5, 0.5, 0.5);

            	// 輸出
                return o;
            }

            // 片元着色器使用的控制函數,必須寫上SV_Target,表明該輸出是一個螢幕上的顔色
            fixed4 frag(v2f i) : SV_Target 
            {
            	// 擷取vert處理後的顔色,再與_Color.rgb相乘
            	fixed3 c = i.color;
            	c *= _Color.rgb;

            	// 最後輸出
                return fixed4(c, 1.0);
            }

            ENDCG
        }
    }

    // 申明備用的Shader,如果以上Pass無法運作的話
    Fallback "Diffuse"
}
           

 最重要的是vertex和fragment所指定的兩個函數vert和frag。它們所在的流水線位置分别是頂點着色器和片元着色器。

 頂點着色器,對應vert函數,正如上文所說SV_POSITION變量是必須的,頂點着色器最重要的職責就是根據一個模型坐标輸出螢幕坐标。

    曲面細分着色器和幾何着色器應該也是可以編寫的,不過學這本書的時候沒有用到,是以暫時忽略。

 裁剪,vert把模型坐标變換到了螢幕坐标,這樣就可以确定每個頂點是否在螢幕中,裁剪就是将不在螢幕中的頂點給裁剪掉。

 螢幕映射,把每個圖元的坐标轉換到螢幕坐标中,經過MVP矩陣後,坐标是在(-1,-1)到(1,1)中的,而螢幕映射就是把它映射到螢幕分辨率中。比如分辨率為1920*1080,則需要把(-1,-1)至(1,1)映射到(0,0)至(1920,1080)。

    三角形設定、三角形周遊。頂點組成三角形片,三角形片才組成網格,組成整個模型。這個兩個階段就是将頂點所組成三角形所覆寫的像素計算出來。

 片元着色器,對應frag函數,對上兩個階段中傳來像素進行處理,最終需要輸出一個SV_Target所代表的最終顔色。

 逐片操作,做深度測試、模闆測試等,通過的像素才能顯示在螢幕上,可以配置。對應上面的代碼ZTest On開啟深度測試。

    螢幕圖像,最終顯示的結果。

總結:

 頂曲幾裁屏,三三片逐屏。記住這個的同時,需要知道vert函數做的是頂點變換對應頂點着色器這個階段,而frag函數是對每個像素顔色進行處理,對應的是片元着色器這個階段。

    而裁剪和逐片操作這兩個階段雖然無法編寫代碼,但可以進行控制。

 今天用自己的語言簡單概括了一下渲染流水線,不知道大家能否通過這篇文章對渲染流水線有個簡單的認識。寫Shader,不知道流水線寫的時候就根本不知道為什麼要這樣去寫,是以掌握這個流水線是非常必要的。

 以後會給大家帶來更多的Shader了解和一些有趣的Shader使用。