UnityShader學習筆記- Stencil Buffer


模板測試(Stencil Test)是現代渲染流水線的一環,其中涉及到的就是模板緩沖(Stencil Buffer),模板緩沖可以用來制作物體的遮罩、輪廓描邊、陰影、遮擋顯示等等效果

為屏幕上的每一個像素保存一個8位的無符號整數,跟模板緩沖區進行比較並決定是否保留像素稱為模板測試

模板測試發生在透明度測試之后,深度測試之前

img

模板緩沖區默認值為0(測試得到),並且我推測模板緩沖區每幀執行完會進行一個刷新

要加模板測試,就在Shader的Pass開頭寫Stencil{ }結構體。如果每個Pass都用,則可以提到外面

Stencil 常見語法格式

Stencil{
    Ref referenceValue          // 參考值 默認值為 0
    Comp comparisonFunction     // 定義參考值與緩沖值比較的方法 默認值為 Always
    Pass stencilOperation       // 定義當通過模板測試時,根據參考值對緩沖值的處理方法 默認值為 keep
    Fail stencilOperation       // 定義當沒有通過模板測試時,根據參考值對緩沖值的處理方法 默認為 keep
    ZFail stencilOperation      // 定義當通過模板測試卻沒有通過深度測試時,根據參考值對緩沖值的處理方法 默認為 keep
}

舉個實際例子

Stencil{    
    Ref 1
    Comp Equal
    Pass Keep
 }

上述代碼的意思是: 我們自己設定了 Ref 參考值為 1。渲染 Pass 得到像素顏色后,拿參考值 1 與模板緩沖中此像素位置的緩沖值比對,只有 Equal 相等才算通過,並且 Keep 保持原有緩沖值,否則丟棄此像素顏色。

關鍵字

stencil{
	Ref referenceValue
	ReadMask  readMask
	WriteMask writeMask
	Comp comparisonFunction
	Pass stencilOperation
	Fail stencilOperation
	ZFail stencilOperation
}

Ref

Ref referenceValue
Ref用來設定參考值referenceValue,這個值將用來與模板緩沖中的值進行比較。referenceValue是一個取值范圍位0-255的整數。

ReadMask

ReadMask readMask
ReadMask 從字面意思的理解就是讀遮罩,readMask將和referenceValue以及stencilBufferValue進行按位與(&)操作,readMask取值范圍也是0-255的整數,默認值為255,二進制位11111111,即讀取的時候不對referenceValue和stencilBufferValue產生效果,讀取的還是原始值。

WriteMask

WriteMask writeMask
WriteMask是當寫入模板緩沖時進行掩碼操作(按位與【&】),writeMask取值范圍是0-255的整數,默認值也是255,即當修改stencilBufferValue值時,寫入的仍然是原始值。

Comp

Comp comparisonFunction
Comp是定義參考值(referenceValue)與緩沖值(stencilBufferValue)比較的操作函數,默認值:always

Pass

Pass stencilOperation
Pass是定義當模板測試(和深度測試)通過時,則根據(stencilOperation值)對模板緩沖值(stencilBufferValue)進行處理,默認值:keep

Fail

Fail stencilOperation

Fail是定義當模板測試(和深度測試)失敗時,則根據(stencilOperation值)對模板緩沖值(stencilBufferValue)進行處理,默認值:keep

ZFail

ZFail是定義當模板測試通過而深度測試失敗時,則根據(stencilOperation值)對模板緩沖值(stencilBufferValue)進行處理,默認值:keep

Comp,Pass,Fail 和ZFail將會應用給背面消隱的幾何體(只渲染前面的幾何體),除非Cull Front被指定,在這種情況下就是正面消隱的幾何體(只渲染背面的幾何體)。你也可以精確的指定雙面的模板狀態通過定義CompFront,PassFront,FailFront,ZFailFront(當模型為front-facing geometry使用)和ComBack,PassBack,FailBack,ZFailBack(當模型為back-facing geometry使用)

自定義一些值

img

img

Comp比較函數

Greater Only render pixels whose reference value is greater than the value in the buffer.
GEqual Only render pixels whose reference value is greater than or equal to the value in the buffer.
Less Only render pixels whose reference value is less than the value in the buffer.
LEqual Only render pixels whose reference value is less than or equal to the value in the buffer.
Equal Only render pixels whose reference value equals the value in the buffer.
NotEqual Only render pixels whose reference value differs from the value in the buffer.
Always Make the stencil test always pass.
Never Make the stencil test always fail.

Operation

Keep Keep the current contents of the buffer.
Zero Write 0 into the buffer.
Replace Write the reference value into the buffer.
IncrSat Increment the current value in the buffer. If the value is 255 already, it stays at 255.
DecrSat Decrement the current value in the buffer. If the value is 0 already, it stays at 0.
Invert Negate all the bits.
IncrWrap Increment the current value in the buffer. If the value is 255 already, it becomes 0.
DecrWrap Decrement the current value in the buffer. If the value is 0 already, it becomes 255.

模板測試判斷依據

和深度測試一樣,在unity中,每個像素的模板測試也有它自己一套獨立的依據,具體公式如下:

if(referenceValue&readMask comparisonFunction stencilBufferValue&readMask)

通過像素

else

拋棄像素

Unity ShaderLab  模板緩存(Stencil Buffer) 基本概念

輪廓描邊

img

思路+Code

兩個Pass,第一個Pass正常渲染,第二個Pass把vertex沿着模型法線膨脹一點然后基於上一個Pass的模板緩沖區來剔除重疊部分

Shader "Unlit/Edge"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _Color("Color",Color) = (1,1,1,1)
        _RefValue("Stencil RefValue",Int) = 0
        _Outline("OutLine Width",Range(0,1)) = 0.05
        _OutlineColor("OutLineColor",Color) = (0,0,0,1)
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        //stencil buffer if zero default and it will be reset at the end of one frame Render
        Stencil{
            Ref [_RefValue]
            Comp Equal
            Pass IncrSat
        }
        CGINCLUDE
         struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                float3 normal:NORMAL;
            };
            struct v2f
            {
                float2 uv : TEXCOORD0;   
                float4 vertex : SV_POSITION;
            };
            #include "UnityCG.cginc"         
            sampler2D _MainTex;
            float4 _MainTex_ST;
            float4 _Color;
            float _Outline;
            float4 _OutlineColor;
        ENDCG
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag            
            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);            
                return o;
            }
            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv);
                return col*_Color;
            }
            ENDCG
        }
         Pass
        {
            CGPROGRAM
            #pragma vertex _vert
            #pragma fragment _frag 
            v2f _vert (appdata v)
            {
                v2f o;
                v.vertex = v.vertex+float4(normalize(v.normal)*_Outline,1);
                o.vertex = UnityObjectToClipPos(v.vertex);                        
                return o;
            }
            fixed4 _frag (v2f i) : SV_Target
            {               
                return _OutlineColor;
            }
            ENDCG
        }
    }
}

產生的問題

1、邊界交融:兩個物體物體在屏幕上有z先后關系時相交部分不會有外輪廓線

2、邊界競爭:寫入了模板緩沖區,並根據模板緩沖區進行剔除,攝像機位置變動,物體的渲染順序發生變化,先謝了模板緩沖的物體會覆蓋后了模板緩沖的物體的模型

解決邊界競爭的關鍵在於模型本體的渲染不能被模板緩沖區影響,所以兩個Pass之間使用不同的Stencil測試,第一個Pass渲染本體並對模板緩沖區進行初始化,也就是把Comp設置成Always,第二個Pass做之前一樣的模板測試

第一個Pass

Pass
        {
            Stencil{
                Ref [_RefValue]
                Comp Always
                Pass IncrSat
            }
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            //...
            ENDCG
        }

第二個Pass

 Pass
        {
            Stencil{
                Ref 0
                Comp Equal
                Pass Keep
            }
            CGPROGRAM
            #pragma vertex _vert
            #pragma fragment _frag           
            //...
            ENDCG
                
         }

實現Unity遮罩的方法

  • 搞一個渲染隊列靠前的平面,然后做模板緩沖區寫入,后來的物體做模板測試就好
    • 如何初始化模板緩沖區,用一個渲染隊列在前面的物體,調成Always
  • 使用透明物體的寫深度方式

非歐幾里得空間

那非歐幾里得空間,又簡單來說:違反現實三維空間幾何規律的空間就可以認為是非歐幾里得空間

img

每個面顯示一個空間

想要達成非歐幾里得的效果,只需要如下設置:

  1. 一個面世界中,只有通過這個四邊形面片(Quad),才能看到這個里面的三維物體(GameObjects)。
  2. 各個面世界不相互干擾,一個面只負責顯示一個世界。

遮罩的處理

Quad Shader注意點

  1. 渲染順序 Queue 標簽,要比其他物體先渲染。
  2. 關閉 Zwrite 深度寫入,否則后面的物體ZTest不過不會顯示。

image-20200522123234826

多個面互相不干擾

img

要想讓面世界之間互不干擾:你顯示你的,我顯示我的。就像上圖所顯示那樣。

其實很簡單,只需要為每個面世界設置不同的 Ref 參考值就好了。

比如左邊顯示圓球的面世界中,四邊形面片(Quad)與其中的物體們(GameObjects)的參考值都設置為 1

右邊顯示圓柱的面世界中,四邊形面片(Quad)與其中的物體們(GameObjects)的參考值都設置為 2

代碼部分

Mask

 Properties
    {
        _RefValue("Stencil Value",Int) = 1
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" "Opaque" = "Geometry-1"}     
        Pass
        {           
             Stencil{
                Ref [_RefValue]
                Comp Always
                Pass Replace
            }
            ZWrite Off
            ColorMask 0
            
            CGPROGRAM
            //...
            ENDCG
        }
    }

Obj

 Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _Color("Color",Color) = (1,1,1,1)
        _RefValue("Stencil Value",Int) = 1
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" "Opaque" = "Geometry"}
        Pass
        {           
             Stencil{
                Ref [_RefValue]
                Comp Equal
                Pass Keep
            }
            
            CGPROGRAM
            //...
            ENDCG
        }
    }

基於Stencil的鏡面效果

鏡面效果往往需要額外創建一個攝像機,根據攝像機的圖像反轉位置來渲染鏡子中的內容,利用stencil進行鏡面區域限定,配合頂點鏡面反轉,也可以實現鏡面效果

如何反轉?

給鏡子下建立一個子物體,子物體的某一條軸垂直鏡面方向,然后把世界空間的物體變換到建立的子物體的空間下,再反轉垂直的軸,即可形成虛像

虛像的處理需要關閉深度測試,或者讓他總是通過也行

image-20200522153109933
Quad物體就是Mirror,有一條軸垂直鏡面的子物體WtoMW_Object:

image-20200522153156996

傳送矩陣的工具物體

子物體上掛載一個腳本,用於傳送矩陣給材質

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//Set World to Mirror World Matrix
public class SetWtoMWMatrix : MonoBehaviour
{
    //WtoMW_Object 的 transform;
    Transform refTransform;
    //”Wrold“ To ”MirrorWorld“ Matrix(世界轉換到鏡子世界的矩陣)
    Matrix4x4 WtoMW;
    public Material material;
    //Y 軸對稱反轉矩陣
    Matrix4x4 YtoNegativeZ = new Matrix4x4(
      new Vector4(1, 0, 0, 0),
      new Vector4(0, 1, 0, 0),
      new Vector4(0, 0, -1, 0),
      new Vector4(0, 0, 0, 1));
    private void Start()
    {
        //material采用拖拽賦值的形式
        refTransform = GameObject.Find("WtoMW_Object").transform;
    }
    void Update()
    {
        //模型的坐標,從世界空間轉到鏡子空間(本質就是把一個要鏡像的物體變換到目前建立的子物體的空間上),再經由反轉Y軸得到鏡子空間的鏡像,
        //反轉Y軸是因為子物體的y軸即是鏡面朝向,其實子物體哪個軸朝外反轉到那個軸就行,然后把鏡像再轉換回世界坐標
        WtoMW = refTransform.localToWorldMatrix * YtoNegativeZ * refTransform.worldToLocalMatrix;
        material.SetMatrix("_WtoMW", WtoMW);
    }
}

MirrorObj

對於要被鏡子照到的物體我們需要形成虛像,所以需要兩個Pass,一個虛像一個實像

Shader "Unlit/MirrorObj"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _RefValue("Ref Value",Int) = 1
    }
    SubShader{     
        Tags { "RenderType"="Opaque" "Queue"="Geometry" }
        //渲染隊列在后一點
        CGINCLUDE
            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };
            struct v2f
            {
                float2 uv : TEXCOORD0;   
                float4 vertex : SV_POSITION;
                float3 worldNormal:TEXCOORD1;
            };
            #include "UnityCG.cginc"
            float4x4 _WtoMW; //矩陣
            sampler2D _MainTex;
            float4 _MainTex_ST;
        ENDCG
            
        //這里渲染虛像的 Pass
        Pass
        {
          Stencil{
            Ref [_RefValue]
            //由於stencil buffer默認是0,所以建議給個1,等於1時說明在鏡面區域內,則可以顯示虛像
            Comp Equal
            Pass keep
            ZFail keep
          }
          ZTest Off
          Cull Front //鏡面顯示背面而不顯式正面
          
          CGPROGRAM
           #pragma vertex vert
           #pragma fragment frag
          //頂點函數  
          v2f vert (appdata v)
          {
            v2f o;
            //首先將模型頂點轉換至世界空間坐標系
            float4 worldPos = mul(unity_ObjectToWorld,v.vertex);
            //再把頂點從世界空間轉換至鏡子空間
            float4 mirrorWorldPos = mul(_WtoMW,worldPos);
            //最后就后例行把頂點從世界空間轉換至裁剪空間
            o.vertex = mul(UNITY_MATRIX_VP,mirrorWorldPos);          
            o.uv = TRANSFORM_TEX(v.uv, _MainTex);
            return o;
          }
          //frag 函數和實體的是一樣的..
          fixed4 frag (v2f i) : SV_Target
          {
             fixed4 col = tex2D(_MainTex, i.uv);
             return col;
          }
          ENDCG
        }
        //這里渲染實體的 Pass
        Pass
        {
          CGPROGRAM
           // ...
          ENDCG

        }
    }
}

Mirror

沒什么好說的,就模板緩沖區初始化,然后搞成透明的

Shader "Unlit/Mirror"
{
  Properties
  {
    _MainTex ("Texture", 2D) = "white" {}
    _RefValue("Ref Value",Int) = 1
    _Color("Color Tint",Color) = (0,0,0,1)
  }
  SubShader
  {
     //注意渲染隊列
    Tags { "RenderType"="Opaque" "Queue"="Geometry-1" } 
    Stencil{
      Ref [_RefValue]
      Comp Always
      Pass Replace
    }//所謂模板緩沖區初始化
    Pass{
      //這里鏡子的正常渲染(默認我使用 Unlit 的代碼
            ZWrite Off
            ColorMask 0
            //不讓他往顏色緩沖區寫東西,這樣就是一個透明的鏡子了
            CGPROGRAM
            //不寫主要流程也沒關系,想給鏡子寫點光照反射就寫,然后記得把上面的ColorMask 0去掉
            ENDCG
    }
  }
}

img
[文中案例來自INDIENOVA阿創]: https://indienova.com/u/1149119967


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM