2019年12月29日 星期日

[札記] Forward Rendering SSR

圖1. Forward Rendering SSR

前言

SSR,利用GBuffer資訊,在Screen Space上做有限的Ray Tracing然後得到反射結果的一種技術。

它的最大好處是不需要設立動態cubemap就能捕捉畫面上大部分的反射內容,相當適合用來綴飾平面物件 (例如少數分散的水灘、平滑度高的表面等等),節省相當多的draw call,當然動態cubemap還是有它發揮的時候 (例如想反射畫面上沒有的東西)。

而常常聽到SSR最常出現的感嘆(或稱抱怨)是,它只能在Deferred Rendering上面使用。
沒錯,官方也明說了這一點,但你知道為什麼只能在Deferred Rendering上使用嗎?
而知道了以後有沒有機會修改成在Forward Rendering也能使用的形式?
當然是可以的,所以才有這篇

Hybrid Rendering Pipeline

混合RP在今天已經算是見怪不見的事情,例如Unity的Deferred Rendering,它也僅於Opaque部分使用Deferred Rendering,後面透明物件還是靠Forward Rendering Pass,這算是混合了兩種RP。

再舉別的例子,很多3A引擎甚至實作過Forward+ Rendering (一種tile-based技術),難道他們就沒有後製特效支援嗎?這方面DOOM就做了一個很好的示範,畫場景時主要的流程以Forward+為主,但同時搭配MRT生成normal、specular buffer,解決了後製的問題,誰說Rendering Pipeline只能寫死呢?

在Forward Rendering中加入必要資訊

SSR需要的資訊如下:
  • Depth Texture: 為了還原view space座標以便Ray Tracing
  • Color Texture: 未做任何後製前的Diffuse Color,計算最終結果需要用到
  • Normal Texture: Ray Tracing時要知道反射方向,計算最終結果也要用到
  • Specular Texture: 不用多解釋了吧!PBR公式的一環,計算最終結果時要考慮gloss

因此要給資訊的第一步,就是去建立color、normal、specular Render Texture
然後所有forward shader都要改寫成MRT輸出 (Multiple Render Target)

圖2. Shader改為MRT輸出

其實就是加入MRT,讓流程有辦法輸出額外資訊而已,的確Deferred Rendering屬於MRT的應用,但不代表MRT只能用於Deferred Rendering,改寫好了以後,就是流程裡面要SetRenderTargets()。

恰恰好,SSR這個東西只適用於Opaque物件,它在Transparent物件之前就完成反射計算,也就是說我們只要在計算完之前替換掉Unity設定的Render Target就可以了。

呼叫ComandBuffer.SetRenderTarget(),在CameraEvent.BeforeForwardOpaque插入這個指令就好了...........才怪!!只單純這樣使用的話,最後還是會被Unity強制蓋回他們的Target。

因此,Native Plugin出動的時候又到了:


圖3. 呼叫Native Plugin設定MRT(上) Plugin內容(下)


底層其實就是Clear & Set而已,但歸功於Unity的無能我們必須多做這一步。




圖4. Forward Rendering With GBuffer

妥善設定MRT後,要在Forward Rendering生成GBuffer沒有問題,並且在最後把結果給取代掉Unity的_CameraGBuffer0~2號。

順便在Script端把內建的DepthTextureMode.DepthNormals給強硬關掉了,因為這會造成Unity畫一次多餘的Pass來生成Normal Texture。
 
            if (forwardGBuffer)
            {
                m_Camera.depthTextureMode &= ~DepthTextureMode.DepthNormals;
            }

Shader需要改的地方

圖5. Reflection Probe修改

內建SSR還有針對Reflection Probe去做Blending,總不能兩種結果直接相加吧?
這邊會用到前面ray tracing以及其他設定的結果來blending,詳細內容就不提了。

總之會需要用到_CameraReflectionsTexture,這張貼圖其實就是indirect specluar的部分(reflection probe、baked skybox等等),這邊是Deferred Rendering獨有的貼圖,沒有的話怎麼辦呢?兩個手法:
  1. 代入Deferred Reflection的算式,這邊使用的方法,其實就是把Internel-DeferredReflection.shader裡面的fragment拿過來而已,compose這一步剛好也是full screen的,所以直接呼叫是通用的。
  2. MRT的時候多一張RenderTexture存反射資訊,比起方法1我會比較想這樣,因為forward pass裡面就一次把反射也算好了,再用方法1就重複計算了,不過這個方法當然會再吃掉VRAM(多一張)。
看需求選擇,這邊選方法一是方便展示。

沒有考慮到的地方

之所以會想用Forward Rendering,就是為了MSAA,而這邊的例子都沒有處理這個,但是要處理的方法也是有的:
  1. RT建立成multisample的形式。
  2. 畫完之後Resolve這些Render Targets。
但是這樣很直觀地就會導致VRAM損耗上升,因為Resolve的Source跟Destination都必須存在於GPU的記憶體上,而這種每個frame都要做的操作又不建議做串流, 吃VRAM會變成問題,建議4GB以上顯卡再來考慮處理MSAA,不然就盡量只開2X。

想耍賴在主畫面用MSAA,GBuffer不用AA也是不行的,至少MRT現在的規定是Render Targets都要相同Sample Count。

參考

Github Code: https://github.com/SquallLiu99/Forward-Rendering-SSR




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了。

2019年11月17日 星期日

[Unity] 快速刷出邊緣光

●圖1. 一台車加上刷光特效

首先感謝 https://free3d.com/3d-models/vehicles
這個網站,上面真的有蠻多素材可以測試
找素材的時間我都寫完10份演算法了XD

目標

簡單來說,目的就是丟一個模型進去,計算完邊緣之後將結果存下來。
這種邊緣的計算跟抗鋸齒要的不太一樣,抗鋸齒可以即時處理,但是想要這種刷光是需要前置輔助的。

如果不自動計算,美術就只能一碗滷肉飯一瓶礦泉水一筆一畫畫在想要的位置上,實在很沒效率。



●圖2. 這個要美術畫大概會起笑吧

演算法

演算法很單純,邊緣查找,不外乎就是把所有頂點一個一個打斷鼻樑抓進來,然後兩兩比較他們的法線的內積


這個公式大家再熟悉不過了,當a跟b都是單位向量的時候角度與內積值會呈反比。

所以工作相當的單純:
  1. 先給一個頂點的距離門檻值,篩掉距離太遠的點 (邊緣之所以叫邊緣就是他們很接近)
  2. 計算兩兩點內積值,若有多重個點,加總內積值以及次數
  3. 平均內積值,輸出到貼圖上面 (想加粗可以改成min,想變細可以改抓max,看需求)

實作方式

從邊緣計算的方式我們可以知道,點數越大的模型,出來的效果越好
因為如果面數太低,點與點之間的距離必定會加大,變成距離門檻值要拉很高才算得出東西,這樣不論怎麼算都會是粗線條。

因此,範例我抓了一個96000點的模型來使用,不過問題來了,這個計算量會是相當龐大的。
96000*95999 =  9215904000
92億次的計算????市面上絕對沒有CPU撐得住

如果修改演算法,先根據距離門檻值把點做分群再算呢?
可能值得一試,但先分群反而還多了前處理的時間,根據切法不同有時候可能還不會真正省到時間

所以,維持簡單的算法,丟到GPU上面算吧!
  • 方法一、 Geometry Shader

    Geometry Shader允許我們輸入一個三角形,一次針對一整個三角形來處理,其中更有一種輸入型別 triangleadj,可以抓取鄰居三角形! (距離門檻值都省了)


    illustration of the various primitive types for a geometry shader object 

    本來的input是0 1 2,丟入triangleadj就會變成去處理024 012 045 234
    這樣邊緣資訊就唾手可得了,不過,這個方法卻有個致命的缺點....
    就是Unity不支援,掰掰洗洗睡


  • 方法二、Compute Shader

    另一個方法,自然就是用Compute Shader加速了!它的特性是多重thread平行化計算,對於這種大量但是規則單一的動作是最為拿手的了!








    在計算時,我的頂點以16383一組為單位,去呼叫compute shader,一次使用1024個threadgroup處理,一個threadgroup又有1024個threads在執行,所以96000*96000次計算,實際上被我打散成 16 * 94 = 1504次!
    如此一來,計算上就是節省為 96000 / 16383 * 1504次,八千多次即可完成
    這實在是非常多的節省~

    當然要用compute shader只得一個缺點: 需要D3D11
    不過現在應該沒有顯卡不支援,若果一個專案用不起支援D3D11的顯卡就趕快逃吧


     

計算結果的優化

最後結果的輸出,絕對不是直接用模型的uv來寫值到貼圖上,要知道compute shader可不像fragment shader一樣,會自動幫我們把pixel之間做線性內插,如果直接輸出根本無法使用:



因此,這邊的輸出並不是最終輸出,還要再做一次smooth的動作,輸出的方式為:


首先創一張足夠容納模型點數的像素圖 (96000開根號 = 310 * 310大小的圖)
只是將它作為一張查找表來使用 (變成256*256只是我輸出一份來預覽,實際上是310*310)

↑哥存的是查找貼圖,不是渲染貼圖


然後,再把這張查找表丟到另外一個Shader做處理:
↑最重要的部分

首先,我們必須把數值取出來,所以這邊利用SV_VertexID系統關鍵字,來取得vertex id,然後做了跟我剛剛在compute shader做的轉換,利用vertex -> fragment shader會幫我們內插數值的特性來優化結果!

其實這個結果就相當於把數值儲存在model裡面使用,但是我不想在即時渲染的時候還是使用高面數版本的模型,我希望能夠低面數存取貼圖的情況

所以,這一小段第二個重點,就是clip-to-texture space的小轉換了
一般情況下,我們做頂點轉換的時候,最終都會投影到[-1,1]這個稱為NDC (Normalized Device Coordinate) Space的空間,所以這邊就是利用模型的uv,假設uv絕不重疊且位於[0,1]之間,透過簡單的轉換轉成[-1,1]就可以了

如此一來,圖2的結果就會展現出來


總結

輸入一個高面數模型 (假設normal正確,uv不重疊並介於0~1之間)
透過compute shader快速取得邊緣值
再透過mapping-to-texture的方式輸出結果
最後就可以使用低面數的模型來繪製出刷光的結果!
生成出來的圖亦可以再做模糊或其它後製處理來平滑細節


2019年5月8日 星期三

Deferred MSAA in Unity

前言

以前在練習寫自己的D3D12引擎的時候就有做過,這次看看能不能取代UnityGBuffer來修改。

Deferred MSAA是可行的,不過解法並不一般,而且因為Unity架構的關係,實現完後還會存在一些需要解決(或忽略)的問題。 底層自己寫引擎能用,到了Unity就不能的情況也不是第一次。

大家都清楚GBuffer一開始用MRT綁入到pipeline,然後一次輸出到多張RT
每張RT存入不同內容(diffusespecularnormal等等),最後再過一個組合pass來完成frame,而Unity生成的RT都沒有開啟multisample


所以必須要靠自己來深入pipeline做修改了!


最低需求D3D11/Shader Model 4.0以上 (其實D3D10.1就是4.0以上了,不過10這種不上不下的東西就不提了)。
並且需要搭配graphic native plugin才能做到

修改GBuffer


說修改可能不太正確,因為unity根本拿不到cpu端的GBuffer,必須另外創然後附加上去:

CreateMapAndColorBuffer("Custom diffuse", 0, RenderTextureFormat.ARGB32, 0, msaaFactor, ref diffuseRT);
CreateMapAndColorBuffer("Custom specular", 0, RenderTextureFormat.ARGB32, 1, msaaFactor, ref specularRT);
CreateMapAndColorBuffer("Custom normal", 0, RenderTextureFormat.ARGB2101010, 2, msaaFactor, ref normalRT);

只貼前三張示意一下,CreateMapAndColorBuffer會去根據msaafactor建立RenderTexture並且要把bindMS設為true並且在裡面使用GetNativeTexturePtrRT資源丟到D3D11 plugin裡面去建立RenderTargetView

之所以這麼麻煩是為了在CameraEvent.BeforeGBuffer這個流程,把我們創好的multisample RT駭進去給pipeline不然使用一般的SetRenderTarget方法Unity是不會鳥你的

gBufferColor[_index] = (ID3D11Texture2D*)_colorBuffer;
if (gBufferColor[_index] == nullptr)
{
return false;
}
D3D11_TEXTURE2D_DESC texDesc;
gBufferColor[_index]->GetDesc(&texDesc);
3D11_RENDER_TARGET_VIEW_DESC rtvDesc;
ZeroMemory(&rtvDesc, sizeof(rtvDesc));
rtvDesc.Format = ConvertTypelessFormat(texDesc.Format);
rtvDesc.ViewDimension = (_msaaFactor > 1) ? D3D11_RTV_DIMENSION_TEXTURE2DMS : D3D11_RTV_DIMENSION_TEXTURE2D;
rtvDesc.Texture2D.MipSlice = 0;
HRESULT rtvResult = m_Device->CreateRenderTargetView(gBufferColor[_index], &rtvDesc, &gBufferColorView[_index]);

上面是底層建立RenderTargetView的部分~接到我們呼叫GetNativeTexturePtr後直接建立即可,注意這邊需要根據msaa有無開啟,去將ViewDimension調整為D3D11_RTV_DIMENSION_TEXTURE2DMS

另外如果場景有燒lightmap,並且有使用shadow mask的話,還會再多一張RenderTarget給light data使用,這部分由於我沒有燒lightmap測試所以就先不做了

修改深度圖

GBuffer要建立multisample的,那麼深度圖當然也是了。

CreateMapAndColorBuffer("Cutsom depth", 32, RenderTextureFormat.Depth, -1, msaaFactor, ref depthRT);

一樣建立一張深度圖,這次是透過使用GetNativeDepthBufferPtr丟到D3D11 Plugin裡面去建立DepthStencilView

gBufferDepth = (ID3D11Texture2D*)_depthBuffer;
if (gBufferDepth == nullptr)
{
  return false;
}
D3D11_TEXTURE2D_DESC texDesc;
gBufferDepth->GetDesc(&texDesc);
D3D11_DEPTH_STENCIL_VIEW_DESC dsvDesc;
ZeroMemory(&dsvDesc, sizeof(dsvDesc));
dsvDesc.Format = ConvertTypelessFormat(texDesc.Format);
dsvDesc.ViewDimension = (_msaaFactor > 1) ? D3D11_DSV_DIMENSION_TEXTURE2DMS : D3D11_DSV_DIMENSION_TEXTURE2D;
dsvDesc.Texture2D.MipSlice = 0;
HRESULT dsvResult = m_Device->CreateDepthStencilView(gBufferDepth, &dsvDesc, &gBufferDepthView);

GBuffer建立很像,RTV改成DSV而已(以後你要建立shader用的資源的話就是SRV)

ConvertTypelessFormat的部分只是一個小函式,將資源格式轉換為RTVDSV等等可以利用的格式。

例如32bit-深度圖的資源格式是R32G8X24_TYPELESS (這是一個enum),我這個小函式就回傳D32_FLOAT_S8X24_UINT建立DepthStencilView
以這個例子就是其實32-bit深度圖會在底層建立成64-bit資源,D32說明有32-bit作為深度圖使用,S8代表有8-bit作為stencil buffer使用,剩下24-bit是多餘的。


格式之間必須嚴格地確認,要同一個group才能建立,例如R32G32B32A32不能建立成R32G32,詳情請找微軟DXGI_FORMAT的說明來看。

繪圖前入侵並修改GBuffer

前面說過,CameraEvent.BeforeGuffer這段需要駭入我們建立的GBuffer

void RenderAPI_D3D11::SetGBufferTarget()
{
if (m_Device == nullptr)
{
     return;
}
ID3D11DeviceContext *immediateContext = nullptr;
m_Device->GetImmediateContext(&immediateContext);
if (immediateContext == nullptr)
{
return;
}
// set gbuffer target
FLOAT clearColor[4] = { 0,0,0,-1 };
for (int i = 0; i < 4; i++)
{
immediateContext->ClearRenderTargetView(gBufferColorView[i], clearColor);
}
// get unity's depth buffer
immediateContext->OMGetRenderTargets(0, NULL, &screenDepthView);
// replace om binding with custom targets
immediateContext->ClearDepthStencilView(gBufferDepthView, D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 0.0f, 0);
immediateContext->OMSetRenderTargets(4, gBufferColorView, gBufferDepthView);
immediateContext->Release();
}

呼叫IssuePluginEvent,在底層做,而清除RT顏色的alpha通道我設定成-1別有用途,裡面有一行取得unity的深度view只是我用來處理msaafactor設定成1時的情況在用的。

Resolve GBuffer

接著就是畫完以後的處置了,先上cpu端的code:
copyGBuffer.SetGlobalTexture(texName[texIdx], emissionRT);
copyGBuffer.Blit(null, BuiltinRenderTextureType.CameraTarget, resolveAA);
copyGBuffer.SetGlobalTexture(texName[texIdx], depthRT);
copyGBuffer.Blit(null, BuiltinRenderTextureType.CameraTarget, resolveAADepth);
for (int i = 0; i < msaaFactor; i++)
{
    copyGBuffer.SetGlobalFloat("_TransferAAIndex", i);
copyGBuffer.SetRenderTarget(diffuseAry, 0, CubemapFace.Unknown, i);
copyGBuffer.SetGlobalTexture("_MsaaTex", diffuseRT);
copyGBuffer.Blit(null, BuiltinRenderTextureType.CurrentActive, transferAA);
copyGBuffer.SetRenderTarget(specularAry, 0, CubemapFace.Unknown, i);
copyGBuffer.SetGlobalTexture("_MsaaTex", specularRT);
copyGBuffer.Blit(null, BuiltinRenderTextureType.CurrentActive, transferAA);
copyGBuffer.SetRenderTarget(normalAry, 0, CubemapFace.Unknown, i);
copyGBuffer.SetGlobalTexture("_MsaaTex", normalRT);
copyGBuffer.Blit(null, BuiltinRenderTextureType.CurrentActive, transferAA);
}
copyGBuffer.SetGlobalTexture("_GBuffer0", diffuseAry);
copyGBuffer.SetGlobalTexture("_GBuffer1", specularAry);
copyGBuffer.SetGlobalTexture("_GBuffer2", normalAry);

先說前兩行的部分,Unity官方文件明寫如果走HDR Rendering(一般來說都會開啟),就不會特別去生成emission buffer,而是直接用camera目標。

所以這邊用兩個自製shader: ResolveAAResolveAADepth,把我們的multisample emissiondepth,給blitCameraTarget就可以了。

For裡面的東西,稍微複雜一點,diffusespecularnormal這三個計算光源的buffer,另外去建立一個texture array,數量為msaa的數量。

multisample RT設定給TransferAA shader,然後再用blit轉移各個subsample pixeltexture array裡。

為何這麼麻煩?
好問題,因為我是想直接在UnityInternal-DeferredShading裡面,直接使用Texture2DMS來做取值,結果因為Unity的關係沒有辦法這樣綁。
所以就改成,先過一個passTexture2DMS的值讀出來存到texturearray裡。

不能一樣使用ResolveAA的方式來處理dsn這三個buffer?
ResolveAA是直接平均數值,但是光源計算如果直接平均normal會導致邊緣出現奇怪的亮點,因此多數引擎的GBuffer Resolve都是在lighting pass時一併處理的。
認命點,到lighting pass時處理吧!

Shader部分

ResolveAA(4x為例):
Texture2DMS<float4, 4> _MsaaTex_4X;
float4 Resolve4X(v2f i)
{
float4 col = 0;
float4 skyColor = tex2D(_SkyTextureForResolve, i.uv);
  [unroll]
for(uint a = 0; a < 4; a++)
   {
       float4 data = _MsaaTex_4X.Load(i.vertex.xy, a);
   data = lerp(data, skyColor, data.a < 0);
   col += data;
   }
col /= _MsaaFactor;
return col;
}

首先丟進來的texture要宣告為Texture2DMS,這樣才能使用Load來做resolve
取值之後直接相加平均即可。


但是中間會去檢查skyColor是多少,這時剛剛在底層設定的-1就發揮作用了,因為GBuffer必定是用黑色來做clear(不讓沒物件的地方有計算),這時如果放著邊緣黑色不管,邊邊平均後是會出現明顯黑邊的,所以當沒有資料的時候(-1),就要取skyColor(或背景色)來做平均。 

SkyTextureForResolve的生成很簡單,只要根據相機設定來下GL.Clear就可以了。

void OnPreCull()  
{
        Graphics.SetRenderTarget(skyTexture);
        if (attachedCam.clearFlags == CameraClearFlags.Skybox)
        {
            GL.ClearWithSkybox(false, attachedCam);
        }
        else
        {
            GL.Clear(false, true, attachedCam.backgroundColor);
        }
        Graphics.SetRenderTarget(null);
}

ResolveAADepth (4X為例):

Stencil
{
    Ref 192
    Comp always
    Pass replace
}       
 
Texture2DMS<float, 4> _MsaaTex_4X;
float Resolve4X(v2f i) { float col = 1; [unroll] float baseCol = _MsaaTex_4X.Load(i.vertex.xy, 0).r; for (uint a = 0; a < 4; a++) { float depth = _MsaaTex_4X.Load(i.vertex.xy, a).r; col = min(depth, col); baseCol = max(depth, baseCol); } col = lerp(col, baseCol, col == 0.0f); return col; } float frag(v2f i, out float oDepth : SV_Depth) : SV_Target { float col = 1; [branch] if (_MsaaFactor == 2) col = Resolve2X(i); else if (_MsaaFactor == 4) col = Resolve4X(i); else if (_MsaaFactor == 8) col = Resolve8X(i); oDepth = col; return col; }

大同小異,但是最後在fragment shader這邊必須指定SV_Depth,這樣才可以輸出深度值 (ZWrite ON!)

Resolve的部分,不再是平均法了,這是因為depth平均會是很奇怪的事情。
一般來說都是根據用途使用minmax來處理。

這邊的處理是預設1(reverse-z最近的深度),再在所有msaa sample裡面選出最小值(reverse-z最遠的深度),作為數值,而如果選出來是0,代表沒有物件(邊緣之處),設定為最遠的深度。

不反過來選最大是怕深度值極端變化時,會把原本的值取代掉成比較近的深度(本來不會亂擋別人的物件開始會亂擋別人。)

強制寫入Stencil Buffer的用處?
眼尖的人會發現,怎麼有個寫入stencil buffer的部分?
其實stencilUnityDeferred Shading時,用來做光源處理的參考。
沒有stencil數值,畫面就完全不受光。(Unity預設寫入192)

因為stencil的部分是沒有辦法複製的 (copyresource()不能從multisample的資源複製到non-multisample資源,resolveresouece()也不適用D32_S8格式)

SV_StencilRef雖然可以輸出stencil但是它是D3D12D3D11.3以上的功能。
在這個情況下,我們可以為stencil建立SRV,丟到shader內藉由SV_StencilRef輸出。

可惜這是D3D11,所以只有這個方式能輸出stencil,輸出255讓它鐵定計算光源。

TransferAA:
Texture2DMS<float4> _MsaaTex;
uint _TransferAAIndex;

float4 frag (v2f i) : SV_Target
{
    return _MsaaTex.Load(i.vertex.xy, _TransferAAIndex);
}       

最簡單的部分,只要將選擇到的sample index輸出到texturearray就行了。

Internel-DeferredShading:

[branch]
if (_MsaaFactor > 1)
{
   for (uint a = 0; a < _MsaaFactor; a++)
   {
     gbuffer0 = _GBuffer0.Load(uint4(uv * _ScreenParams.xy, a, 0));
     gbuffer1 = _GBuffer1.Load(uint4(uv * _ScreenParams.xy, a, 0));
     gbuffer2 = _GBuffer2.Load(uint4(uv * _ScreenParams.xy, a, 0));
     UnityStandardData data = UnityStandardDataFromGbuffer(gbuffer0, gbuffer1, gbuffer2);

     float3 eyeVec = normalize(wpos - _WorldSpaceCameraPos);
     half oneMinusReflectivity = 1 - SpecularStrength(data.specularColor.rgb);

     UnityIndirect ind;
     UNITY_INITIALIZE_OUTPUT(UnityIndirect, ind);
     ind.diffuse = 0;
     ind.specular = 0;

     col += UNITY_BRDF_PBS(data.diffuseColor, data.specularColor, oneMinusReflectivity, data.smoothness, data.normalWorld, -eyeVec, light, ind);
    }
    col /= _MsaaFactor;
   }
else
{
    UnityStandardData data = UnityStandardDataFromGbuffer(gbuffer0, gbuffer1, gbuffer2);

    float3 eyeVec = normalize(wpos - _WorldSpaceCameraPos);
    half oneMinusReflectivity = 1 - SpecularStrength(data.specularColor.rgb);

    UnityIndirect ind;
    UNITY_INITIALIZE_OUTPUT(UnityIndirect, ind);
    ind.diffuse = 0;
    ind.specular = 0;

    col = UNITY_BRDF_PBS(data.diffuseColor, data.specularColor, oneMinusReflectivity, data.smoothness, data.normalWorld, -eyeVec, light, ind);
}       

其實就只是根據msaa參數去改變行為,沒有msaa就照常,有的話就從我們丟進去的texturearray取值計算。

結果

先來個簡單的場景: 只有地板、球球、方塊。
放大後看,確實是上了MSAA,與Forward AA相差無幾




再來看看UnityDemo場景維京村落(後製先暫時全關了):




獲得了不錯的表現。

效能方面,在1070 ti上面8x大概1ms上下,平常2x4x在用效能就可以了。


缺點

l   這邊沒去修改Deferred Reflection,所以用這個方法會沒有反射,所以必須把GraphicsSettings.DeferredReflectionshader設定為No Support(也就是不支援),這樣還是會計算反射顏色出來,只是不走DeferredReflection。如果堅持要DeferredReflection,就要把GBuffer再丟進去並修改shader

l   光源的Culling Mask將會失效(只剩everything有用),前面說過Unity利用stencil buffer來做光源culling。但是要從multisamplestencil buffer resolveunity的,基本上是無法做,stencil要寫入值必須使用reference value,而且這個數值只能常數設定,沒有辦法取另外一張stencil的來用。只有copyresource能完全複製,不過在multisample的狀況下也無法使用。

l   GPU VRAM增加了,因為除了內建GBuffer以外我又額外建立multisample target,然後透明物件一樣無法抗鋸齒。
因為透明物件的繪畫是在GBuffer組合之後,使用Forward Rendering畫的)
簡單的解法就是透過後製AA加強透明物件的部分,複雜的解法就是把透明物件再畫到一張multisample  texture,然後再resolve,不過就要另外寫shader將transparent buffer跟主畫面合理融合(手動blending)了,一般以簡單解法為居多。


如果能接受這幾個問題,那其實效果還是不錯的。



參考資料

Renderdoc (根本文無關,但是這個工具的極度方便,能解析一個遊戲的frame背後是怎麼下繪圖指令的)