筆者使用的是 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 菲涅耳近似等式 :
v 是視角方向,n 是表面法線,F0 是反射系數
② Empricial 菲涅耳近似等式 :

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 時,這時該物體就是一個具有邊緣光照效果的漫反射物體

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

