1. 程式人生 > >【Unity Shader】(八) ------ 高階紋理之立方體紋理及光線反射、折射的實現

【Unity Shader】(八) ------ 高階紋理之立方體紋理及光線反射、折射的實現

筆者使用的是 Unity 2018.2.0f2 + VS2017,建議讀者使用與 Unity 2018 相近的版本,避免一些因為版本不一致而出現的問題。

 

   【Unity Shader】(三) ------ 光照模型原理及漫反射和高光反射的實現
   【Unity Shader】(四) ------ 紋理之法線紋理、單張紋理及遮罩紋理的實現
   【Unity Shader】(五) ------ 透明效果之半透明效果的實現及原理
   【Unity Shader】(六) ------ 複雜的光照(上)
   【Unity Shader】(七) ------ 複雜的光照(下)

 

 

前言

關於紋理,之前在 【Unity Shader】(四) ------ 紋理之法線紋理、單張紋理及遮罩紋理的實現 已經解釋過相關原理,不過那些是屬於低維紋理,在高階紋理中,也有許多紋理是我們常見到的或常用的,同時它們能夠實現十分精美的效果。受限於篇幅,本文主要介紹立方體紋理及其相關應用,下一篇中將繼續介紹其它高階紋理。

 

一. CubeMap

單單看標題,讀者可能會不太明白我要說什麼,不過說到天空盒,讀者應該就懂了。我們來看一下官方對 CubeMap 的定義:

可以簡單理解為:CubeMap 是六個假想面的集合,這六個面對應著一個正方體的 6 個面,每個面表示沿著世界空間下的軸向觀察所得的影象。整體代表著環境的反射。 CubeMap 正常用於捕捉環境反射,而讀者熟悉的天空盒子和環境對映也是常常使用這種紋理

 

1.2 對立方體紋理的取樣

我們先說如何取樣,在詳細說如何製作 CubeMap ,對立方體紋理取樣需要提供一個三維的紋理座標。這個座標會表示一個方向(世界空間下),這個方向向量從中心出發,向外延伸,然後和 6 個面相交,然後就可以通過交點來取樣得到結果

 

1.3 使用立方體紋理的優劣

紋理不止一種,立方體紋理常用也是有其理由的,我們可以看到其優劣

  • 實現起來簡單快速(稍後會解釋),效果好
  • 不實時,加入新光源或物體時需重新生成
  • 不能模擬多次反射

當然,在現實中,我們在專案中使用普通的立方體紋理作為天空盒子等操作效果已經足夠好了。

 

1.4 立方體紋理的佈局

雖然名字中帶有立方體,但紋理佈局並不全然是一個立方體的展開。事實上,Unity 支援著數種佈局的立方體紋理,而且大多數情況下,Unity 會自動檢測它們。下面列舉幾種常見的佈局

 

常見的:

 

圓柱形佈局(全景圖常用)

 

球形佈局

 

預設情況下,Unity 會檢視紋理的寬高比以確定最合適的佈局

 

1.5 如何製作立方體紋理

製作立方體紋理有三種方法,下面我們逐一介紹

 

1.5.1 CubeMap 特殊佈局紋理製作

製作立方體紋理,最簡單的方法就是在紋理圖的 Inspector 面板中設定為 Cube,如圖

 

這張紋理就變成了立方體紋理了,然後把這張紋理賦給一個材質便可。

 

 

 

 官方也是推薦使用這種方法,因為這種方法可以

  • 壓縮紋理資料
  • 修正邊緣,光澤反射卷積(光滑反射)
  • 支援HDR

 

我們來欣賞一下HDR製作的天空盒子

 

 

1.5.2 使用 6 張紋理製作

使用 6 張獨立不同的紋理手動建立立方體紋理也是常見的一種方法。建立一個材質,shader 設定為 Skybox / 6 Sided 。

要注意的是:

  • 每張紋理都是獨立的,且要注意其對應的位置
  • Wrap Mode 設定為 Clamp ,防止在邊界處出現不匹配的現象
  • Exposure 代表天空盒子的亮度

 

就可以實現以下的效果:

 

謹記:每張紋理都必須正確對應其對應的位置

 

1.5.3 指令碼生成紋理

第三種方法比較特殊,前面兩張方法都是使用定義好的貼圖,製作出來的立方體紋理也是全域性共享的。而第三種方法不使用準備好的影象,而是依賴於指令碼,由物體在不同的位置生成。核心方法為 Camera.RenderToCubeMap ,這個方法可以從任意位置觀察到的影象儲存到 6 張影象中,從而建立當前位置的立方體紋理。我們可以在 Unity 官方指令碼手冊中找到其解釋及用法。

 

當然需要注意的是:

  • Camera.RenderToCubeMap (Cubemap cubemap, int faceMask“靜態” 的方法。當場景變化時,立方體紋理不會變化,從效果上看,類似 “烘焙”。
  • RenderToCubemap (RenderTexture cubemap, int faceMask) 是 “動態” 的方法,能夠實時渲染,但同時也需要注意資源的消耗。

對於這種方法實現的立方體紋理,我不打算在這裡贅述了,因為本文重點在後文,感興趣的讀者可以自行實現一下。

 

 

二. 光線反射

 

2.1 何為光線反射

反射是光現象中最為常見的一種,且遵循光的反射定律,即光射到一個介面時,其入射光線與反射光線成相同角度。光入射到不同介質的介面上會發生折射,如圖

 

 

反射時會出現以下情況:

  • 反射線跟入射線和法線在同一平面
  • 反射線和入射線分居法線兩側,並且與介面法線的夾角(分別叫做入射角和反射角)相等
  • 反射角等於入射角

 

前文介紹瞭如何製作立方體紋理,現在我們需要用上它來實現一些效果。反射是光現象中最為常見的一種,而使用了反射效果的物體看起來就像在表面鍍了一層金屬膜一樣。要模擬反射效果也是比較簡單的,理論上只要使用入射光線的方向和表面法線方向計算出反射方向,再用反射方向對立方體紋理取樣就行。現在我們來實現一下

 

2.2 反射的實現

I. 建立一個場景,天空盒使用在 1.5.1 或 1.5.2 中製作的立方體紋理;建立一個 Cube 和一個 Material,一個 shader,命名為 Reflection 。編輯 shader

 

II. 先定義 Properties 塊

 

其中 _ReflectAmount 控制整體反射程度,_Cubemap 表示要輸入的立方體紋理,用來儲存反射結果。

 

III. 包含相關的標頭檔案和宣告與 Properties 塊 相匹配的屬性

其中,要注意的是,立方體紋理的型別為 samplerCUBE

 

IV. 定義輸入輸出結構體

在輸出結構體中多定義了一個反射方向,所以 SHADOW_COORDS 中的插值暫存器變為 4

 

V. 定義頂點著色器

頂點著色器裡面的操作我們之前已經說過很多次了,這裡主要是多了一個計算反射方向的步驟,我們使用 reflect 函式,有關 reflect 函式我在  【Unity Shader】(三) ------ 光照模型原理及漫反射和高光反射的實現 中已經介紹過了,讀者可以翻看一下

 

VI. 定義片元著色器

 場景中只有平行光,光照計算比較簡單,這裡不再贅述。而對立方體紋理取樣,我們則是用了  texCUBE 函式,我們可以在MSDN上找到它的定義

 

在使用該函式時,我們也沒有對  i.worldReflect 進行歸一化,是因為這裡的引數僅僅是作為一個方向變數(筆者測試過歸一化的情況,結果一樣)。

將所有計算結果混合得到最終顏色返回,再加上一個  FallBack "Reflective/VertexLit"  完成。

 

VII. 儲存,回到 Unity 檢視效果,以下是不同 _ReflectAmount 的反射效果

  很抱歉的是 git 圖錄制的清晰度不夠好,以下兩圖是 _ReflectAmount 為 1 時的效果

 

完整程式碼:

 1 Shader "Unity/01-Reflection" {
 2     Properties {
 3         _Color ("Color Tint", Color) = (1,1,1,1)
 4         _ReflectColor("Reflection Color",Color) = (1,1,1,1)
 5         _ReflectAmount("Reflect Amount",Range(0,1)) = 1
 6         _Cubemap("Reflection Cubemap",Cube) = "_Skybox"{}
 7  } 8  SubShader 9  { 10 11  Pass 12  { 13 Tags { "LightMode"="ForwardBase" } 14 15  CGPROGRAM 16 #pragma multi_compile_fwdbase 17 #pragma vertex vert 18 #pragma fragment frag 19 #include "Lighting.cginc" 20 #include "AutoLight.cginc" 21 22  fixed4 _Color; 23  fixed4 _ReflectColor; 24 float _ReflectAmount; 25  samplerCUBE _Cubemap; 26 27 struct a2v 28  { 29  float4 vertex : POSITION; 30  float3 normal : NORMAL; 31  }; 32 33 struct v2f 34  { 35  float4 pos : SV_POSITION; 36  float3 worldnormal : TEXCOORD0; 37  float3 worldpos : TEXCOORD1; 38  float3 worldViewDir : TEXCOORD2; 39  float3 worldReflect : TEXCOORD3; 40 SHADOW_COORDS(4) 41  }; 42 43  v2f vert(a2v v) 44  { 45  v2f o; 46 o.pos = UnityWorldToClipPos(v.vertex); 47 o.worldnormal = UnityObjectToWorldNormal(v.normal); 48 o.worldpos = mul(unity_ObjectToWorld,v.vertex).xyz; 49 o.worldViewDir = UnityWorldSpaceViewDir(o.worldpos); 50 o.worldReflect = reflect(-o.worldViewDir,o.worldnormal); 51  TRANSFER_SHADOW(o); 52 return o; 53  } 54 55  fixed4 frag(v2f i) : SV_Target 56  { 57 fixed3 worldnormal = normalize(i.worldnormal); 58 fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldpos)); 59 fixed3 worldViewDir = normalize(i.worldViewDir); 60 61 fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz; 62 63 fixed3 diffuse = _LightColor0.rgb * _Color.rgb * max(0,dot(worldnormal,worldLightDir)); 64 65 fixed3 reflection = texCUBE(_Cubemap,i.worldReflect).rgb * _ReflectColor.rgb; 66  UNITY_LIGHT_ATTENUATION(atten,i,i.worldpos); 67 fixed3 color = ambient + lerp(diffuse ,reflection,_ReflectAmount) * atten; 68 return fixed4(color,1.0); 69 70  } 71  ENDCG 72  } 73 74 75  } 76 FallBack "Reflective/VertexLit" 77 }

 

至此,我們介紹完了反射的效果,接下來,我們來討論光現象中的另一種常見的折射效果

 

三. 光線折射

 

3.1 何為光線折射 

光從一種介質進入另一種具有不同折射率的介質,或者在同一種介質中折射率不同的部分執行時,由於波速的差異,使光的執行方向改變的現象就稱為光的折射。而光在發生折射時入射角與折射角符合斯涅耳定律:

 

 

常見的折射率有

  • 真空:1
  • 水:1.3330
  • 玻璃:一般約為 1.5

如果讀者遇到其它物質折射的情況,可自行查閱該物質的折射率。

當得到了折射方向之後,我們就可以使用它來對立方體紋理取樣”,相信讀者頭腦中可能會浮現出這個想法,但這個想法事實上是不夠嚴謹的。對於透明的物體,應該模擬兩次折射才會更為準確,光線射入物體內部,光線從內部射出。然而要在實時渲染中模擬出第二種折射是很複雜且耗費資源的,並且模擬第一次折射得到效果在大多數情況下也是良好的,所以,通常來說,我們的確會執行這種不太嚴謹的想法,即只模擬第一次折射

 

3.2 實踐

其實折射的 shader 程式碼和 反射的程式碼相差不大,所以我們進行和反射一樣的操作,然後進行幾處修改。下面列出這些值得注意的修改之處。

I. 在 Properties 塊中新增一個屬性 _RefractRatio,代表不同介質的透射比。比如光從空氣射到水體,透射比約為 1 / 1.3;同理光從空氣射到玻璃,透射比約為 1 / 1.5;

 

II. 定義與 Properties 塊匹配的屬性

 

III. 在頂點著色器中計算折射方向

 

 

我們使用的是 CG 函式 refract,找到它的定義如下

 

 

這裡需要注意的是,根據定義我們可以得知,引數是 入射光線的方向向量,表面法線的方向向量,透射比,與 reflect 函式不同,這裡明確指出需要方向向量,所以我們需要對這兩個向量歸一化

 

 IV. 其它的程式碼基本只需要替換相應的變數名字就可以了,這裡直接給出完整程式碼

 1 Shader "Unity/02-Refraction" {
 2     Properties {
 3         _Color ("Color Tint", Color) = (1,1,1,1)
 4         _RefractColor("Refraction Color",Color) = (1,1,1,1)
 5         _RefractAmount("Refract Amount",Range(0,1)) = 1
 6         _RefractRatio("Refract Ratio",Range(0,1)) = 1
 7         _Cubemap("Refraction Cubemap",Cube) = "_Skybox"{}
 8  } 9  SubShader 10  { 11 12  Pass 13  { 14 Tags { "LightMode"="ForwardBase" } 15 16  CGPROGRAM 17 #pragma multi_compile_fwdbase 18 #pragma vertex vert 19 #pragma fragment frag 20 #include "Lighting.cginc" 21 #include "AutoLight.cginc" 22 23  fixed4 _Color; 24  fixed4 _RefractColor; 25 float _RefractAmount; 26 float _RefractRatio; 27  samplerCUBE _Cubemap; 28 29 struct a2v 30  { 31  float4 vertex : POSITION; 32  float3 normal : NORMAL; 33  }; 34 35 struct v2f 36  { 37  float4 pos : SV_POSITION; 38  float3 worldnormal : TEXCOORD0; 39  float3 worldpos : TEXCOORD1; 40  float3 worldViewDir : TEXCOORD2; 41  float3 worldRefract : TEXCOORD3; 42 SHADOW_COORDS(4) 43  }; 44 45  v2f vert(a2v v) 46  { 47  v2f o; 48 o.pos = UnityWorldToClipPos(v.vertex); 49 o.worldnormal = UnityObjectToWorldNormal(v.normal); 50 o.worldpos = mul(unity_ObjectToWorld,v.vertex).xyz; 51 o.worldViewDir = UnityWorldSpaceViewDir(o.worldpos); 52 o.worldRefract = refract(normalize(o.worldViewDir),normalize(o.worldnormal),_RefractRatio); 53  TRANSFER_SHADOW(o); 54 return o; 55  } 56 57  fixed4 frag(v2f i) : SV_Target 58  { 59 fixed3 worldnormal = normalize(i.worldnormal); 60 fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldpos)); 61 fixed3 worldViewDir = normalize(i.worldViewDir); 62 63 fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz; 64 65 fixed3 diffuse = _LightColor0.rgb * _Color.rgb * max(0,dot(worldnormal,worldLightDir)); 66 67 fixed3 Refraction = texCUBE(_Cubemap,normalize(i.worldRefract)).rgb * _RefractColor.rgb; 68  UNITY_LIGHT_ATTENUATION(atten,i,i.worldpos); 69 fixed3 color = ambient + lerp(diffuse ,Refraction,_RefractAmount) * atten; 70 return fixed4(color,1.0); 71 72  } 73  ENDCG 74  } 75 76 77  } 78 FallBack "Reflective/VertexLit" 79 }

 

V.儲存,回到 Unity 檢視效果

不同透射比的折射效果:

 

由於 gif 圖清晰度有限,所以這裡做反射和折射的對比,更能看出效果

反射:

 

折射:

 

 通過對比,我們可以看到圖二中的光線似乎是被 “扭曲了” ,這正是光線的角度改變了,也正是我們要實現的折射效果

 

四. 菲涅耳反射

 

相信讀者並不會對這個名字感到陌生,沒錯,在我們生活中,最最最為常見的菲涅耳反射現象無疑是水邊。當你站在湖邊或河邊時,你能清晰地看到腳邊的水邊一切景象,而當你擡頭看向遠處水面時,卻只能看到一片白光。

當光射到物體表面時,一部分發生反射,一部分進入物體內部,然後發生折射或反射。反射光和入射光存在一定的比例關係,而這個關係可以由菲涅耳等式計算。 

而菲涅耳等式有兩條應用廣泛的近似等式:

Schlick 菲涅耳近似等式 :

                             \large {\color{Red} F_{Schlick}(v,n) = F _{0} + (1 - F_{0}) (1 - v \cdot n)^{5} } 

v 是視角方向,n 是表面法線,F0 是反射係數

Empricial 菲涅耳近似等式 : 

\large {\color{Red} F_{Empricial}(v,n) = max(0,min(1,bias + scale \times ( 1 -v\cdot n)^{power}))}

bias,scale,power 是控制項

使用菲涅耳近似等式,我們可以模擬邊界處的反射、折射/漫反射光強間的變化。特別是油漆,水面這種材質。本文只是介紹其原理及簡單實現,在以後的學習中,我們將會利用它來製作一條有趣的河流或水面。 

 

4.2 簡單菲涅耳反射的實現

此處我們使用 Schlick 菲涅耳近似等式 來實現一個菲涅耳反射現象。與實現折射時相同,大部分的計算都是一樣的,所以這裡同樣只提出值得注意的地方。

 

 I. 定義 Properties 塊

 

II. 定義相匹配變數

 

III. 計算反射方向

 

IV. 在片元著色器中計算菲涅耳反射,然後混合得到最終顏色

 

V. 完整程式碼:

 1 Shader "Unity/03-Fresne" {
 2     Properties {
 3         _Color ("Color Tint", Color) = (1,1,1,1)
 4         _FresnelScale ("Fresnel Scale",Range(0,1)) = 0.5
 5         _Cubemap("Refraction Cubemap",Cube) = "_Skybox"{}
 6     }
 7  SubShader 8  { 9 10  Pass 11  { 12 Tags { "LightMode"="ForwardBase" } 13 14  CGPROGRAM 15 #pragma multi_compile_fwdbase 16 #pragma vertex vert 17 #pragma fragment frag 18 #include "Lighting.cginc" 19 #include "AutoLight.cginc" 20 21  fixed4 _Color; 22 float _FresnelScale; 23  samplerCUBE _Cubemap; 24 25 struct a2v 26  { 27  float4 vertex : POSITION; 28  float3 normal : NORMAL; 29  }; 30 31 struct v2f 32  { 33  float4 pos : SV_POSITION; 34  float3 worldnormal : TEXCOORD0; 35  float3 worldpos : TEXCOORD1; 36  float3 worldViewDir : TEXCOORD2; 37  float3 worldReflect : TEXCOORD3; 38 SHADOW_COORDS(4) 39  }; 40 41  v2f vert(a2v v) 42  { 43  v2f o; 44 o.pos = UnityWorldToClipPos(v.vertex); 45 o.worldnormal = UnityObjectToWorldNormal(v.normal); 46 o.worldpos = mul(unity_ObjectToWorld,v.vertex).xyz; 47 o.worldViewDir = UnityWorldSpaceViewDir(o.worldpos); 48 o.worldReflect = reflect(-o.worldViewDir,o.worldnormal); 49  TRANSFER_SHADOW(o); 50 return o; 51  } 52 53  fixed4 frag(v2f i) : SV_Target 54  { 55 fixed3 worldnormal = normalize(i.worldnormal); 56 fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldpos)); 57 fixed3 worldViewDir = normalize(i.worldViewDir); 58 59 fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz; 60 61 fixed3 diffuse = _LightColor0.rgb * _Color.rgb * max(0,dot(worldnormal,worldLightDir)); 62 63 fixed3 reflection = texCUBE(_Cubemap,i.worldReflect).rgb; 64 65 fixed3 fresnel = _FresnelScale + (1 - _FresnelScale) * pow(1 - dot(worldViewDir,worldnormal),5); 66 67  UNITY_LIGHT_ATTENUATION(atten,i,i.worldpos); 68 69 fixed3 color = ambient + lerp(diffuse ,reflection,saturate(fresnel)) * atten; 70 71 return fixed4(color,1.0); 72 73  } 74  ENDCG 75  } 76 77 78  } 79 FallBack "Reflective/VertexLit" 80 }

 

 

VI.檢視效果

當 _FresnelScale 為 0 時,這時該物體就是一個具有邊緣光照效果的漫反射物體

 

如果覺得圖片效果不太明顯,讀者可以自行實現,感受一下。

 

五. 總結

對於立方體紋理,讀者可能不一定會熟悉,但你一定熟悉天空盒,而立方體紋理正是天空盒和環境對映的實現的常用方法。同時,在場景中存在天空盒的情況下,物體對環境的採光正是本文所探討的問題。無論是反射或是折射都是再通常不過的現象,所以我們也應該熟悉其實現。希望本文能對您有所幫助。