【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 取樣
完整程式碼:
Shader "Unity/RenderTexture/Mirror" { Properties { _MainTex ("Albedo (RGB)", 2D) = "white" {} } SubShader { Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" sampler2D _MainTex; struct a2v { fixed4 vertex : POSITION; fixed4 texcoord : TEXCOORD0; }; struct v2f { fixed4 pos : SV_POSITION; fixed4 uv : TEXCOORD0; }; v2f vert(a2v v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.uv = v.texcoord; o.uv.x = 1 - o.uv.x; return o; } fixed4 frag(v2f i) : SV_Target { return tex2D(_MainTex,i.uv); } ENDCG } } FallBack Off }
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 中看到它的定義
(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.完整程式碼
Shader "Unity/RenderTexture/Glass" { Properties { _MainTex ("Main Tex", 2D) = "white" {} _BumpMap ("Normal Map",2D) = "bump" {} _CubeMap ("Environment CubeMap",Cube) = "_Skybox"{} _Distortion ("Distortion",Range(0,100)) = 10 _RefractAmount ("Refract Amount",Range(0.0,1.0)) = 1.0 } SubShader { Tags { "Queue" = "Transparent" "RenderType" = "Opaque" } GrabPass { "_RefractionTex" } Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" sampler2D _MainTex; float4 _MainTex_ST; sampler2D _BumpMap; float4 _BumpMap_ST; samplerCUBE _CubeMap; float _Distortion; float _RefractAmount; sampler2D _RefractionTex; float4 _RefractionTex_TexelSize; struct a2v { float4 vertex : POSITION; float3 normal : NORMAL; float4 tangent : TANGENT; fixed4 texcoord : TEXCOORD1; }; struct v2f { float4 pos : SV_POSITION; float4 screen : TEXCOORD0; fixed4 uv : TEXCOORD1; float4 TtoW0 : TEXCOORD2; float4 TtoW1 : TEXCOORD3; float4 TtoW2 : TEXCOORD4; }; v2f vert(a2v v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.screen = ComputeGrabScreenPos(o.pos); o.uv.xy = TRANSFORM_TEX(v.texcoord,_MainTex); o.uv.zw = TRANSFORM_TEX(v.texcoord,_BumpMap); float3 worldPos = mul(unity_ObjectToWorld, v.vertex); fixed3 worldNormal = UnityObjectToWorldNormal(v.normal); fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz); fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w; o.TtoW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x); o.TtoW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y); o.TtoW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z); ; return o; } fixed4 frag(v2f i) : SV_Target { float3 worldPos = float3(i.TtoW0.w,i.TtoW1.w,i.TtoW2.w); fixed3 worldViewDir = UnityWorldSpaceViewDir(worldPos); fixed3 bump = UnpackNormal(tex2D(_BumpMap, i.uv.zw)); float2 offset = bump.xy * _Distortion * _RefractionTex_TexelSize.xy; i.screen.xy = offset * i.screen.z + i.screen.xy; fixed3 refrCol = tex2D(_RefractionTex, i.screen.xy / i.screen.w).rgb; bump = normalize(half3(dot(i.TtoW0.xyz, bump), dot(i.TtoW1.xyz, bump), dot(i.TtoW2.xyz, bump))); fixed3 reflDir = reflect(-worldViewDir, bump); fixed4 texColor = tex2D(_MainTex, i.uv.xy); fixed3 reflCol = texCUBE(_CubeMap, reflDir).rgb * texColor.rgb; fixed3 finalColor = reflCol * (1 - _RefractAmount) + refrCol * _RefractAmount; return fixed4(finalColor, 1); } ENDCG } } FallBack "Diffuse" }
VIII.儲存,回到 Unity ,檢視效果
上圖均是 _RefractAmount 為 0.75 的效果。希望讀者能夠動手實現一下,這樣才能比圖片更能感受到這個效果。
四. 總結
渲染紋理是十分常用的高階紋理,我們常常用它來實現一些十分精美的效果,除此之外,還有一種程式紋理。程式紋理是指由計算機生成的影象,這些影象可以做到十分的真實及豐富,不過筆者並沒有學習過相關知識,所以就不誤人子弟了。
在實現玻璃效果的 shader 中,涉及了各方面的操作,整體上還是有點複雜的,如果讀者感到吃力或完全看不懂,那我希望讀者去翻看一下前面的知識點,包括紋理取樣,光線反射,折射這些現象的原理及實現方法。最後,希望本文能對您有所幫助。