最近在工作中需要實現一個功能,用到了模板測試。但奇怪的是,模板測試竟然不起作用!在解決問題的過程中,發現了一些有趣的知識點。通過本文,可以瞭解在unity中,深度緩衝和模板緩衝到底是怎麼儲存的。
Unity版本:2021.3.16f1
URP版本:12.1.8
RenderDoc:1.29
需要注意的是,URP的版本迭代,程式碼改動較大,最好與上面的版本一致。否則,可能會因為版本不同,產生無謂的麻煩。
後面的實驗需要使用到RenderDoc。關於怎麼在Unity中使用RenderDoc,可以檢視最後的參考文獻部分。
由於後續需要修改URP的原始碼進行測試,所以需要移動URP原始碼的路徑。新建URP專案,原始碼的路徑是類似這種:xxx\Library\PackageCache(xxx是URP專案的資料夾名)。需要將以下兩個URP原始碼資料夾移動到xxx\Packages資料夾下:
移動後,Packages資料夾類似這樣:
實現一個基礎的Shader,包含了深度測試和模板測試。程式碼很簡單,就不贅述了。如下所示:
Shader "Test/Hello World"
{
Properties
{
_Color ("Main Color", Color) = (1,1,1,1)
[Header(Stencil)]
[Enum(UnityEngine.Rendering.CompareFunction)]_StencilComp ("Stencil Comparison", Float) = 8
[IntRange]_Stencil ("Stencil ID", Range(0,255)) = 0
[Enum(UnityEngine.Rendering.StencilOp)]_StencilPass ("Stencil Pass", Float) = 0
}
SubShader
{
Tags { "Queue" = "Geometry" "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline" }
Pass
{
Tags { "LightMode" = "UniversalForward" }
Cull Off
ZTest LEqual
ZWrite On
Stencil
{
Ref [_Stencil]
Comp [_StencilComp]
Pass [_StencilPass]
}
HLSLPROGRAM
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#pragma vertex vert
#pragma fragment frag
struct Attributes
{
float4 positionOS: POSITION;
};
struct Varyings
{
float4 vertex: SV_POSITION;
};
half4 _Color;
Varyings vert(Attributes input)
{
Varyings output = (Varyings)0;
output.vertex = TransformObjectToHClip(input.positionOS.xyz);
return output;
}
half4 frag(Varyings input): SV_Target
{
return _Color;
}
ENDHLSL
}
}
}
需要設定一下測試的場景環境。使用上面的Shader新建兩個材質球:Far和Near,如下設定:
Far材質球,設定為總是通過模板測試,替換模板值3,Render Queue設為2000。
Near材質球,設定模板緩衝值為3時才通過,保留模板緩衝值,Render Queue設為2010。
通過上面的設定,會先渲染Far材質球,寫入模板緩衝3。然後再渲染Near材質球,只有模板緩衝中值為3的區域才會渲染。
使用Frame Debugger檢視渲染流程,可以發現,確實是先渲染Far,再渲染Near。整體的渲染流程如下:
注意上圖中紅框中的部分,是顏色緩衝紋理的名稱。在程式碼中使用全域性搜尋,可以找到如下部分:
通過觀察分析,可以發現,深度緩衝和模板緩衝,主要是受到下面程式碼的影響:
colorDescriptor.depthBufferBits的程式碼註釋如下:
這個值代表渲染紋理的深度緩衝精度位元值,支援0,16,24,32這四個值。
下面,分別把colorDescriptor.depthBufferBits設為上面的四個值,檢視效果。
colorDescriptor.depthBufferBits = (useDepthRenderBuffer) ? 0 : 0;
場景效果
Frame Debugger
RenderDoc
分析:從場景效果看,只渲染了天空盒,沒有顯示出Far或Near。但從Frame Debugger上看,流程並沒有改變,還是先渲染Far,再渲染Near,接著再渲染天空盒。只是天空盒將Far和Near都覆蓋了。從RenderDoc看,只有顏色紋理RT0。從這些內容分析以下,應該是因為沒有了深度緩衝和模板緩衝,導致深度測試和模板測試不起作用了。
colorDescriptor.depthBufferBits = (useDepthRenderBuffer) ? 16 : 0;
場景效果
Frame Debugger 與上面相同,略
RenderDoc
分析:從場景效果看,顯示出Far和Near,但是模板測試並沒有起作用,因為完整的渲染出了Near。從Frame Debugger上看,流程並沒有改變。從RenderDoc看,除了顏色紋理RT0,還多渲染了一張紋理DS(從名字看,應該是Depth Stencil)。在RT0中右鍵選中Far範圍內的一點,再切換到DS,可以在RenderDoc的底部看到選中點的深度、模板資訊。從上圖可以看出,DS紋理的格式是R16,後面的值是選中點的深度緩衝值。這樣,可以推測,有了深度緩衝,深度測試應該是起作用了,但是模板緩衝還是沒有起作用,因為沒有模板緩衝。
colorDescriptor.depthBufferBits = (useDepthRenderBuffer) ? 24 : 0;
場景效果
Frame Debugger 與上面相同,略
RenderDoc
分析:從場景效果和Frame Debugger上看,效果和流程與開始實驗前完全一樣。從RenderDoc看,與設為16時一樣,都有RT0和DS兩張紋理。但DS紋理的格式和內容是不同的,在上圖底部可以發現,DS的格式是D32S8,後面還有深度緩衝值和模板緩衝值。與設為16時相比,DS紋理的格式不同,紋理的資訊中,還多了模板緩衝值。這樣,可以推測,深度緩衝和模板緩衝都有了,深度測試和模板測試也都起作用了。
colorDescriptor.depthBufferBits = (useDepthRenderBuffer) ? 32 : 0;
場景效果與上面相同,略
Frame Debugger 與上面相同,略
RenderDoc與上面相同,略
可以發現,設為32時,與設為24時的效果完全相同。這是為什麼呢?
colorDescriptor.depthBufferBits的原始碼如下:
public int depthBufferBits
{
get => GraphicsFormatUtility.GetDepthBits(this.depthStencilFormat);
set => this.depthStencilFormat = RenderTexture.GetDepthStencilFormatLegacy(value, this.graphicsFormat);
}
設為24時,單步偵錯的結果如下:
設為32時,單步偵錯的結果如下:
從上面可以發現,設定depthBufferBits的int值,並不會向一般的屬性那樣直接儲存int值。而是經過計算之後,儲存到GraphicsFormat型別的變數中。而當設定的值是24和32時,儲存的GraphicsFormat型別的變數都是D32_SFloat_S8_UInt。這也就解釋了為什麼設為24和32時,RenderDoc中完全一致的問題。
上面的實驗結果,可以用下面的圖表簡潔表達:
DS紋理 | 深度測試 | 模板測試 | |
---|---|---|---|
0 | ✖ | ✖ | ✖ |
16 | ✔ | ✔ | ✖ |
24 | ✔ | ✔ | ✔ |
32 | ✔ | ✔ | ✔ |
回到最初遇到的問題:模板測試不起作用。根據上面的表格,在專案中查了一下,是因為depthBufferBits設為了0,導致深度測試和模板測試都不起作用了。思路延伸一下:從效能優化的角度考慮,如果某種情況下不需要深度測試或模板測試,可以賦予depthBufferBits一個比較低的值,這樣,DS紋理佔用的記憶體會比較小,甚至不需要申請DS紋理的記憶體。