0x00 前言
hi,大家好。不知不覺時間就推進到了2018年,所以首先祝大家元旦快樂。由於前一段時間工作的內容發生了一些變化,因此有一段時間沒有更新博客了。不過時間來到了元旦,還是得寫點東西的。那么本文就來聊一聊如何利用環境映射來實現一條水晶龍吧,內容主要包括如何實現反射、折射以及菲涅耳效果。
0x01 反射
首先,我仍然使用來自斯坦福的上鏡率超高的模型,這次是龍的模型。
將它導入到Unity的工程中。
同時,我們還需要一個CubeMap,用來提供環境信息。可以看到,使用了模型導入后Unity的默認material的龍,在藍天白雲的映襯下… 就像一條石膏龍。
好了,基本准備工作已經完成了。那么作為水晶龍打造計划的第一步,我們首先來實現水晶龍對環境的反射,讓龍模型根據反射的結果去這個CubeMap上采樣。
可能有些朋友會好奇,水晶龍的反射怎么才能和CubeMap建立起聯系呢?其實很簡單,我畫一張示意圖各位就明白了。
其實這個過程很簡單,就是求的反射方向然后去cubemap上采樣作為水晶龍身上該點的顏色即可。
當然,由於折射和反射是一個常用的需求,因此hlsl和glsl都有內置的a方法用來計算反射方向和折射方向。但是下文我還是會給出相關的推導過程。
接下來,我們需要實現對反射方向的計算。同樣,我也來畫一張示意圖來推導反射的過程,大家就應該能明白了。
其中向量I代表入射光線,從眼睛到物體表面。向量N代表表面法向量,向量R則代表反射光線。
通過上面的推導,我們就可以在shader內實現自己的反射函數了。
//計算反射方向 float3 CaculateReflectDir(float3 I, float3 N) { float3 R = I - 2.f * N * dot(I, N); return R; }
一旦獲取了反射方向之后,就可以在fragment shader內對CubeMap采樣了。
fixed4 frag(v2f input) : SV_Target { float3 reflectedDir = CaculateReflectDir(input.viewDir, input.normalDir); fixed4 reflectCol = texCUBE(_Cube, reflectedDir); return reflectCol; }
結果我們的龍就從石膏龍變成了能夠反射周圍環境顏色的龍了。
0x02 折射
實現了反射的效果之后,我們還可以利用相似的思路來實現一些類似的效果,例如光的折射效果。
所謂的光的折射指的是當光通過不同密度的兩種材質之間的介面時——比如空氣和水——光的方向會發生改變。
當光波從一種介質傳播到另一種具有不同折射率的介質時,會發生折射現象,其入射角與折射角之間的關系,可以用斯涅爾定律(Snell’s Law)來描述。
下面我就利用斯涅爾定律來計算折射方向,進而正確的在CubeMap上為龍的折射效果進行采樣。同樣,我也會畫一張示意圖,並將推導過程寫在上面。
按照上文的公式推導,我們可以很簡單的翻譯成對應的shader方法。
//計算折射方向 float3 CaculateRefractDir(float3 I, float3 N, float ratio) { float cosTheta = dot(-I, N); float cosTheta2 = sqrt(1.f - pow(ratio,2) * (1 - pow(cosTheta,2))); float3 T = ratio * (I + N * cosTheta) - N * cosTheta2; return T; }
設n1/n2的值為0.9,對龍進行一次折射(因為光從空氣進入模型會發生一次折射,從模型進入空氣仍然會有一次折射,為了簡單我們只考慮一次折射的情況)的結果如下圖:
fixed4 frag(v2f input) : SV_Target { float3 reflectedDir = CaculateReflectDir(input.viewDir, input.normalDir); fixed4 reflectCol = texCUBE(_Cube, reflectedDir); float3 refractedDir = CaculateRefractDir(normalize(input.viewDir), input.normalDir, .2f); fixed4 refractCol = texCUBE(_Cube, refractedDir); }
0x03 菲涅爾效果
ok,在上文中我們分別討論了如何在shader內實現反射和折射效果。但是在現實生活中,反射和折射可能是同時發生的。
一個生活中簡單的小例子就是我們只有在幾乎垂直向下看的時候,才能看到池塘內水下的情況。而在一個較小的角度觀察池塘時,又會因為大部分會被反射而幾乎沒有什么折射,因而很難透過水的表面看到池塘內的情況。
這就是因為當光從一種具有折射率為n1的介質向另一種具有折射率為n2的介質傳播時,在兩者的交界處(通常稱作界面)可能會同時發生光的反射和折射——這便是菲涅耳效果。
而菲涅爾方程則描述了不同光波分量被折射和反射的情況。也描述了波反射時的相變。但是,精確描述底層物理現象的菲涅耳公式是非常復雜的,因此在圖形學中往往會采用菲涅耳公式的近似,而非菲涅耳公式本身。
常見的菲涅耳公式的近似包括所謂的Schlick’s approximation,它的公式如下:
R(θ) = R0 + (1 - R0)(1 - cosθ)5
詳細的信息各位可以查看https://en.wikipedia.org/wiki/Schlick%27s_approximation。
還有一個近似則是我們要使用的經驗近似,相對來說經驗近似的效果更可控。
R = max(0, min(1, bias + scale * (1.0 + I • N)power))
其中R代表了反射系數。向量I從眼睛指向物體表面,向量N是該點的法線,bias、scale、power則是用來調整菲涅耳效果表現的經驗參數。
而這個近似公式的基本概念就是當I和N幾乎重合的時候,反射系數應該接近0,大部分的光會被折射。當I和N分開的時候,反射系數逐漸增大,被反射的光變多。而這個系數也應該保持在0~1之間。因此,在shader中可以像下面這樣來實現:
//菲涅耳效果 float CaculateFresnelApproximation(float3 I, float3 N) { float fresnel = max(0, min(1, _FresnelBias + _FresnelScale * pow(min(0.0, 1.0 + dot(I, N)), _FresnelPower))); return fresnel; }
最終,在fragment shader內利用菲涅耳近似公式將反射效果和折射效果結合在一起,就能夠實現一個更加真實的水晶龍效果了。
Demo地址:https://github.com/chenjd/Unity-Miscellaneous-Shaders
-EOF-
最后打個廣告,歡迎支持我的書《Unity 3D腳本編程》
歡迎大家關注我的公眾號慕容的游戲編程:chenjd01