【Unity Shader】(九) ------ 高級紋理之渲染紋理及鏡子與玻璃效果的實現
筆者使用的是 Unity 2018.2.0f2 + VS2017,建議讀者使用與 Unity 2018 相近的版本,避免一些因為版本不一致而出現的問題。
【Unity Shader】(三) ------ 光照模型原理及漫反射和高光反射的實現 【Unity Shader】(四) ------ 紋理之法線紋理、單張紋理及遮罩紋理的實現 【Unity Shader】(五) ------ 透明效果之半透明效果的實現及原理 【Unity Shader】(六) ------ 復雜的光照(上) 【Unity Shader】(七) ------ 復雜的光照(下) 【Unity Shader】(八) ------ 高級紋理之立方體紋理及光線反射、折射的實現
前言
本文承接前文 【Unity Shader】(八) ------ 高級紋理(上),介紹另外一種高級紋理:渲染紋理及一些相關應用。建議讀者先翻看前文再閱讀本文會更容易理解。
一. 渲染紋理
渲染紋理是本文的重點介紹對象。如果你使用過 RenderTexture 來實現一些特殊的效果,那麽你會更能理解本文的內容。
1.1 什麽是渲染紋理
在筆者以前的博文中介紹了許多概念,其中大多提到了 緩沖(buffer)這個名詞 ,在之前我們實現的效果中,都是將攝像機的渲染效果輸出到顏色緩沖中,然後顯示到屏幕上。GPU 允許我們將渲染結果輸出到一個中間緩沖,稱為渲染目標紋理。
根據官方的定義,我們可知,渲染紋理是一種可以實時更新的特殊紋理,同時我們也可以將它像普通紋理一樣應用於一個材質中。那麽我們如何創建一個渲染紋理呢?通常我們會使用以下兩種方法來創建一個渲染紋理:
- 在 Project 下右鍵創建
- 利用 GrabPass 或者 OnRenderImage 來獲取當前屏幕圖像(OnRenderImage 函數是我們實現屏幕特效的核心方法之一,所以我不打算在此處進行介紹)
通過以上的方法我們就可以創建出一個渲染紋理了,那麽我們來利用它實現一些效果。
二. Mirror
先來看看我們要實現的效果
可以看到場景中有一面區域可以鏡像映射場景中的事物圖像,這就是我們要實現的類似鏡子的效果。那麽現在我們開始實現它。
2.1 準備工作
(1)創建一個場景,其中為了觀察效果,我使用了前文實現的立方體紋理來作為天空盒。
(2)創建 2 個 Cube,2 個 Sphere,分別賦予不同的顏色用於區別。當然你可以放上你喜歡的模型。
(3)創建一個 Quad ,將 Quad 的位置放在步驟創建的 Cube 和 Sphere 前面,面向 Cube 和 Sphere 。
(4)創建一個 Material 和 一個 RenderTexture ,命名為 Mirror 。將 RenderTexture 賦予材質,將材質賦予 Quad 。
(5)創建一個攝像機,調整位置,視野,使其相當於 Quad 望向於 Cube 和 Spere,將 RenderTexture 賦予攝像機的 Target Texture。
(6)先觀察一下效果。
可以看到 Quad 的確有點像一面鏡子一樣,但有一點十分詭異。沒錯,那就是物體位置在 X 軸上相反了。
前面說過,我們調整攝像機,讓其相當於望向物體,那麽它的視野應該是這樣的
如果不做什麽修改,直接把 RenderTexture 賦予 Quad,那麽 Quad 上的圖像就是這樣的,很顯然不符合我們的思維習慣
(7)因為鏡子是鏡像的,所以我們要解決步驟 6 中出現的問題,創建一個 shader 命名為 Mirror,實現以下的效果。
2.2 實現 shader
要解決上述問題其實在思路上是比較簡單的,只需要進行 X 軸(水平方向上的翻轉)就可以了,只是涉及了 UV 和紋理采樣的操作,且不用計算光照等,所以這個 shader 是比較簡單的。
I. 定義 Properties 塊
我們在 Properties 中只需要一個紋理屬性,對應著前面創建的 RenderTexture 。
II. 定義輸入輸出結構體
III.接下來就是在頂點著色器中翻轉 UV 的 x 分量,然後在片元著色器中利用翻轉過後的 UV 來對 RenderTexture 采樣
完整代碼:
1 Shader "Unity/RenderTexture/Mirror" { 2 Properties { 3 4 _MainTex ("Albedo (RGB)", 2D) = "white" {} 5 6 } 7 SubShader 8 { 9 Pass 10 { 11 CGPROGRAM 12 #pragma vertex vert 13 #pragma fragment frag 14 #include "UnityCG.cginc" 15 16 sampler2D _MainTex; 17 struct a2v 18 { 19 fixed4 vertex : POSITION; 20 fixed4 texcoord : TEXCOORD0; 21 }; 22 23 struct v2f 24 { 25 fixed4 pos : SV_POSITION; 26 fixed4 uv : TEXCOORD0; 27 }; 28 29 v2f vert(a2v v) 30 { 31 v2f o; 32 o.pos = UnityObjectToClipPos(v.vertex); 33 o.uv = v.texcoord; 34 o.uv.x = 1 - o.uv.x; 35 return o; 36 } 37 38 fixed4 frag(v2f i) : SV_Target 39 { 40 return tex2D(_MainTex,i.uv); 41 } 42 43 44 45 ENDCG 46 47 } 48 49 } 50 FallBack Off 51 }
IV.關閉 FallBack,保存回到 Unity,查看效果
可以看到鏡子確實翻轉了。
當我們移動物體的時候
可以看到鏡子有實時地映射出圖像
三. Glass
介紹完了鏡子效果,我們接著來介紹另外一個與鏡子相關的物體,玻璃。玻璃絕對是很常見的一種效果,而我們實現這種效果的時候正好可以介紹前文所說的使用 GrabPass 抓取屏幕圖像的方法。我們先來看一下官方文檔對其的定義。
3.1 GrabPass
ShaderLab: GrabPass
GrabPass is a special pass type - it grabs the contents of the screen where the object is about to be drawn into a texture. This texture can be used in subsequent passes to do advanced image based effects.
可以看到 GrabPass 是一種特殊的 Pass ,它可以抓取屏幕中要將對象繪制到紋理中的內容,而且抓取到的紋理可以在其他 Pass 中使用。而它的使用方法如下:
GrabPass 和我們之前使用的 Pass 一樣,寫在 SubShader 中,同樣可以使用 Name 和 Tag 的命令。它有兩種使用方法
- GrabPass {} ,這種方法抓取時,後續的 Pass 可以通過 _GrabTexture 來訪問屏幕圖像,要註意的是,對於為一個使用它的物體,Unity 都會為其單獨進行一次抓取操作。這樣每個物體都可以得到不同的屏幕圖像,這取決於這個物體的渲染順序及當前屏幕的緩沖顏色。當然,這樣會造成不小的性能消耗。
- GrabPass { “TextureName” } ,指定一張紋理,抓取的屏幕圖像會存儲到這張紋理中,而後續的 Pass 可以訪問這張紋理來訪問屏幕圖像。這種方法抓取屏幕時,Unity 只會在每一幀為第一個使用這張紋理的物體執行一次抓取屏幕的操作。所以,如果場景中有復數個物體使用了這張紋理,那麽它們得到的屏幕圖像其實是一樣的,且為第一個使用這張紋理的物體得到的屏幕圖像。
3.2 準備工作
(1)創建一個 Cube 和 一個 Sphere,將 Sphere 放置在 Cube 中心。
(2)創建一個 Material 和 一個 shader,命名為 Glass,將 Material 賦予 Cube。
(3)修改 shader。
3.3 實現玻璃 shader
先從我們的需求出發,整理思路。我們要實現的是一個玻璃的效果,那麽玻璃必定涉及光線的反射和折射,所以我們要計算光照;同時玻璃是透明的,我們要註意渲染順序;一般而言,玻璃也有不少是花紋的,所以也涉及紋理采樣的操作;而且我們還要抓取屏幕。綜合起來,大概如下
- 計算光照
- 紋理采樣
- 透明物體的處理
- 屏幕抓取
上面就是我們需要註意的主要的幾個板塊,那麽現在我們開始實現這個 shader。
I. 定義 Properties 塊
一般也有許多玻璃是帶紋理的,所以這裏也定義了普通紋理和法線紋理的屬性,同時還有天空盒的屬性,至於用不用就看實際情況了。_Distortion 表示光線折射時的扭曲程度,_RefractAmount 為 0 時,只含反射效果,_RefractAmount 為 1 時,只含折射效果。
II. 定義渲染隊列,且抓取屏幕
因為玻璃是透明物體,所以渲染隊列設置為 Transparent ,而後面的渲染狀態的設置讀者可能會感到奇怪,這裏先不提,在後面的學習中,我們還會看到這個問題的。而在 GrabPass 中,我們指定了一個紋理 _RefractionTex。
III. 定義相匹配的屬性
這裏需要註意的是,我們定義了 _RefractionTex,為了在其它 Pass 中通過它來訪問屏幕圖像,同時 _RefractionTex_TexelSize 表示紋理的紋素大小,對屏幕圖像采樣時使用。
IV.接著定義輸入輸出結構體
這裏需要註意的是,我們要將法線方向從切線空間轉換到世界空間中,所以我們要構造一個轉換矩陣。而輸出結構體中,screen 代表我們要對被抓取的屏幕圖像的采樣坐標,TtoW0,TtoW1,TtoW2 則用於構建轉換矩陣。
V.定義頂點著色器
頂點著色器和片元著色器是最重要的兩個部分。這裏我們分步驟來解釋這裏的操作
(1)先對頂點坐標進行空間轉換。
(2)利用 Unity 內置函數 ComputeGrabScreenPos 得到對應抓取屏幕圖像的采樣坐標。我們可以在 UnityCG.cginc 中看到它的定義
(3)然後對紋理采樣,如果對紋理的相關操作不熟悉的讀者,可以翻看 【Unity Shader】(四) ------ 紋理之法線紋理、單張紋理及遮罩紋理的實現 這篇博文。
(4)最後構建對應此頂點的轉換矩陣,實際上該矩陣是 3 x 3 的矩陣,而定義成 4 維變量則是為了利用 w 分量來存儲世界空間的頂點坐標。
VI.定義片元著色器
(1)在頂點著色器中,我們利用 TtoW0,TtoW1,TtoW2 的 w 分量來存儲世界空間下的頂點坐標,現在我們直接把它抽出來即可
(2)計算視角方向
(3)利用內置函數 UnpackNormal 得到切線空間下法線方向
(4)計算真正的屏幕坐標,然後采樣,得到模擬的折射顏色。對這個算法感到疑惑的讀者,可以去查閱一下透視除法
(5)分別利用 TtoW0,TtoW1,TtoW2 和上面得到的切線空間下的法線方向做點乘,就可以得到世界空間下的法線方向
(6)利用得到的新的法線方向來計算反射方向
(7)對主紋理采樣
(8)對環境映射進行采樣,得到反射顏色
(9)在計算最終顏色的式子中,我們可以看到,如果 _RefractAmount 為 0,那麽只有反射顏色,如果 _RefractAmount 為 1,那麽只有折射顏色。
VII.完整代碼
1 Shader "Unity/RenderTexture/Glass" { 2 Properties { 3 4 _MainTex ("Main Tex", 2D) = "white" {} 5 _BumpMap ("Normal Map",2D) = "bump" {} 6 _CubeMap ("Environment CubeMap",Cube) = "_Skybox"{} 7 _Distortion ("Distortion",Range(0,100)) = 10 8 _RefractAmount ("Refract Amount",Range(0.0,1.0)) = 1.0 9 10 11 } 12 SubShader 13 { 14 Tags { "Queue" = "Transparent" "RenderType" = "Opaque" } 15 GrabPass { "_RefractionTex" } 16 Pass 17 { 18 CGPROGRAM 19 #pragma vertex vert 20 #pragma fragment frag 21 #include "UnityCG.cginc" 22 23 sampler2D _MainTex; 24 float4 _MainTex_ST; 25 sampler2D _BumpMap; 26 float4 _BumpMap_ST; 27 samplerCUBE _CubeMap; 28 float _Distortion; 29 float _RefractAmount; 30 sampler2D _RefractionTex; 31 float4 _RefractionTex_TexelSize; 32 33 struct a2v 34 { 35 float4 vertex : POSITION; 36 float3 normal : NORMAL; 37 float4 tangent : TANGENT; 38 fixed4 texcoord : TEXCOORD1; 39 }; 40 41 struct v2f 42 { 43 float4 pos : SV_POSITION; 44 float4 screen : TEXCOORD0; 45 fixed4 uv : TEXCOORD1; 46 float4 TtoW0 : TEXCOORD2; 47 float4 TtoW1 : TEXCOORD3; 48 float4 TtoW2 : TEXCOORD4; 49 }; 50 51 v2f vert(a2v v) 52 { 53 v2f o; 54 o.pos = UnityObjectToClipPos(v.vertex); 55 o.screen = ComputeGrabScreenPos(o.pos); 56 o.uv.xy = TRANSFORM_TEX(v.texcoord,_MainTex); 57 o.uv.zw = TRANSFORM_TEX(v.texcoord,_BumpMap); 58 59 float3 worldPos = mul(unity_ObjectToWorld, v.vertex); 60 fixed3 worldNormal = UnityObjectToWorldNormal(v.normal); 61 fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz); 62 fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w; 63 64 o.TtoW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x); 65 o.TtoW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y); 66 o.TtoW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z); 67 ; return o; 68 } 69 70 fixed4 frag(v2f i) : SV_Target 71 { 72 float3 worldPos = float3(i.TtoW0.w,i.TtoW1.w,i.TtoW2.w); 73 fixed3 worldViewDir = UnityWorldSpaceViewDir(worldPos); 74 75 fixed3 bump = UnpackNormal(tex2D(_BumpMap, i.uv.zw)); 76 77 float2 offset = bump.xy * _Distortion * _RefractionTex_TexelSize.xy; 78 i.screen.xy = offset * i.screen.z + i.screen.xy; 79 fixed3 refrCol = tex2D(_RefractionTex, i.screen.xy / i.screen.w).rgb; 80 81 bump = normalize(half3(dot(i.TtoW0.xyz, bump), dot(i.TtoW1.xyz, bump), dot(i.TtoW2.xyz, bump))); 82 fixed3 reflDir = reflect(-worldViewDir, bump); 83 fixed4 texColor = tex2D(_MainTex, i.uv.xy); 84 fixed3 reflCol = texCUBE(_CubeMap, reflDir).rgb * texColor.rgb; 85 86 fixed3 finalColor = reflCol * (1 - _RefractAmount) + refrCol * _RefractAmount; 87 88 return fixed4(finalColor, 1); 89 } 90 91 92 93 ENDCG 94 95 } 96 97 } 98 FallBack "Diffuse" 99 }
VIII.保存,回到 Unity ,查看效果
上圖均是 _RefractAmount 為 0.75 的效果。希望讀者能夠動手實現一下,這樣才能比圖片更能感受到這個效果。
四. 總結
渲染紋理是十分常用的高級紋理,我們常常用它來實現一些十分精美的效果,除此之外,還有一種程序紋理。程序紋理是指由計算機生成的圖像,這些圖像可以做到十分的真實及豐富,不過筆者並沒有學習過相關知識,所以就不誤人子弟了。
在實現玻璃效果的 shader 中,涉及了各方面的操作,整體上還是有點復雜的,如果讀者感到吃力或完全看不懂,那我希望讀者去翻看一下前面的知識點,包括紋理采樣,光線反射,折射這些現象的原理及實現方法。最後,希望本文能對您有所幫助。
【Unity Shader】(九) ------ 高級紋理之渲染紋理及鏡子與玻璃效果的實現