1. 程式人生 > >[Unity基礎]從“漫反射光照模型”瞭解Unity Shader渲染原理

[Unity基礎]從“漫反射光照模型”瞭解Unity Shader渲染原理

最近在看《UnityShader入門精要》來進行Shader的入門學習。看完第六章的”Unity中的基礎光照“後,對前面所講的頂點著色器和片元著色器有了更透徹的理解,於是做了個小實驗,一方面驗證自己對著色器渲染原理的理解是否正確,另一方面想親眼看看渲染的整個流程,加深記憶。

為了計算方便,實驗場景儘量簡單化

1.場景模型只要一個平行光,一個帶漫反射Shader材質的膠囊和一個用於參照地平線的terrain;
2.平行光設定為沿x軸旋轉正45度。這樣一來,其向量可以表示為(0,-1,1),方便計算;
3.平行光強度設定為1,顏色為純白,即(1,1,1,1),減少干擾;
4.環境光強度設定為0.2,可以明顯看到效果。另外環境光強度在這裡的範圍是0~8,其實實際範圍是0~3.445,由於這個問題導致先前計算一直出錯,我也是醉了;
5.在膠囊上掛個指令碼,只聲明瞭”public color a;“,為了方面使用Unity自帶取色器。

實驗Shader程式碼:

shader "Caddress Unity Shader/Diffuse Vertex-Level"
{
    Properties{
        _Diffuse("Diffuse",color) = (1,1,1,1)
    }

    SubShader{
        Pass{
            Tags{"LightMode" = "ForwardBase"}

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
#include "Lighting.cginc" fixed4 _Diffuse; struct a2v { float4 vertex : POSITION; float3 normal : NORMAL; }; struct v2f { float4 pos : SV_POSITION; fixed3 color : COLOR; }; v2f vert(a2v v){ v2f o; o.pos = mul(UNITY_MATRIX_MVP, v.vertex); fixed3 ambient
= UNITY_LIGHTMODEL_AMBIENT.xyz; fixed3 worldNormal = normalize(mul(v.normal,(float3x3)_World2Object)); fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz); fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal,worldLight)); o.color = ambient + diffuse; return o; } fixed frag(v2f i) : SV_Target{ return fixed4(i.color,1); } ENDCG } } Fallback "Diffuse" }

程式碼來自書的第6章第四節,逐頂點的漫反射光照模型

首先這裡先介紹下頂點著色器和片元著色器,我們在螢幕上所看到的3D效果,其實只是各個畫素的顏色協調出來的障眼法。程式設計師用程式碼在背後構建了一個數據流的三維世界,但要將這個世界展示在螢幕上,就需要利用裡面的資料關聯螢幕的畫素顏色,讓我們看到的效果和現實世界的一樣。
頂點著色器
雖然叫著色器,但它和著色還沒有太大的關係,因為你總不能對一個沒有面積的點著色吧。頂點著色器只是讓模型上的各個頂點攜帶相關的資訊,在上面的shader裡,我們讓它攜帶頂點的位置座標和頂點顏色兩個資訊。至於一個點怎麼攜帶資訊,你可以想象成其背後有個頂點類什麼的。

片元著色器
片元著色器才是真正進行著色的地方,其返回值o.color就是該畫素上的最終顏色。片元可以理解成畫素,但畫素只含有顏色資訊,而片元還攜帶著深度,法線等資訊。

由上面的程式碼可看出,Shader利用頂點的法線資料算好顏色後,就直接傳給片元著色器去畫,這種渲染方式比較簡單,易於在Unity上做實驗還原。如果是逐片元光照,那就是利用每個片元攜帶的法線來計算顏色了,計算量太大。

fixed3 worldNormal = normalize(mul(v.normal,(float3x3)_World2Object));
fixed3 worldLight = normalize(_WorldSpaceLightPos0.xyz);

這兩行得出了世界座標下的法線和光源的向量,並做了歸一化處理。我們在Scene下看到的就是世界空間。

dot(worldNormal,worldLight)

這裡對兩個資料進行求點積。點積的求法有兩種,一種是座標法,另一種是角度法。後面兩種方法都會用到,那個方便來哪個。

diffuse = _LightColor0.rgb * _Diffuse.rgb * saturate(dot(worldNormal,worldLight));

這裡求出了漫反射的顏色值,其中光源顏色和漫反射顏色我們都設成了白色(1,1,1),所以_LightColor0和_Diffuse各引數都是1,實際結果就是點積的結果,由於兩個向量都是歸一化,因此其結果就是兩個向量夾角的cos值(0~1)。

o.color = ambient + diffuse;

得出的漫反射資料和環境光資料進行求和,由於我們把環境光強度設定為0.2,因此環境光各引數為1/3.445,即(0.058,0.058,0.058)。

為了方面計算,我們選用膠囊最頂的一個三角面來還原著色規律。如下圖:

毋庸置疑,藍色範圍的左下角頂點向量方向為y軸,這裡用夾角計算比較方便,其向量與光源方向的夾角是45°,餘弦值為0.707。
因此o.color = (0.058,0.058,0.058)+ (0.707,0.707,0.707),
最後的結果再乘以255得到的rgb是:(195,195,195),
由於物體表面顏色為(255,0,0)純紅色,
因此在藍色範圍左下角頂點附近的畫素顏色應為(195,0,0)


由側檢視可看出,半球體從y軸到x軸的弧線過渡一共分為8塊,因此藍色範圍內的右頂點和上頂點的方向向量分別是y軸沿x方向旋轉11.5°和y軸沿z方向旋轉11.5度。(圖的原地畫錯了,應該往上偏移一點,但結果沒錯)

上頂點方向往z軸偏移,使用角度計算比較方便,該向量與光源方向的夾角是(90 - 11.25)+ 45度,cos值為0.555。加上環境光的資料,其上頂點附近的rgb是(156,0,0)。

最難計算是右頂點了,由於空間幾何學得不怎麼好,找不到公式計算夾角,只能用座標法計算了,我們假設網格一格單位為1,那上頂點的向量座標就是(0,1/tan(11.5),1)。即(1,4.91,0)歸一化後為(0.2,0.982,0)。另外,光源向量座標也可以用這個網格單元的座標系,只要方向對了就沒問題,因此光源向量(0,-1,1)歸一化為(0,-0.707,0.707)。

點積得到的結果是0.694,加上環境光後得到的(0.752,0,0),乘以255得到rgb(191,0,0)。

以上的點積結果應該都為負數,但在Shader背後會自動改為正數,這是因為只有當光源方向和法線方向相對的時候(即點積為負),才應該有光照,當兩個向量點積只要大於等於0,無論數值多少,o.color都不會加上Diffuse的值。我們把光源強度調到最大

可以看出,當法線與光源方向垂直之後,或者方向一致後,顏色就只剩下環境光的效果了(0.058 * 255 = 14.79)。

說到光源強度,其範圍是0~8沒錯。它的改變也會對物體光照效果有影響,其實就是_LightColor0的係數,當初設定為1也是為了方面計算。例如,之前藍色範圍的左下角頂點附近畫素rgb為((0.058 + 0.707)* 255 = 195,0,0),如果將光照強度取1.3,則該畫素rgb為((0.058 + 0.707 * 1.3)* 255,0,0)。即(249.16,0,0)

前面只驗證了頂點附近的點,這是因為頂點被網格線遮擋了,而附近的點受另外兩點顏色值的影響較小。為什麼這麼說,因為逐頂點光照的渲染是按照三角形中線定理來著色的,如果沒有另外兩個頂點的著色,那麼這個三角形的顏色其實是該頂點沿著中線由100%~0%變化的。例如計算重心附近的顏色:

重心為各頂點中線交點處,重心到各個頂點距離為各個中線的2/3,因此重心附近的rgb應為頂點rgb的33%(1/3(195+156+191),0,0)。即(180.6,0,0)。存在一定誤差。

結論

由此可以看出,所謂渲染只是對某個畫素按照Shader寫的規則附上顏色而已。而光照是對模型原有的顏色進行線性加深,以此達到背光效果。至於紋理的渲染規則就不是簡單的線性加深了,但渲染的原理不變:如果是逐頂點渲染,則按照三角形中線定理(紋理應該就不會使用這種方法了吧),如果是逐片元渲染就利用該片元上的資訊計算出最終要在畫素上顯示的顏色。