2019年11月27日 星期三

[札記] Rendering手筋 - 透明物件深度 & 將Unity GBuffer用好用滿

兩個小主題紀錄一下。

透明物件深度

 

●圖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);
}

如此一來,物件就會像正常Rendering似的考量到深度值的大小來決定要不要寫入深度值,從而達成圖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
    .
如果有燒shadowmask,會還有一個RT4(shadowmask)存在。
今天的重點在於未使用的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了。