Esfog_UnityShader教程_漫反射DiffuseReflection
這篇是系列教程的第三篇,最近工作比較緊,所以這個周六周日就自覺去加了剛回來就打開電腦補上這篇,這個系列的教程我會盡量至少保證一周寫一篇的.如果大家看過我的上一篇教程《Esfog_UnityShader教程_UnityShader語法實例淺析》的話,相信已經(jīng)對UnityShader有了一些了解了,我們從這篇開始就不會再專門糾纏語法了,一般都會在用到的時候特殊說明一下.如果你還對UnityShader的基礎(chǔ)語法比較陌生,那么推薦看一下本系列的前兩篇文章地址是http://www.cnblogs.com/Esfog/p/3562022.html.
?
漫反射DiffuseReflection
?
?光照是圖形渲染里的一個重要課題,處理的好壞直接影響了游戲展示給玩家的場景效果,做的越好越真實,給玩家代入感也就越強烈,一般的光照模型中包括4種:環(huán)境光,自發(fā)光,漫反射,高光。而光照處理里面兩個最常見的課題也就是漫反射和鏡面反射(高光),這一篇我們討論漫反射,關(guān)于鏡面反射的相關(guān)內(nèi)容將在下一篇中講解.提到漫反射大家一定不會陌生,因為我們在小學(xué)或是初中就一定知道了什么是漫反射和鏡面反射了.不過為了下面講解的讓大家更容易理解,我還是簡單的用自己的語言描述一下漫反射的概念.
?
如上圖(圖片來自網(wǎng)絡(luò)),當(dāng)光照射到物體表面時,由于物體表面的凹凸不平,導(dǎo)致光向各個方向反射出去,在理想狀態(tài)下,我們認(rèn)為光向各個方向反射的量是相同的,不難理解,這樣就會無論我們從哪個角度觀察物體,物體上的某一點看上去都是一樣亮的.也就是說我們的眼睛從各個角度接收到來自這個點的光線量都是相同.在游戲里,攝像機也就相當(dāng)于我們的眼睛,那我們要做的也就是計算出光照在物體表面后,在每個像素上的反射強度,無論我們從什么角度看,都讓它始終保持這個值.原理就說到這里,如果你對我說的不理解,那也無妨,總之你知道漫反射就是無論你從哪個角度看,看到某個點的亮度都應(yīng)是一樣的就可以了.下面來我們來看具體的計算原理.
如上圖(圖片取自《Cg Programming in Unity》),要計算漫反射,我們需要知道兩個量一個是物體表面的法線N,另一個是光的入射方向L(注意,我們這里考慮的只有平行光DirectionalLight,對于點光源等其他光源的計算方式略有不同,讀者可自行搜索了解),
在寫具體代碼之前,我們先說明一下計算的原理,原理就是我們通過計算光的入射方向(這里所謂的入射其實是入射方向的反方向,之所以這樣是為了方便計算)和物體表面向量的夾角來決定這點的光照強度,兩個向量的夾角越小就說明越來越接近于關(guān)照直射,這時候當(dāng)然反射的光也就越強,如果夾角大于等于90度那么反射的光強度越來越弱,超過90度就完全看不到了.額外說一點:也許你會糾結(jié),前面明明說我們假設(shè)光在任何方向上的反射都是同量的,而且物體表面的凹凸不平也應(yīng)該是平均的,那么為什么直射的地方就一定比其它地方看上去量的,說實話這個問題一開始糾結(jié)了我很久,它超出了我對Shader的理解范圍,偏大到物理方面了,后來我個人認(rèn)為可能是由于夾角越大的時候光在向各個方向反射的時候表面內(nèi)部光來回傳遞所消耗的能量就越多,最后反射出去的也就越少了.這只是我的個人理解,如果哪位有更權(quán)威的解釋,請評論告訴我,如果你壓根沒有把這當(dāng)成一個我問題就不要去思考他了,我這個人比較愛鉆牛角尖,什么都愛刨根問底,也不知道是好是壞.
那么在通過夾角來計算光的亮度之前,我們需要對N和L兩個向量進行歸一化處理,如果大家學(xué)過線性代數(shù)或者高中數(shù)學(xué)沒忘干凈的話,那么一定對它不陌生,如果你實在記不起來就去上網(wǎng)搜搜吧.在CG里對向量進行歸一化的操作我們使用normalize函數(shù),這是CG數(shù)學(xué)庫提供給我們的.然后我們利用向量的點積公式N·L = |N|*|L|*cosθ(如果你對點積也不了解,我就不過分解釋了,上網(wǎng)搜一下).由于我們剛剛對N和L進行了歸一化,他們的模就是1了,那么N·L = cosθ了.也就是說我們直接對N和L進行點積運算會得到兩個向量的夾角余弦值,我們知道如果加角θ越接近月0°那么N·L也就越接近于1,越接近月90°,N·L就越接近于0.所以我們只要把光的顏色乘以這個點積結(jié)果就會達(dá)到我們想要的角度越小光越強,角度越大光越弱的效果了.下面給出《The Cg Tutorial》中給出的漫反射計算公式:
diffuse = Kd * lightColor * max(N·L,0)
下面我們就來通過實際代碼來講述一下具體的計算過程.
?
1 Shader "Esfog/Diffuse" 2 { 3 Properties 4 { 5 _MainTex ("Base (RGB)", 2D) = "white" {} 6 } 7 SubShader 8 { 9 Pass 10 { 11 Tags { "RenderType"="Opaque" "LightMode"="ForwardBase"} 12 CGPROGRAM 13 #pragma vertex vert 14 #pragma fragment frag 15 #include "UnityCG.cginc" 16 17 uniform sampler2D _MainTex; 18 uniform float4 _LightColor0; 19 struct VertexOutput 20 { 21 float4 pos:SV_POSITION; 22 float2 uv_MainTex:TEXCOORD0; 23 float3 normal:TEXCOORD1; 24 }; 25 26 VertexOutput vert(appdata_base input) 27 { 28 VertexOutput o; 29 o.pos = mul(UNITY_MATRIX_MVP,input.vertex); 30 o.uv_MainTex = input.texcoord.xy; 31 o.normal = normalize(mul(float4(input.normal,0),_World2Object)); 32 return o; 33 } 34 35 float4 frag(VertexOutput input):COLOR 36 { 37 float3 normalDir = normalize(input.normal); 38 float3 lightDir = normalize(_WorldSpaceLightPos0.xyz); 39 float3 Kd = tex2D(_MainTex,input.uv_MainTex).xyz; 40 float3 diffuseReflection = Kd * _LightColor0.rgb * max(0,dot(normalDir,lightDir)); 41 return float4(diffuseReflection,1); 42 } 43 ENDCG 44 } 45 } 46 FallBack "Diffuse" 47 }?
?
?
這里我假定大家已經(jīng)看過我的上一篇教程或者有一定的Shader語法基礎(chǔ),所以不會像上一篇一樣一行行解釋,只選擇與本篇相關(guān)的或者新出現(xiàn)的內(nèi)容進行說明.
第11行 大家會發(fā)現(xiàn)我們的Tags里面比上一次多了一個"LightMode"="ForwardBase",這句話的作用其實是和Unity處理場景中的所有光源的方案有關(guān)系的,一般默認(rèn)使用的處理方式為Forward(具體細(xì)節(jié)請參考官方文檔Unity's Rendering behind the scenes一節(jié)的內(nèi)容).如果處理方式為Forward那么當(dāng)我們在Tags里寫上這一句的時候,簡單的理解為,當(dāng)渲染這個物體的時候場景中的第一個平行光源的一些參數(shù)我們可以直接在這個Pass中使用.包括光的顏色_LightColor0,光的在世界空間的位置_WorldSpaceLightPos0等.如果你不適用這種方法的話你也可以通過在外部寫一個腳本將光的一些參數(shù)和任何你想傳送的變量傳送到Shader中,具體方式我們在日后遇到相關(guān)問題的時候具體說.
第18行?float4 _LightColor0;我們定義了一個新的_LightColor0來接收存儲光照的顏色,我們并沒有在Properties中定義也沒有通過外部腳本來傳值,那它是做什么用的呢?其實由于我們上邊在Tags中添加的新標(biāo)簽,所以_LightColor0會被Unity自動賦值為場景中第一個平行光的顏色(這也不一定,其實和光源的RenderMode屬性有關(guān),但如果你不做修改的話,那就是用第一個光源).
第23行?float3 normal:TEXCOORD1;我們在頂點著色器的返回結(jié)構(gòu)中多添加了一個變量,看名知意我們要把頂點的發(fā)線經(jīng)過插值后傳到片段著色器中,我們之前說過TEXCOORD0~TEXCOORDX(具體視顯卡能力而定),可以用來保存任何我們需要插值的內(nèi)容,由于我們想在片段著色器中來計算物體表面的漫反射所以就需要將法線傳過去.
那么有一個很值得思考的問題:"為什么要在片段著色器中計算漫反射呢?",這是個很好的問題,在到底在頂點著色器還是片段著色器中來進行光照的處理并無一個明確的規(guī)定,這是一個對于性能和效果的權(quán)衡,如果在頂點著色器中計算的話很顯然我們的計算次數(shù)和頂點的數(shù)量一致,也就是很少,但是效果就不好,一般來說一個像素所在的三角片上的三個頂點對光源的捕捉有可能不足,就會導(dǎo)致最后用三個頂點計算出來的漫反射顏色來插值出的面片顏色就會不準(zhǔn)確,也就會出現(xiàn)本該很亮的地方卻不亮.所以說在頂點著色器中進行光照計算的性能會很高(因為計算次數(shù)遠(yuǎn)遠(yuǎn)小于片段著色器),但是效果差,而在片段著色器中計算則正相反,性能會很低,但是效果會很好(因為每個像素都是單獨通過法線來計算的,而不是直接用頂點插值出來的).所以具體情況具體分析.
第31行o.normal = normalize(mul(float4(input.normal,0),_World2Object));這一句中我們把模型本身自帶的頂點法線屬性,先進行空間變換將其變換到世界空間中去,為什么要換到世界空間中呢,其實只要保證要進行操作的兩個向量在同一個空間,那么具體是哪個空間并不重要,不過由于Unity為我們提供的大量參數(shù)都是在世界空間中的,所以我們就變換到世界空間中去吧,這里還有一點很重要,向量的空間變換與點不同,它要右乘上目標(biāo)變換矩陣的逆的轉(zhuǎn)置,具體的數(shù)學(xué)原因有些復(fù)雜,大家可以自己查一下,那么原來的目標(biāo)矩陣式模型空間到世界空間unity中為我們提供了這個矩陣_Object2World,不過我們要的不是它,我們先來求他的逆,unity也為我們提供了_World2Object,其實嚴(yán)格上來講這并不是_Object2World的逆,我們呢要將這個矩陣*unity_Scale.w之后再將矩陣的最右下角的數(shù)置為1.這其中的問題我只是在國外的論壇上看到過一些解釋,我也無法很準(zhǔn)確的表達(dá)出來,等以后我弄明白了再告訴大家,如果有人知道也請您一定告訴我,不過為什么這里我們沒有進行剛才的幾步操作呢,因為這兩步操作都是針對縮放的,由于我們馬上要對他進行歸一化所以對我們只要他的方向正確就好,大小無所謂。這樣我們得到了它的逆,我們要繼續(xù)求它的轉(zhuǎn)置,不過這個就不需要了,之前我們都是將矩陣放在mul函數(shù)的左邊,而向量或點放在右側(cè),我們調(diào)換一下他們的位置就相當(dāng)于乘上了轉(zhuǎn)置(不明白就看看線性代數(shù)吧),由于mul要求點或者向量必須表示成4維的,所以我們將他轉(zhuǎn)換成float4并將最后一位填0(原因后面有解釋).上述結(jié)束以后我們進行了歸一化,傳給了頂點輸出結(jié)構(gòu)中相應(yīng)變量,這樣做只為了保證在插值的時候不同頂點之間的法線是在同一個標(biāo)準(zhǔn)下進行的.因為法線只起到方向的作用,如果有的頂點的法線長度太長,而有的很短,那么各個分量在進行差值的時候就會出現(xiàn)錯誤的結(jié)果,當(dāng)然一般美術(shù)同學(xué)做出來的模型自帶的法線屬性都是單位向量,所以這里只是為了以防萬一.
第37行?float3 normalDir = normalize(input.normal); 我們定義個了一個float3變量來保存插值后在當(dāng)前片段上的法線,由于經(jīng)過插值會使我們原本的單位向量不再是單位向量(如果你想知道具體原因,就想一下插值過程知識滿足了各個分量的插值結(jié)果,但并不能保證整體上還是一個單位向量),所以我們需要再次進行歸一化.
第38行float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);我們又定義了一個float3變量來保存入射光的方向(反方向),這里有一些需要說明的,Unity中的平行光源的位置是沒有意義的,只有它的方向代表了光線的方向.而在3D數(shù)學(xué)中,點和向量的概念有時候是很模糊的.為了區(qū)分它們,在齊次空間中我們用一個4維的行(列)向量來表示它們,xyz分量都相同,只有w分量不同,點的w分量為1,向量的w分量為0,如果想知道具體原因的話,就去看看線性代數(shù)吧,這個不太好解釋.而我們的平行光源其實他可以看做一個向量,他具體在哪沒有意義,我們要的只是一個方向,_WorldSpaceLightPos0是Unity為我們賦值的一個表示場景中第一個平行光的位置(這里不一定是第一個, 解釋通上面的光照顏色).由于位置無關(guān),那么我們就直接取出他的前三個分量,把它看做一個由世界空間原點到光源位置的一個向量,然后我們再對他進行歸一化最終就得到我們想要的結(jié)果了,也許這里我也解釋的不是很清楚,我自己理解這里的時候也不是一下子就明白的.
第39行float3 Kd = tex2D(_MainTex,input.uv_MainTex).xyz;這個Kd就是我們在上面一開始提到的那個書中給出的計算漫反射的公式,其中Kd表示材質(zhì)的漫反射顏色,這個說法很有迷惑感,我個人是這樣理解的,根據(jù)物理上的說法,物體本身如果沒有光照射,它是呈現(xiàn)不出任何的顏色的,而最終表現(xiàn)給眼睛的顏色實際上是光源找到物體表面沒有被物體表面吸收而反射回來的顏色,那么我們這個Kd不如理解成將光吸收后反射出去的顏色成分(其中3個分量分別對應(yīng)對RGB三種顏色值的反射量).如果你不糾結(jié)于此就當(dāng)我沒說.總之我們這里講Kd直接賦成我們的紋理顏色就可以了,你就理解成物體本身就這個顏色也成.
第40行就是利用了我們一開始的公式將光照顏色計算出來,有三個地方需要說明,其中_LightColor是光源的顏色,這個在上面解釋過了,至于為什么要和Kd相乘,我個人覺得這是和色彩處理相關(guān)的,因為相乘的情況下顏色的結(jié)合看起來是最自然的和真實世界中最相近.第二個地方就是dot(normalDir,lightDir),這個dot是Cg提供給我們來計算兩個光源的點積的,為什么這么做一開始我們說過了,最后一點就是max函數(shù),之所以將點積計算出來之后還要和0去取一個最大值,主要是為了避免當(dāng)normal和lightdir的夾角大于90度的時候點積計算出現(xiàn)負(fù)值,會導(dǎo)致整個漫反射顏色計算出來是個負(fù)值進而導(dǎo)致整體的光照計算出現(xiàn)錯誤,當(dāng)大于90度的時候漫反射顏色已經(jīng)失去意義,所以需要用max函數(shù)保證不會出現(xiàn)負(fù)值.
(~ o ~)~好了系列教程的第三篇到此結(jié)束了,和前兩篇基礎(chǔ)的不同,這一次可能涉及到一些數(shù)學(xué)概念和Unity的東西所以我描述起來和大家理解起來都比較費勁,不過沒什么東西是一蹴而就的,我給大家寫一篇文章的背后,我自己都不知花了多久去弄清楚一個概念,去理解一個公式.總之希望大家不要因為一時的不理解而放棄學(xué)習(xí)和探索。你選擇學(xué)習(xí)Shader就說明你一定是對游戲開發(fā)又更高追求的人,成功從來都是不易的,那些唾手可得的東西并沒有什么值得讓人驕傲和羨慕的.希望大家和我一道繼續(xù)向前向前!
下面是兩幅圖來展示一下效果:
上面是使用的我們上篇教程所講述的直接使用貼圖顏色的效果
上面這張是使用了我們上面寫的Shader.有了明暗效果是不是看上去更真實了一些了,當(dāng)然你可能覺得效果并沒有什么特別好,因為漫反射要和其他光照處理一起使用才更加完美.我們這里只用了漫反射,連環(huán)境光都沒有使用,所以并不是特別理想.
add:(2015/4/8)
添加一個關(guān)于法線為什么乘以逆的轉(zhuǎn)置的一個簡單推導(dǎo)
尊重他人智慧成果,歡迎轉(zhuǎn)載,請注明作者esfog,原文地址http://www.cnblogs.com/Esfog/p/3577412.html.
轉(zhuǎn)載于:https://www.cnblogs.com/Esfog/p/3577412.html
總結(jié)
以上是生活随笔為你收集整理的Esfog_UnityShader教程_漫反射DiffuseReflection的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: DPKG命令与软件安装、APT
- 下一篇: 1.网页学习-开始学习第一步: