使用Unity製作起霧的窗戶效果著色器

遊資網發表於2019-09-20
本教程由遊戲開發者Linden Reid介紹如何製作著色器實現起霧的窗戶效果,主要分為三部分內容:

  • 高斯模糊效果
  • 讀寫紋理
  • 根據紋理修改模糊效果


使用Unity製作起霧的窗戶效果著色器

我們在每一部分的最後會提供可以使用的著色器,你可以從中學習方法,以便在製作其它著色器時重用或改寫。

獲取實現水霧窗戶效果的著色器程式碼:

https://github.com/lindenreid/Unity-Shader-Tutorials/blob/master/Assets/Materials/Shaders/window.shader

高斯模糊

窗戶的起霧效果通過高斯模糊和往上面新增輕微的著色來實現。

我們會通過使用GrabPass標籤,獲取已經在攝像機渲染的窗戶後面的畫素,然後對這些畫素應用高斯模糊演算法。

很多文章講解過高斯模糊的實現原理,本文使用了《GLSL程式碼的高斯模糊教程》來編寫自定義的著色器:

https://github.com/mattdesl/lwjgl-basics/wiki/ShaderLesson5

使用GrabPass

我們需要獲取窗戶後面的畫素,以便對其進行模糊處理,我們可以使用Unity的GrabPass。

GrabPass將在物件後渲染的畫素繪製到著色器可以訪問的紋理上。使用該渲染批次時,我們需要使用SubShader程式碼塊中的GrabPass標籤。

  1. SubShader
  2. {
  3.    //繪製透明窗戶也很重要
  4.    Tags
  5.    {
  6.       "Queue" = "Transparent"
  7.    }

  8.    //將物件後的螢幕內容抓取到_BGTex中
  9.    GrabPass
  10.    {
  11.       "_BGTex"
  12.    }
  13.    // … 其它著色器程式碼…
複製程式碼

接下來,在CGPROGRAM標籤中,確保已經包含Unity.cginc檔案,從而使用讀取GrabPass的特別函式。

  1. #include "UnityCG.cginc"
複製程式碼

為了能夠讀取GrabPass紋理,我們需要合適的紋理座標。

Unity通過ComputeScreenGrabPos函式非常簡單的獲得座標,只要將剪輯空間頂點位置作為輸入提供,該函式就能夠給出合適的紋理座標來讀取GrabPass紋理。我們可以在頂點著色器中進行這項計算。

  1. vertexOutput output;
  2. output.pos = UnityObjectToClipPos(input.vertex);
  3. output.grabPos = ComputeGrabScreenPos(output.pos);
複製程式碼

應用模糊效果

現在,我們可以在片元著色器Fragment Shader中讀取紋理,然後應用模糊效果。

我們通過以下引數編寫模糊演算法。

  1. float4 gaussianBlur(
  2.    float2 dir,
  3.    float4 grabPos,
  4.    float res,
  5.    sampler2D tex,
  6.    float radius
  7. )

  8. {
  9. //模糊演算法在此編寫
  10. }
複製程式碼

該演算法會獲取GrabPass紋理,即“tex”,應用模糊效果,並返回float4型別的畫素顏色。

下面介紹每個引數的含義:

float2 dir:模糊效果將應用於兩個通道,所以我們需要“dir”即方向引數。效果會在X方向和Y方向各應用一次,因為我們使用(1,0)表示X方向,(0,1)表示Y方向,所以獲得的是Float2型別。

float4 grabPos:grabPos變數表示模糊畫素的紋理座標。

float res:res變數表示X軸和Y軸上的紋理解析度。

sampler2D tex:該變數表示要模糊的紋理。我們需要整個紋理,因為模糊演算法會對原始畫素附近的畫素進行取樣。

float radius:該變數表示從原始畫素到模糊位置的距離。數值越大,模糊效果越強。

接下來,讓我們定義控制模糊效果所需的引數。

我們需要一個浮點數定義模糊強度,我們將其定義為_BlurRadius,並在著色器程式碼的開始將該變數公開給屬性塊的材質。

我們還需要GrabPass紋理,該紋理的名稱要和GrabPass標籤中的名稱相同,本示例中為_BGTex。我們可以通過建立_YourTextureName_TexelSize屬性來獲取需要紋理的大小資訊。

我們給模糊效果加入了深藍色著色,使效果更明顯。如果想使用該顏色,請新增顏色到屬性中,我們將其命名為_FogColor。

  1. //屬性
  2. //在材質設定
  3. uniform float4 _FogColor;
  4. uniform float _BlurRadius;

  5. //獲取通道
  6. uniform sampler2D _BGTex;
  7. uniform float4 _BGTex_TexelSize;
複製程式碼


現在,我們得到了將模糊效果應用到背景紋理的所需資訊。

我們打算將模糊效果應用到兩個渲染批次:一個在X方向,另一個在Y方向。通常,我們會讓第二個模糊渲染批次處理第一個模糊的結果,而不是處理原始背景影象。但這樣需要更多著色器,過程也會更復雜。

所以,我對模糊的處理比較簡單,在兩個方向模糊了原始影象並新增效果。該方法的缺點是:1、模糊的質量較低。2、模糊部分比原始影象更亮,因為新增了效果。

我們將模糊部分乘以著色顏色。請注意,_TexelSize在.zw屬性中包含紋理的xy大小。

  1. float4 blurX = gaussianBlur(float2(1,0),
  2.                             input.grabPos,
  3.                             _BGTex_TexelSize.z,
  4.                             _BGTex,
  5.                             _BlurRadius);

  6. float4 blurY = gaussianBlur(float2(0,1),
  7.                             input.grabPos,
  8.                             _BGTex_TexelSize.w,
  9.                             _BGTex,
  10.                             _BlurRadius);

  11. return (blurX + blurY) * _FogColor;
複製程式碼

效果

我們將該著色器應用到材質上,並將其附加到場景的一個平坦表面上,現在著色器實現了基本的模糊效果。

下圖是起霧窗戶效果的預覽。

使用Unity製作起霧的窗戶效果著色器

讀寫紋理

為了按照滑鼠互動改變著色器效果,我們需要將滑鼠移動寫入紋理,並在著色器讀取該紋理。

從著色器讀取紋理

首先,我們在著色器中建立名為_MouseMap的sampler2D屬性。

  1. uniform sampler2D _MouseMap;
複製程式碼

在片元著色器中,繪製該紋理以便除錯。

  1. float4 mouseSample=tex2D(_MouseMap,input.texCoord.xy);
複製程式碼

以上就是片元著色器的功能,用於實現紋理的讀寫過程。對_MouseMap屬性進行編寫前,我們將得到不透明灰色平面,如下所示。

使用Unity製作起霧的窗戶效果著色器

使用C#程式碼寫入紋理

為了寫入紋理,我們需要建立C#指令碼,並將指令碼附加到平面。

我們可以通過C#程式碼的Material.Set函式,設定著色器屬性。只需要讓屬性的字串名稱對應在著色器的對應名稱即可。

  1. void OnMouseDrag ()
  2. {
  3.    //從滑鼠位置向螢幕建立光線
  4.    //然後測試對紋理的碰撞效果
  5.    Ray ray = cam.ScreenPointToRay(Input.mousePosition);
  6.    RaycastHit hit;

  7.    if(Physics.Raycast(ray, out hit, 100))
  8.    {
  9.       Color color = new Color(1, 0, 0, 1);

  10.       //把紋理座標轉換為畫素座標
  11.       int x = (int)(hit.textureCoord.x * texture.width);
  12.       int y = (int)(hit.textureCoord.y * texture.height);

  13.       //寫入被碰到的畫素
  14.       texture.SetPixel(x, y, color);

  15.      //寫入Radius範圍內的相鄰畫素
  16.       for (int i = 0; i < texture.height; i++)
  17.       {
  18.          for (int j = 0; j < texture.width; j++)
  19.          {
  20.             float dist = Vector2.Distance(new Vector2(i,j),
  21.                                           new Vector2(x,y)
  22.             );

  23.             if(dist <= Radius)
  24.                texture.SetPixel(i, j, color);
  25.             }
  26.       }

  27.       //應用改動並告知著色器
  28.       texture.Apply();
  29.       destinationRenderer.material.SetTexture("_MouseMap", texture);
  30.    }
  31. }

複製程式碼

我們為BlurColor選取了黑色,所以執行時場景效果如下圖所示。

寫入滑鼠位置

現在,我們新增一個OnMouseDrag()函式,當玩家點選划動平面時,在滑鼠位置周圍繪製圓圈。請將MeshCollider元件附加到平面物件,使它接收OnMouseDrag()事件。


現在執行遊戲,我們應該能在紋理上使用滑鼠進行繪圖了。

使用Unity製作起霧的窗戶效果著色器

根據紋理修改模糊效果

現在,我們可以根據剛建立的滑鼠拖動紋理來改變模糊效果。

根據滑鼠拖動紋理應用模糊效果

我們返回到著色器部分,根據從紋理讀取的數值應用模糊部分。由於我們在滑鼠點選的位置繪製了紅色,而且紋理預設是黑色,因此我們可以根據紅色通道修改模糊和著色量。

我們要進行以下乘法。

  1. _BlurRadius * (1 - red channel)
複製程式碼

由於紅色通道的數值在0~1之間,因此紅色數值越大,模糊的半徑越小。這種情況下,紅色通道會是0或1,所以它會在紅色繪製的位置移除模糊效果。

著色顏色同理,只不過需要在未應用起霧效果的部分定義_ClearColor。

  1. // r = 1表示滑鼠點選
  2. // r = 0表示沒有滑鼠操作
  3. float blurRadius = _BlurRadius * (1-mouseSample.r);
  4. float4 color = mouseSample.r*_ClearColor + (1.0-mouseSample.r)*_FogColor;

  5. float4 blurX = gaussianBlur(float2(1,0),
  6.                             input.grabPos,
  7.                             _BGTex_TexelSize.z,
  8.                             _BGTex,
  9.                             blurRadius);

  10. float4 blurY = gaussianBlur(float2(0,1),
  11.                             input.grabPos,
  12.                             _BGTex_TexelSize.w,
  13.                             _BGTex,
  14.                             blurRadius);

  15. return (blurX + blurY) * color;
複製程式碼

現在,我們可以在視窗進行繪製,點選的位置將消退模糊和著色效果。

使用Unity製作起霧的窗戶效果著色器

我們已經得到了不錯的窗戶起霧著色器。但是為什麼不做的更復雜一些呢?

時間演算法

在處理著色器前,請思考一下演算法的原理。基本上,我們需要根據點選指定畫素的時間,來修改模糊量。畫素時間值越小,表示它被點選的時間越近,因此起霧效果較小。

我們還需要最大持續時間來定義畫素恢復起霧效果的速度。該值將用於把時間轉換為標準化數值,即0~1,用於調整最小值和最大值之間的模糊量。

演算法如下所示。

  1. age = current time - time drawn
  2. percent max age = age / max age
複製程式碼

然後,我們將標準化的“percent max age”值應用到模糊半徑和著色。畫素時間值越小,百分比最大持續時間越小,從而使模糊強度越小。

類似地,我們會根據percent max age值,使用較小的著色顏色量和較大的清晰顏色。

  1. blur radius = max radius * percent max age
  2. tint = (1 - percent max age)*(clear color) + (percent max age)*(fog color)
複製程式碼

應用時間

為了將其應用於著色器,首先我們將畫素繪製時間寫入滑鼠貼圖紋理的r通道,而不是隻寫入1.0。

  1. Color color = new Color(Time.timeSinceLevelLoad, 0, 0, 1);
複製程式碼

接下來,在著色器應用之前的演算法,獲取percent max age值。

  1. //從滑鼠點選紋理獲取畫素繪製的時間
  2. float timeDrawn = tex2D(_MouseMap, input.texCoord.xy).r;

  3. //時間 = 當前時間 - 繪製時間
  4. float age = clamp(_Time.y - timeDrawn, 0.0001, _Time.y);

  5. //百分比最大時間 = 時間/最大時間
  6. float percentMaxAge = saturate(age / _MaxAge);
複製程式碼

最後,我們將percent max age值應用到模糊半徑和著色顏色。

  1. // 時間越長表示百分比最大時間越大,從而有更大的模糊效果
  2. float blurRadius = _BlurRadius * percentMaxAge;
  3. float4 color = (1-percentMaxAge)*_ClearColor + percentMaxAge*_FogColor;
複製程式碼

現在,模糊效果會根據定義的最大持續時間進行恢復。如下圖所示,我們將_MaxAge設為1秒,使模糊效果快速淡化。

使用Unity製作起霧的窗戶效果著色器

結語

本教程介紹瞭如何將顏色之外的資訊編碼到紋理中,以及如何利用該方法實現不錯的效果。

獲取水霧窗戶效果的著色器程式碼:

https://github.com/lindenreid/Unity-Shader-Tutorials/blob/master/Assets/Materials/Shaders/window.shader

作者:Linden Reid  
來源:Unity官方平臺
原地址:https://mp.weixin.qq.com/s/cLyCGg6h9WGkIk5Xj4KU1w

相關文章