模板測試(Stencil Test)是現代渲染流水線的一環,其中涉及到的就是模板緩沖(Stencil Buffer),模板緩沖可以用來制作物體的遮罩、輪廓描邊、陰影、遮擋顯示等等效果
為屏幕上的每一個像素保存一個8位的無符號整數,跟模板緩沖區進行比較並決定是否保留像素稱為模板測試
模板測試發生在透明度測試之后,深度測試之前
模板緩沖區默認值為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使用)
自定義一些值
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
拋棄像素
輪廓描邊
思路+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
- 使用透明物體的寫深度方式
非歐幾里得空間
那非歐幾里得空間,又簡單來說:違反現實三維空間幾何規律的空間就可以認為是非歐幾里得空間
每個面顯示一個空間
想要達成非歐幾里得的效果,只需要如下設置:
- 一個面世界中,只有通過這個四邊形面片(Quad),才能看到這個里面的三維物體(GameObjects)。
- 各個面世界不相互干擾,一個面只負責顯示一個世界。
遮罩的處理
Quad Shader注意點
- 渲染順序 Queue 標簽,要比其他物體先渲染。
- 關閉 Zwrite 深度寫入,否則后面的物體ZTest不過不會顯示。
多個面互相不干擾
要想讓面世界之間互不干擾:你顯示你的,我顯示我的。就像上圖所顯示那樣。
其實很簡單,只需要為每個面世界設置不同的 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進行鏡面區域限定,配合頂點鏡面反轉,也可以實現鏡面效果
如何反轉?
給鏡子下建立一個子物體,子物體的某一條軸垂直鏡面方向,然后把世界空間的物體變換到建立的子物體的空間下,再反轉垂直的軸,即可形成虛像
虛像的處理需要關閉深度測試,或者讓他總是通過也行
Quad物體就是Mirror,有一條軸垂直鏡面的子物體WtoMW_Object:
傳送矩陣的工具物體
子物體上掛載一個腳本,用於傳送矩陣給材質
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
}
}
}
[文中案例來自INDIENOVA阿創]: https://indienova.com/u/1149119967