Unity Shader入門精要學習筆記 - 第10章 高級紋理
轉載自 馮樂樂的 《Unity Shader入門精要》
立方體紋理
在圖形學中,立方體紋理是環境映射的一種實現方法。環境映射可以模擬物體周圍的環境,而使用了環境映射的物體可以看起來像鍍了層金屬一樣反射出周圍的環境。
和之前見到的紋理不同,立方體紋理一共包含了6張圖像,這些圖像對應了一個立方體的6個面,立方體紋理的名稱也由此而來。立方體的每個面表示沿著世界空間下的軸向觀察所得的圖像。和之前使用二維紋理坐標不同,對立方體紋理采樣我們需要提供一個三維的紋理坐標,這個三維紋理坐標表示了我們在世界空間下的一個3D方向。這個方向矢量從立方體的中心出發,當它向外部延伸時就會和立方體的6個紋理之一相交,而采樣得到的結果就是由該焦點計算而來的。下圖給出了使用方向矢量對立方體紋理采樣的過程。
使用立方體紋理的好處在於,它的實現簡單快速,而且得到的效果也比較好。但它有一些缺點,例如當場景中引入了新的物體,光源,或者物體發生移動時,我們就需要重新生成立方體紋理。除此以外,立方體紋理也僅可以反射環境,但不能反射使用了該立方體紋理的物體本身。這是因為,立方體紋理不能模擬多次反射的結果,例如兩個金屬球互相反射的情況(事實上,Unity5 引入的全局光照系統允許實現這一的自反射效果)。由於這樣的原因,想要得到令人信服的渲染結果,我們應該盡量對凸面體而不要對凹面體使用立方體紋理(因為凹面體會反射自身)。
立方體紋理在實時渲染中有很多應用,最常見的是用於天空盒子以及環境映射。
天空盒子是遊戲中用於模擬背景的一種方法。天空盒子這個名字包含了兩個信息:它是用於模擬天空的,它是一個盒子。當我們在場景中使用了天空盒子時,整個場景就被包圍在一個立方體內。這個立方體的每個面使用的技術就是立方體紋理映射技術。
在Unity中,想要使用天空盒子非常簡單。我們只需要創建一個Skybox材質,再把它賦給該場景的相關設置即可。
我們首先來看如何創建一個Skybox材質。
1)新建一個材質
2)在該材質的Unity Shader 下拉菜單中選擇Skybox/6 Sided,該材質需要6張紋理
3)將6張紋理賦給材質上
上述步驟得到的材質如下圖所示。
上面的材質中,除了6張紋理屬性外還有3個屬性:Tint Color,用於控制該材質的整體顏色;Exposure,用於調整天空盒子的亮度;Rotation,用於調整天空盒子沿+y軸方向的旋轉角度。
下面,我們來看一下如何為場景添加Skybox。
1)新建一個場景
2)在Window -> Lighting 菜單中,把之前的材質賦給Skybox選項。如下圖所示。
為了讓攝像機正常顯示天空盒子,我們還需要保證渲染場景的攝像機的Camera組件的Clear Falgs 被設置為Skybox。這樣,我們得到場景如下圖所示。
需要說明的是,在Window -> Lighting ->Skybox 中設置的天空盒子會應用於該場景中的所有攝像機。如果我們希望某些攝像機可以使用不同的天空盒子,可以通過向該攝像機添加Skybox組件來覆蓋掉之前的設置。也就是說,我們可以在攝像機上單擊Component ->Rendering -> Skybox 來完成對場景默認天空盒子的覆蓋。
在Unity中,天空盒子是在所有不透明物體之後渲染的,而其背後使用的網格是一個立方體或一個細分後的球體。
除了天空盒子,立方體紋理最常見的用處是用於環境映射。通過這種方法,我們可以模擬出金屬質感的材質。
在Unity5中,創建用於環境映射的立方體紋理的方法有三種:第一種方法是直接由一些特殊布局的紋理創建;第二種方法是手動創建一個Cubemap資源,再把6張圖賦給它;第三種方法是由腳本生成。
如果使用第一種方法,我們需要提供一張具有特殊布局的紋理,例如類似立方體展開圖的交叉布局、全景布局等。然後,我們只需要把該紋理的Texture Type 設置為Cubemap即可,Uniry 會為我們做好剩下的事情。在基於物理的渲染中,我們通常會使用一張HDR圖像來生成高質量的Cubemap。
第二種方法是 Unity5之前的版本中使用的方法。我們首先需要在項目資源中創建一個Cubemap,然後把6張紋理拖拽到她的面板中。在Unity5中,官方推薦使用第一種方法創建立方體紋理,這是因為第一種方法可以對紋理數據進行壓縮,而且可以支持邊緣修正、光滑反射和HDR等功能。
前兩種方法都需要我們提前準備好立方體紋理的圖像,它們得到的立方體紋理往往是被場景中的物體所公用的。但在理想情況下,我們希望根據物體在場景中位置的不同,生成它們各自不同的立方體紋理。這時,我們就可以在Unity中使用腳本來創建。這時通過利用Unity提供的Camera.RenderToCubemap函數來實現的。Camera.RenderToCubemap函數可以把從任意位置觀察到的場景圖像存儲到6張圖像中,從而創建出該位置上對應的立方體紋理。
在Unity 的腳本手冊給出了如何使用Camera.RenderToCubemap函數來創建立方體紋理的代碼。其中關鍵代碼如下:
[csharp] view plain copy
- void OnWizardCreate(){
- GameObject go = new GameObject("CubemapCamera");
- go.AddComponent<Camera>();
- go.transform.position = renderFromPosition.position;
- go.GetComponent<Camera>().RenderToCubemap(cubemap);
- DestroyImmediate(go);
- }
在上面的代碼中,我們在renderFromPisition 位置處動態創建一個攝像機,並調用Camera.RenderToCubemap函數把當前位置觀察到的 圖像渲染到用於指定的立方體紋理cubemap中,完成後再銷毀臨時攝像機。由於該代碼需要添加菜單欄條目,因此我們需要把它放在Editor 文件夾下才能正確執行。
當準備好上述代碼後,要創建一個Cubemap非常簡單。
1)我們創建一個空的GameObject對象。我們會使用這個GameObejct的位置信息來渲染立方體紋理
2)新建一個用於存儲的立方體紋理(在Project 視圖下單擊右鍵,選擇Create -> Legacy -> Cubemap 來創建)。為了讓腳本可以順利將圖像渲染到該立方體紋理中,我們需要在它的面板中勾選Readable選項。
3)從Unity菜單欄選擇GameObject->Render into Cubemap,打開我們在腳本中實現的用於渲染立方體紋理的窗口,並把第1步中創建的GameObject和第2步中創建的Bubemap_0分別拖拽到窗口中的Render From Position和Cubemap選項,如下圖所示。
4)單擊窗口中的Render!按鈕,就可以把從該位置觀察到的世界空間下的6張圖像渲染到Cubemap_0中,如下圖所示。
需要註意的是,我們需要為Cubemap設置大小,即上圖中的Face size選項。Face size值越大,渲染出來的立方體紋理分辨率越大,效果可能更好,但需要占用的內存也越大,這可以由面板最下方顯示的內存大小得到。
準備好了需要的立方體紋理後,我們就可以對物體使用環境映射技術。而環境映射最常見的應用就是反射和折射。
使用了反射效果的物體通常看起來就像鍍了層金屬。想要模擬反射效果很簡單,我們只需要通過入射光線的方向和表面法線方向來計算反射方向,再利用反射方向對立方體紋理采樣即可。我們可以得到類似下圖的結果。
我們需要做如下準備工作。
1)新建一個場景。我們替換掉Unity默認的天空盒子,把之前創建的天空盒子材質拖拽到Window -> Lighting -> Skybox 選項中。
2)向場景拖拽一個Teapot模型,並調整它的位置。
3)新建一個材質,把材質賦給Teapot模型
4)新建一個Shader,賦給材質,代碼如下
- Shader "Unity Shaders Book/Chapter10-Reflection"{
- Properties{
- _Color("Color Tint",Color)=(1,1,1,1)
- //用於控制反射顏色
- _ReflectColor("Reflection Color",Color)=(1,1,1,1)
- //用於控制這個材質的反射程度
- _ReflectAmount("Reflect Amount",Range(0,1)) = 1
- //用於模擬反射的環境映射紋理
- _Cubemap("Reflection Cubemap",Cube)="_Skybox"{}
- }
- SubShader{
- Tags {"RenderType"="Opaque" "Queue"="Geometry"}
- Pass{
- Tags{"LightMode"="ForwardBase"}
- CGPROGRAM
- #pragma multi_compile_fwdbase
- #pragma vertex vert
- #pragma fragment frag
- #include "Lighting.cginc"
- #include "AutoLight.cginc"
- fixed4 _Color;
- fixed4 _ReflectColor;
- fixed _ReflectAmount;
- samplerCUBE _Cubemap;
- struct a2v{
- float4 vertex : POSITION;
- float3 normal : NORMAL;
- };
- struct v2f {
- float4 pos : SV_POSITION;
- float3 worldPos : TEXCOORD0;
- fixed3 worldNormal : TEXCOORD1;
- fixed3 worldViewDir : TEXCOORD2;
- fixed3 worldRefl : TEXCOORD3;
- SHADOW_COORDS(4)
- };
- v2f vert(a2v v){
- v2f o;
- o.pos = mul(UNITY_MATRIX_MVP,v.vertex);
- o.worldNormal = WorldObjectToWorldNormal(v.normal);
- o.worldPos = mul(_Object2World,v.vertex).xyz;
- o.worldViewDir = UnityWorldSpaceViewDir(o.worldPos);
- //計算了該頂點處的反射方向
- o.worldRefl = reflect(-o.worldViewDir,o.worldNormal);
- }
- fixed4 frag(v2f i) : SV_Target{
- fixed3 worldNormal = normalize(i.worldNormal);
- fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
- fixed3 worldViewDir = normalize(i.worldViewDir);
- fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
- fixed3 diffuse = _LightColor0.rgb*_Color.rgb*max(0,dot(worldNormal,worldLightDir));
- //對立方體紋理的采樣需要使用texCUBE函數
- fixed3 reflection = texCUBE(_Cubemap,i.worldRefl).rgb*_ReflectColor;
- UNITY_LIGHT_ATTENUATION(atten,i,i.worldPos);
- //使用_ReflectAmount 來混合漫反射顏色和反射顏色,並和環境光照相加後返回。
- fixed3 color = ambient + lerp(diffuse,reflection,_ReflectAmount)*atten;
- return fixed4(color,1.0);
- }
- ENDCG
- }
- }
- Fallback "Reflective/VertexLit"
- }
折射的物理原理比反射復雜一些。定義:當光線從一種介質斜射入另一種介質時,傳播方向一般會發生改變。當給定入射角時,我們可以使用斯涅耳定律來計算反射角。當光從介質1沿著河表面法線夾角為θ(1)的方向斜射入介質2時,我們可以使用如下公式計算折射光線與法線的夾角θ(2):
其中η(1)和η(2) 分別是兩個介質的折射率。折射率是一項重要的物理常數,例如真空的折射率是1,而玻璃的折射率一般是1.5.下圖給出了這些變量之間的關系。
通常來說,當得到折射方向後我們就會直接使用它來對立方體紋理進行采樣,但這是不符合物理規律的。對一個透明物體來說,一種更準確的模擬方法需要計算兩次折射——一次是當光線進入它的內部時,而另一次則是從它內部射出時。但是,想要阿紫實時渲染中模擬出第二次折射方向是比較復雜的,而且僅僅一次模擬得到的效果從視覺上看起來“也挺像那麽回事的”。正如我們之前提到的——圖形學第一準則“如果它看起來是對的,那麽它就是對的”。因此,在實時渲染中我們通常僅模擬第一次折射。
我們得到的效果如下圖所示:
我們添加一個Shader實現上述效果。
- Shader "Unity Shaders Book/Chapter 10/Refraction" {
- Properties {
- _Color ("Color Tint", Color) = (1, 1, 1, 1)
- _RefractColor ("Refraction Color", Color) = (1, 1, 1, 1)
- _RefractAmount ("Refraction Amount", Range(0, 1)) = 1
- //不同介質的透射比,用來計算折射方向
- _RefractRatio ("Refraction Ratio", Range(0.1, 1)) = 0.5
- _Cubemap ("Refraction Cubemap", Cube) = "_Skybox" {}
- }
- SubShader {
- Tags { "RenderType"="Opaque" "Queue"="Geometry"}
- Pass {
- Tags { "LightMode"="ForwardBase" }
- CGPROGRAM
- #pragma multi_compile_fwdbase
- #pragma vertex vert
- #pragma fragment frag
- #include "Lighting.cginc"
- #include "AutoLight.cginc"
- fixed4 _Color;
- fixed4 _RefractColor;
- float _RefractAmount;
- fixed _RefractRatio;
- samplerCUBE _Cubemap;
- struct a2v {
- float4 vertex : POSITION;
- float3 normal : NORMAL;
- };
- struct v2f {
- float4 pos : SV_POSITION;
- float3 worldPos : TEXCOORD0;
- fixed3 worldNormal : TEXCOORD1;
- fixed3 worldViewDir : TEXCOORD2;
- fixed3 worldRefr : TEXCOORD3;
- SHADOW_COORDS(4)
- };
- v2f vert(a2v v) {
- v2f o;
- o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
- o.worldNormal = UnityObjectToWorldNormal(v.normal);
- o.worldPos = mul(_Object2World, v.vertex).xyz;
- o.worldViewDir = UnityWorldSpaceViewDir(o.worldPos);
- // 計算折射方向,第一個參數即為入射光線的方向,必須是歸一化的矢量
- //第二個參數是表面法線,同樣需要歸一化
- //第三個參數是入射光線所在介質的折射率和折射光線所在介質的折射率之間的比值
- o.worldRefr = refract(-normalize(o.worldViewDir), normalize(o.worldNormal), _RefractRatio);
- TRANSFER_SHADOW(o);
- return o;
- }
- fixed4 frag(v2f i) : SV_Target {
- fixed3 worldNormal = normalize(i.worldNormal);
- fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
- fixed3 worldViewDir = normalize(i.worldViewDir);
- fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
- fixed3 diffuse = _LightColor0.rgb * _Color.rgb * max(0, dot(worldNormal, worldLightDir));
- // Use the refract dir in world space to access the cubemap
- fixed3 refraction = texCUBE(_Cubemap, i.worldRefr).rgb * _RefractColor.rgb;
- UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
- // Mix the diffuse color with the refract color
- fixed3 color = ambient + lerp(diffuse, refraction, _RefractAmount) * atten;
- return fixed4(color, 1.0);
- }
- ENDCG
- }
- }
- FallBack "Reflective/VertexLit"
- }
在實時渲染中,我們經常會使用菲涅爾反射來根據視角方向控制反射程度。通俗地講,菲涅爾反射描述了一種光學現象,即當光線照射到物體表面時,一部分發生反射,一部分進入物體內部,發生折射或散射。被反射的光和入射光之間存在一定的比率關系,這個比率關系可以通過菲涅爾等式進行計算。一個經常使用的例子是,當你站在湖邊,直接低頭看腳邊的水面時,你會發現水幾乎是透明的,你可以直接看到水底的小魚和石子;但是,當你擡頭看遠處的水面時,會發現幾乎看不到水下的情況,而只能看到水面反射的環境。這就是所謂的菲涅爾效果。事實上,不僅僅是水、玻璃這樣的反光物體具有菲涅爾效果,幾乎任何物體都或多或少包含了菲涅爾效果,這是基於物理渲染中非常重要的一項高光反射計算因子。
那麽,我們如何計算菲涅爾反射呢?這就需要使用菲涅耳等式。真實世界的菲涅耳等式是非常復雜的,但在實時渲染中,我們通常會使用一些近似公式來計算。其中一個著名的近似公式就是Schlick 菲涅耳近似等式:
其中,F(0)是一個反射系數,用於控制菲涅耳反射的強度,v是視角方向,n是法線表面。另一個應用比較廣泛的等式是Empricial菲涅耳近似等式:
其中,bias、scale和power是控制項、
使用上面的菲涅耳近似等式,我們可以在邊界處模擬反射光強和折射光強/漫反射光強之間的變化。在許多車漆、水面等材質的渲染中,我們會經常使用菲涅耳反射來模擬更加真實的反射效果。
我們使用Schlick菲涅耳近似等式來模擬菲涅耳反射。效果如下:
我們新建一個Unity Shader 實現上述效果。代碼如下:
- Shader "Unity Shaders Book/Chapter 10/Fresnel" {
- Properties {
- _Color ("Color Tint", Color) = (1, 1, 1, 1)
- _FresnelScale ("Fresnel Scale", Range(0, 1)) = 0.5
- _Cubemap ("Reflection Cubemap", Cube) = "_Skybox" {}
- }
- SubShader {
- Tags { "RenderType"="Opaque" "Queue"="Geometry"}
- Pass {
- Tags { "LightMode"="ForwardBase" }
- CGPROGRAM
- #pragma multi_compile_fwdbase
- #pragma vertex vert
- #pragma fragment frag
- #include "Lighting.cginc"
- #include "AutoLight.cginc"
- fixed4 _Color;
- fixed _FresnelScale;
- samplerCUBE _Cubemap;
- struct a2v {
- float4 vertex : POSITION;
- float3 normal : NORMAL;
- };
- struct v2f {
- float4 pos : SV_POSITION;
- float3 worldPos : TEXCOORD0;
- fixed3 worldNormal : TEXCOORD1;
- fixed3 worldViewDir : TEXCOORD2;
- fixed3 worldRefl : TEXCOORD3;
- SHADOW_COORDS(4)
- };
- v2f vert(a2v v) {
- v2f o;
- o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
- o.worldNormal = UnityObjectToWorldNormal(v.normal);
- o.worldPos = mul(_Object2World, v.vertex).xyz;
- o.worldViewDir = UnityWorldSpaceViewDir(o.worldPos);
- o.worldRefl = reflect(-o.worldViewDir, o.worldNormal);
- TRANSFER_SHADOW(o);
- return o;
- }
- fixed4 frag(v2f i) : SV_Target {
- fixed3 worldNormal = normalize(i.worldNormal);
- fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
- fixed3 worldViewDir = normalize(i.worldViewDir);
- fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
- UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
- fixed3 reflection = texCUBE(_Cubemap, i.worldRefl).rgb;
- fixed fresnel = _FresnelScale + (1 - _FresnelScale) * pow(1 - dot(worldViewDir, worldNormal), 5);
- fixed3 diffuse = _LightColor0.rgb * _Color.rgb * max(0, dot(worldNormal, worldLightDir));
- fixed3 color = ambient + lerp(diffuse, reflection, saturate(fresnel)) * atten;
- return fixed4(color, 1.0);
- }
- ENDCG
- }
- }
- FallBack "Reflective/VertexLit"
- }
渲染紋理
在之前的學習中,一個攝像機的渲染結果會輸出到顏色緩沖中個,並顯示到我們的屏幕上。現代的GPU允許我們把整個三維場景渲染到一個中間緩沖中,即渲染目標紋理(RTT),而不是傳統的幀緩沖或後備緩沖。與之相關的是多重渲染目標(MRT),這種技術指的是GPU允許我們把場景同時渲染到多個渲染目標紋理中,而不再需要為每個渲染目標紋理單獨渲染完整的場景。延遲渲染就是使用多重渲染目標的一個應用。
Unity為渲染目標紋理定義了一種專門的紋理類型——渲染紋理。在Unity中使用渲染紋理通常有兩種方式:一種方式是在Project 目錄下創建一個渲染紋理,然後把某個攝像機的渲染目標設置成該渲染紋理,這樣一來該攝像機的渲染結果就會實時更新到渲染紋理中,而不會顯示在屏幕上。使用這種方法,我們還可以選擇渲染紋理的分辨率、濾波模式等紋理屬性。另一種方式是在屏幕後處理時使用GrabPass命令或OnRederImage函數來獲取當前屏幕圖像,Unity會把這個屏幕圖像放到一張和屏幕分辨率等同的渲染紋理中,下面我們可以在自定義的Pass中把它們當成普通的紋理來處理,從而實現各種屏幕特效。
我們先學習如何使用渲染紋理來模擬鏡子效果。我們目標是得到如下圖效果。
為此,我們需要做如下準備工作
1)新建一個場景,去掉天空盒子
2)新建材質,新建一個Shader,Shader賦給這個材質
3)場景中創建6個立方體,調整它們的位置和大小,使得它們構成圍繞攝像機的房間的6面墻。場景中添加3個點光源。
4)創建3個球體和兩個立方體,調整位置和大小
5)創建一個四邊形(Quad),調整它的位置和大小,它將作為鏡子
6)在Project視圖下創建一個渲染紋理(右鍵單擊Create->Render Texture),命名為"MirrorTexture"。它使用的紋理如下圖所示。
7)最後,為了得到從鏡子觸發觀察到的場景圖像,我們還需要創建一個攝像機,並調整它的位置、裁剪平面、視角等,使得它的顯示圖像是我們希望的鏡子圖像。由於這個攝像機不需要直接顯示在屏幕上,而是用於渲染到紋理。因此,我們把之前創建的MirrorTexture拖拽到該攝像機的Target Texture上。下圖顯示了攝像機面板和渲染紋理的相關設置。
鏡子實現的原理很簡單,它使用一個渲染紋理作為輸入屬性,並把該渲染紋理在水平方向上翻轉後直接顯示到物體上即可。我們修改之前創建的Shader的代碼。
- Shader "Unity Shaders Book/Chapter 10/Mirror" {
- Properties {
- //對應了由鏡子攝像機渲染得到的渲染紋理
- _MainTex ("Main Tex", 2D) = "white" {}
- }
- SubShader {
- Tags { "RenderType"="Opaque" "Queue"="Geometry"}
- Pass {
- CGPROGRAM
- #pragma vertex vert
- #pragma fragment frag
- sampler2D _MainTex;
- struct a2v {
- float4 vertex : POSITION;
- float3 texcoord : TEXCOORD0;
- };
- struct v2f {
- float4 pos : SV_POSITION;
- float2 uv : TEXCOORD0;
- };
- //頂點著色器中計算紋理坐標
- v2f vert(a2v v) {
- v2f o;
- o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
- o.uv = v.texcoord;
- //翻轉x分量的紋理坐標。這是因為,鏡子裏顯示的圖像都是左右相反的
- o.uv.x = 1 - o.uv.x;
- return o;
- }
- //在片元著色器中對渲染紋理進行采樣和輸出
- fixed4 frag(v2f i) : SV_Target {
- return tex2D(_MainTex, i.uv);
- }
- ENDCG
- }
- }
- FallBack Off
- }
在unity中,我們還可以在Unity Shader 中使用一種特殊的Pass 完成獲取屏幕圖像的目的,這就是GrabPass。當我們再Shader中定義了一個GrabPass後,Unity會把當前屏幕的圖像繪制在一張紋理中,以便我們在後續的Pass中訪問它。我們通常會使用GrabPass來實現諸如玻璃等透明材質的模擬,與使用簡單的透明混合不同,使用GrabPass可以讓我們隊物體後面的圖像進行更復雜的處理,例如使用法線來模擬折射效果,而不再是簡單的和原屏幕顏色進行混合。
需要註意的是,在使用GrabPass 的時候,我們需要額外小心物體的渲染隊列設置。正如之前所說,GrabPass 通常渲染透明物體,盡管代碼裏並不包含混合指令,但我們往往仍然需要把物體的渲染隊列設置成透明隊列(即"Queue"="Transprent")。這樣才可以保證當渲染該物體時,所有的不透明物體都已經被繪制在屏幕上,從而獲得正確的屏幕圖像。
我們用GrabPass模擬一個玻璃效果。我們可以得到類似下圖的效果。這種效果實現非常簡單,我們首先使用一張法線紋理來修改模型的法線信息,然後使用反射方法,通過一個Cubemap來模擬玻璃的反射,而在模擬折射時,則使用了GrabPass獲取玻璃後面的屏幕圖像,並使用切線空間下的法線會屏幕紋理坐標偏移後,再對屏幕圖像進行采樣來模擬近似的折射效果。
我們需要做如下準備工作
1)新建一個場景,去掉天空盒子
2)新建材質,新建一個Shader,Shader賦給這個材質
3)構建一個測試玻璃效果的場景,我們構建了一個由6面墻圍成的封閉房間,並在房間中放置了一個立方體和一個球體,其中球體位於立方體內部,這是為了模擬玻璃對內部物體的折射效果。把材質賦給立方體
4)我們使用之前實現的創建立方體紋理的腳本(通過GameObject -> Render into Cubemap打開編輯器窗口)來創建它,如下圖所示。
我們對Shader進行修改
- Shader "Unity Shaders Book/Chapter 10/Glass Refraction" {
- Properties {
- //玻璃的紋理
- _MainTex ("Main Tex", 2D) = "white" {}
- //玻璃的法線紋理
- _BumpMap ("Normal Map", 2D) = "bump" {}
- //模擬反射的環境紋理
- _Cubemap ("Environment Cubemap", Cube) = "_Skybox" {}
- _Distortion ("Distortion", Range(0, 100)) = 10
- //用於控制折射程度,為0時,只包含反射效果,為1時,只包含折射效果
- _RefractAmount ("Refract Amount", Range(0.0, 1.0)) = 1.0
- }
- SubShader {
- Tags { "Queue"="Transparent" "RenderType"="Opaque" }
- //通過GrabPass 定義了一個抓取屏幕圖像的Pass。在這個Pass中我們定義了一個字符串。
- //該字符串內部的名稱決定了抓取得到的屏幕圖像將會存入哪個紋理中
- 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;
- fixed _RefractAmount;
- sampler2D _RefractionTex;
- float4 _RefractionTex_TexelSize;
- struct a2v {
- float4 vertex : POSITION;
- float3 normal : NORMAL;
- float4 tangent : TANGENT;
- float2 texcoord: TEXCOORD0;
- };
- struct v2f {
- float4 pos : SV_POSITION;
- float4 scrPos : TEXCOORD0;
- float4 uv : TEXCOORD1;
- float4 TtoW0 : TEXCOORD2;
- float4 TtoW1 : TEXCOORD3;
- float4 TtoW2 : TEXCOORD4;
- };
- v2f vert (a2v v) {
- v2f o;
- o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
- //通過內置的ComputeGrabScreenPos函數來得到對應抓取的屏幕圖像的采樣坐標
- o.scrPos = ComputeGrabScreenPos(o.pos);
- //計算了_MainTex和_BumpMap的采樣坐標
- o.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex);
- o.uv.zw = TRANSFORM_TEX(v.texcoord, _BumpMap);
- float3 worldPos = mul(_Object2World, v.vertex).xyz;
- fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
- fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
- fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w;
- //計算該頂點對應的從切線空間到世界空間的變換矩陣,並把該矩陣的每一行分別存儲在TtoW0~TtoW2中
- 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 {
- //通過TtoW0等變量的w得到世界坐標
- float3 worldPos = float3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w);
- //計算該片元對應的視角方向
- fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(worldPos));
- // 對法線紋理進行采樣,得到切線空間下的法線方向
- fixed3 bump = UnpackNormal(tex2D(_BumpMap, i.uv.zw));
- // 對屏幕圖像的采樣坐標進行偏移,模擬折射效果
- float2 offset = bump.xy * _Distortion * _RefractionTex_TexelSize.xy;
- i.scrPos.xy = offset * i.scrPos.z + i.scrPos.xy;
- fixed3 refrCol = tex2D(_RefractionTex, i.scrPos.xy/i.scrPos.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"
- }
在前面的實現中,我們在GrabPass中使用一個字符串指明了被抓取的屏幕圖像將會存儲在哪個名稱的紋理中。實際上,GrabPass支持兩種形式。
直接使用GrabPass{},然後再後續的Pass中直接使用_GrabTexture來訪問屏幕圖像。但是,當場景中有多個物體都使用了這樣的形式來抓取屏幕時,這種方法的性能消耗比較大,因為對於每一個使用它的物體,Unity都會為它單獨進行一次昂貴的屏幕抓取操作。但這種方法可以讓每個物體得到不同的屏幕圖像,這取決於它們的渲染隊列以及渲染它們時當前的屏幕緩沖中的顏色。
使用GrabPass{"TextureName"},正如之前實現的一樣,我們可以在後續的Pass中使用TextureName來訪問屏幕圖像。使用這種方法同樣可以抓取屏幕,但Unity只會在每一幀時為第一個使用名為TextureNane的紋理的物體執行一次抓取屏幕的操作,而這個紋理同樣可以在其他Pass中被訪問。這種方法更搞笑,因為不管場景中有多少物體使用了該命令,每一幀中Unity都只會執行一次抓取工作,但這也意味著所有物體都會使用同一張屏幕圖像。不過,在大多數情況下這已經足夠了。
盡管GrabPass和之前使用的渲染紋理+額外的攝像機的方式都可以抓取屏幕圖像,但它們還是有一些不同的。GrabPass的好處在於實現簡單,我們只需要再Shader中寫幾行代碼就可以實現抓取屏幕的目的。而要使用渲染紋理的話,我們首先需要創建一個渲染紋理和一個額外的攝像機,再把該攝像機的Render Target 設置為新建的渲染紋理對象,最後把該渲染紋理傳遞給響應的Shader。
但從效率上來說,使用渲染紋理的效率往往要好於GrabPass,尤其在移動設備上。使用渲染紋理我們可以自定義渲染紋理的大小,盡管這種方法需要把部分場景再次渲染一遍,但我麽可以通過調整攝像機的渲染層來減少二次渲染時的場景大小,或使用其他方法來控制攝像機是否需要開啟。而使用GrabPass獲取到的圖像分辨率和顯示屏幕是一致的,這意味著在一些高分辨率的設備上可能會造成嚴重的帶寬影響。而且在移動設備上,GrabPass雖然不會重新渲染場景,但它往往需要CPU直接讀取後備緩沖中的數據,破壞了CPU和GPU之間的並行性,這是比較耗時的,甚至在一些移動設備上這是不支持的。
在Unity5中,Unity引入了命令緩沖來允許我們擴展Unity的渲染流水線。使用命令緩沖我們也可以得到類似抓屏的效果,它可以在不透明物體渲染後把當前的圖像復制到一個臨時的渲染目標紋理中,然後在那裏進行一些額外的操作,例如模糊等,最後把圖像傳遞給需要使用它的物體進行處理和顯示。除此之外,命令緩沖還允許我們事先很多特殊的效果。
程序紋理
程序紋理指的是那些由計算機生成的圖像,我們通常使用一些特定的算法來創建個性化圖案或非常真實的自然元素,例如木頭、石子等、使用程序紋理的好處在於我們可以使用各種參數來控制紋理的外觀,而這些屬性不僅僅是那些顏色屬性,甚至可以完全不同類型的圖案屬性,這使得我們可以得到更加豐富的動畫和視覺效果。
我們先使用一個算法來生成一個波點紋理,如下圖所示。我們可以在腳本中調整一些參數,如背景顏色、波點顏色等,以控制最終生成的紋理外觀。
為此,我們需要進行如下準備工作。
1)創建一個場景,去掉天空盒子
2)創建一個參數,新建一個Shader,賦給材質
3)新建一個立方體,上步材質賦給它
4)創建一個腳本ProceduralTextureGeneration.cs,拖拽到上步中的立方體中。
修改ProceduralTextureGeneration.cs 代碼。
[csharp] view plain copy
- [ExecuteInEditMode]
- public class ProceduralTextureGeneration : MonoBehaviour {
- //為了保存生成的程序紋理,我們聲明了一個Texture2D類型的紋理變量
- public Material material = null;
- /*
- 註意到,對於每個屬性我們使用了get/set的方法,
- 為了在面板上修改屬性時仍可以執行set函數,我們使用了
- 一個開源插件 SetProperty。這使得當我們修改了材質屬性時,
- 可以執行_UpdateMaterial函數來使用新的屬性重新生成程序紋理。
- */
- //紋理的大小,數值通常是2的整數冪
- #region Material properties
- [SerializeField,SetProperty("textureWidth")]
- private int m_textureWidth = 512;
- public int textureWidth{
- get{return m_textureWidth;}
- set{
- m_textureWidth = value;
- _UpdateMaterial();
- }
- }
- //紋理的背景顏色
- [SerializeField,SetProperty("backgroundColor")]
- private Color m_backgroundColor = Color.white;
- public Color backgroundColor{
- get{return m_backgroundColor;}
- set{
- m_backgroundColor=value;
- _UpdateMaterial();
- }
- }
- //圓點的顏色
- [SerializeField,SetProperty("circleColor")]
- private Color m_circleColor = Color.yellow;
- public Color circleColor{
- get{return m_circleColor;}
- set{
- m_circleColor=value;
- _UpdateMaterial();
- }
- }
- //模糊因子,這個參數是用來模糊圓形邊界的
- [SerializeField,SetProperty("blurFactor")]
- private float m_blurFactor = 2.0f;
- public float blurFactor{
- get{return m_blurFactor;}
- set{
- m_blurFactor=value;
- _UpdateMaterial();
- }
- }
- #end region
- void Start(){
- if(material == null){
- Renderer renderer = gameObject.GetComponent<Renderer>();
- if(renderer == null){
- Debug.LogWarning("cannot find a renderer");
- return;
- }
- material = renderer.sharedMaterial;
- }
- _UpdateMaterial();
- }
- void _UpdateMaterial(){
- if(material != null){
- m_generatedTexture = _GenerateProceduralTexture();
- material.SetTexture("_MainTex",m_generatedTexture);
- }
- }
- Texture2D _GenerateProceduralTexture(){
- Texture2D proceduralTexture = new Texture2D(textureWidth,textureWidth);
- //定義圓與圓之間的間距
- float circleInterval = textureWidth / 4.0f;
- //定義圓的半徑
- float radius = textureWidth / 10.0f;
- //定義模糊系數
- float edgeBlur = 1.0f / blurFactor;
- for(int w = 0; w < textureWidth; w++){
- for(int h = 0; h < textureWidth; h++){
- //依次畫9個圓
- for(int i = 0; i < 3; i++){
- for(int j = 0; j < 3; j++){
- Vector2 circleCenter = new Vector2(circleInterval*(i+1),circleInterval*(j+1));
- //計算當前像素與圓心的距離
- float dist = Vector2.Distance(new Vector2(w,h),circleCenter) - radius;
- //模糊圓的邊界
- Color color = _MixColor(circleColor,new Color(pixel.r,pixel.g,pixel.b,0.0f),
- Mathf.SmoothStep(0f,1.0f,dist*edgeBlur));
- //與之前得到的顏色進行混合
- pixel = _MixColor(pixel,color,color.a);
- }
- }
- proceduralTexture.SetPixel(w,h,pixel);
- }
- }
- proceduralTexture.Apply();
- return proceduralTexture;
- }
- }
我們調整腳本面板中的材質參數來得到不同的程序紋理,如下圖所示
在Unity中,有一類專門使用程序紋理的材質,叫做程序材質。這類材質和我們之前使用的那些材質本質上是一樣的,不同的是,它們使用的紋理不是普通的紋理,而是程序紋理。需要註意的是,程序材質和它使用的程序紋理並不是在Unity中創建的,而是使用了一個名為Substance Designer的軟件在Unity外部生成的。
Substance Designer是一個非常出色的紋理生成工具,很多3A的遊戲項目都使用了由它生成的材質。我們可以從Unity的資源商店或網絡中獲取到很多免費或付費的Substance材質。這些材質都是以sbsar 為後綴的,如下圖所示,我們可以直接把這些材質像其他資源一樣拖入到Unity項目中。
當把這些文件導入Unity後,Unity就會生成一個程序紋理資源。程序紋理資源可以包含一個或多個程序材質,例如下圖就包含了兩個程序紋理——Cereals和Cereals_1,每個程序紋理使用了不同的紋理參數,因此Unity為它們生成了不同的程序紋理,例如Cereals_Diffuse和Cereals_Diffuse等。
通過單擊程序材質,我們可以在程序紋理的面板上看到該材質使用的Unity Shader 及其屬性、生成程序紋理使用的紋理屬性、材質預覽等信息。
程序材質的使用和普通材質是一樣的,我們把它們拖拽到相應的模型上即可。程序紋理的強大之處很大原因在於它的多變性,我們可以通過調整程序紋理的屬性來控制紋理的外觀,甚至可以生成看似完全不同的紋理。下圖給出了調整Cereals 程序材質的不同紋理屬性得到的不同材質效果。
可以看出,程序材質的自由度很高,而且可以和Shader配合得到非常出色的視覺效果,它是一種非常強大的材質類型。
Unity Shader入門精要學習筆記 - 第10章 高級紋理