透明物件深度
●圖1. 透明物件也輸出深度
●圖2. 輸出深度的同時也不影響畫面
由於截圖畫質,所以會有banding現象請忽視。
在一般的情況下,我們不太會去輸出透明物件深度值,但隨著品質要求越來越高,總是會需要一些資訊到後製處理階段去。
不像Cutout物件,可以直接用Opaque的方式再用clip或discard pixel的方式來將深度值裁切掉;如果直接輸出透明物件的深度值,整片還是會輸出到Depth Buffer,造成RGB透掉的部分也有深度值的奇怪現象。
也就是說,最理想的是做到per-pixel depth output。
在D3D10之後,多了一個SV_Depth,可以讓我們在pixel shader輸出深度值。
float4 fragExample(v2f i, out float oDepth : SV_Depth) : SV_Target
{
oDepth = i.pos.z;
return float4(1,0,0,1);
}
這就是一般pipeline在做的事了,將光柵化後的z座標輸出到depth buffer,用SV_Depth就相當於這件事,那現在有了釣竿,我們就知道怎麼釣魚了。
sampler2D _CameraDepthTexture;
float4 fragExample(v2f i, out float oDepth : SV_Depth) : SV_Target
{
// oDepth = i.pos.z * i.alpha; // 不好,沒考慮到背景
float2 screenUV = i.screenUV.xy / i.screenUV.w; // 來自vertex shader的ComputeScreenPos
float sceneDepth = tex2D(_CameraDepthTexture, screenUV).r;
oDepth = lerp(sceneDepth, i.pos.z, i.alpha);
return float4(1,0,0,1);
}
輸出深度值時,考量到最終的Alpha(透明度)就行了,當然直接乘上去是會有問題的,透掉的部分是有可能還存在物件的,所以這邊跟當前的scene depth做lerp,不需要使用linear01depth之類的,因為我們要的就是raw depth。
但很可惜,還差了一步,由於SV_Depth無視順序,強制寫值,並且會打破early-z culling,做不了Greater Equal的比較(沒reversed-z的話是Less Equal),雖然大多數的時候透明物件都是從後面排到前面,但仍有交錯的可能,所以,再做個最終改寫。
struct v2f
{
centroid float4 pos : SV_POSITION;
};
sampler2D _CameraDepthTexture;
float4 fragExample(v2f i, out float oDepth : SV_DepthGreaterEqual) : SV_Target
{
// oDepth = i.pos.z * i.alpha; // 不好,沒考慮到背景
float2 screenUV = i.screenUV.xy / i.screenUV.w; // 來自vertex shader的ComputeScreenPos
float sceneDepth = tex2D(_CameraDepthTexture, screenUV).r;
oDepth = lerp(sceneDepth, i.pos.z, i.alpha);
return float4(1,0,0,1);
}
而centroid關鍵字是使用SV_DepthGreaterEqual,它的作用我不贅述,有興趣可參考此連結。
有了透明物件的深度資訊就可以發展出更多應用。
將Unity GBuffer用好用滿
根據官方文件,大家都知道Unity的GBuffer格式:
- RT0, ARGB32 format: Diffuse color (RGB), occlusion (A).
- RT1, ARGB32 format: Specular color
(RGB), roughness (A). - RT2, ARGB2101010 format: World space normal (RGB), unused (A).
- RT3, ARGB2101010 (non-HDR) or ARGBHalf (HDR) format: Emission + lighting + lightmaps
+ reflection probes
buffer. - Depth+Stencil buffer
.
今天的重點在於未使用的channel,RT2跟RT3的channel均未使用,我們是否可以最大化利用它?
會想利用的起因是有時會收到一些特殊需求,例如說指定物件不要接收陰影、指定物件不要有specular highlight (shader設定只對forward有用,似乎是BUG),這些光改standard shader達不到,因為它隱含實作在Internal-DeferredShading.shader裡面,我們必須把資料帶過來這個shader,然後再做處理。
從RT2&RT3的格式ARGB2101010我們可以知道,A是一個2-bit數值,因此排列組合上共有0 1 2 3四種數值可以利用:
half4 normalBuffer = tex2D(_CameraGBufferTexture2, i.uv);
float atten = lerp(atten, 1, (normalBuffer.a == 1)); // 數值為1時忽略陰影
float highlight = lerp(highlight, 0, (normalBuffer.a == 2)); // 數值為2時忽略高光
這樣根據通道數值來決定動作的方式就有了,但是這邊卻有個陷阱。
ARGB2101010格式對應D3D底層為DXGI_FORMAT_R10G10B10A2_UNORM,這種格式會將數值正規化為[0~1]之間,所以絕對不會有什麼1~3的數值的,當然如果使用DXGI_FORMAT_R10G10B10A2_UINT格式,就可以直接儲存了,但是Rendering不會用UINT。
因此,我們需要修改code:
half4 normalBuffer = tex2D(_CameraGBufferTexture2, i.uv);
float atten = lerp(atten, 1, (normalBuffer.a == 1/3.0f)); // 數值為1時忽略陰影
float highlight = lerp(highlight, 0, (normalBuffer.a == 2/3.0f)); // 數值為2時忽略高光
一個2-bit UNORM格式的數值,將會以0.0f, 1/3, 2/3, 1.0f來做區分。
以此類推,3-bit UNORM就會是0.0f, 1/4, 2/4, 3/4,1.0f來區隔。
然後在shader內指定這些數字,就可以做出功能區隔了,GBuffer的價值在這邊算是用到滿了。
範例
完整測試專案連結
在2017.4.3以上開啟即可,相當簡單的測試,不過並沒有直接預覽深度值的功能,需使用FrameDebugger或者是RenderDoc (圖1的瀏覽器) 之類的工具。
至於GBuffer利用的部分,改了specular highlight的部分,這部分Unity雖然有判斷keyword但是實測根本沒用,所以用GBuffer帶數值的方式決定要不要開起highlight,修改後,對Deferred Rendering的Opaque物件也能正常開關specular highlight了。