主頁 >  其他 > DirectX11 With Windows SDK--29 計算著色器:記憶體模型、執行緒同步;實作順序無關透明度(OIT)

DirectX11 With Windows SDK--29 計算著色器:記憶體模型、執行緒同步;實作順序無關透明度(OIT)

2020-09-11 10:23:28 其他

前言

由于透明混合在不同的繪制順序下結果會不同,這就要求繪制前要對物體進行排序,然后再從后往前渲染,但即便是僅渲染一個物體(如上一章的水波),也會出現透明繪制順序不對的情況,普通的繪制是無法避免的,如果要追求正確的效果,就需要對每個像素位置對所有的像素按深度值進行排序,本章將介紹一種僅DirectX11及更高版本才能實作的順序無關的透明度(Order-Independent Transparency,OIT),雖然它是用像素著色器來實作的,但是用到了計算著色器里面的一些相關知識,

這一章綜合性很強,在學習本章之前需要先了解如下內容:

章節內容
11 混合狀態
12 深度/模板狀態、平面鏡反射繪制(僅深度/模板狀態)
14 深度測驗
24 Render-To-Texture(RTT)技術的應用
28 計算著色器:波浪(水波)
深入理解與使用緩沖區資源(結構化緩沖區、位元組地址緩沖區)

學習目標:

  1. 熟悉記憶體模型、執行緒同步
  2. 熟悉順序無關透明度

DirectX11 With Windows SDK完整目錄

Github專案原始碼

歡迎加入QQ群: 727623616 可以一起探討DX11,以及有什么問題也可以在這里匯報,

DirectCompute 記憶體模型

DirectCompute提供了三種記憶體模型:基于暫存器的記憶體設備記憶體組內共享記憶體,不同的記憶體模型在記憶體大小、速度、訪問方式等地方有所區別,

基于暫存器的記憶體:它的訪問速度非常快,但是暫存器不僅有數目限制,暫存器指向的資源內部也有大小限制,如紋理暫存器(t#),常量緩沖區暫存器(b#),無序訪問視圖暫存器(u#),臨時暫存器(r#或x#)等,而且我們在使用暫存器時也不是直接指定某一個暫存器來使用,而是通過著色器物件(例如tbuffer,它是在GPU內部的記憶體,因此訪問速度特別快)對應到某一暫存器,然后使用該著色器物件來間接使用該暫存器的,而且這些暫存器是隨著著色器編譯出來后定死了的,因此暫存器的可用情況取決于當前使用的著色器代碼,

下面的代碼展示了如何宣告一個基于暫存器的記憶體:

tbuffer tb : register(t0)
{
    float weight[256];        // 可以從CPU更新,只讀
}

設備記憶體:通常指的是D3D設備創建出來的資源(如紋理、緩沖區),這些資源可以長期存在,只要參考計數不為0,你可以給這些資源創建很大的記憶體空間來使用,并且你還可以將它們作為著色器資源或者無序訪問資源系結到暫存器中供使用,當然,這種作為著色器資源的訪問速度還是沒有直接在暫存器上創建的記憶體物件來得快,因為它是存盤在GPU外部的顯存中,盡管這些記憶體可以通過非常高的內部帶寬來訪問,但是在請求值和回傳值之間也有一個相對較高的延遲,盡管無序訪問視圖可以用于在設備記憶體中實作與基于暫存器的記憶體相同的操作,但當執行頻繁的讀寫操作時,性能將會收到嚴重影響,此外,由于每個執行緒都可以通過無序訪問視圖讀取或寫入資源中的任何位置,這需要手動同步對資源的訪問,也可以使用原子操作,又或者定義一個合理的訪問方式避免出現多個執行緒訪問到設備記憶體的同一個資料,

組內共享記憶體:前面兩種記憶體模型是所有可編程著色階段都可使用的,但是group shared memory只能在計算著色器使用,它的訪問速度比設備記憶體資源快些,比暫存器慢,但是也有明顯的記憶體限制——每個執行緒組最多只能分配32KB記憶體,供內部所有執行緒使用,組內共享的記憶體必須確定執行緒將如何與記憶體互動和使用記憶體,因此它還必須同步對該記憶體的訪問,這將取決于正在實作的演算法,但它通常涉及到前面描述的執行緒尋址,

這三種型別的記憶體提供了不同的訪問速度和可用的大小,使得它們可以用于與其能力相匹配的不同情況,這也給計算著色器提供了更大的記憶體操作靈活性,下表則是對記憶體模型的總結:

記憶體模型 訪問速度 可用記憶體 使用方式
基于暫存器的記憶體 很快 宣告暫存器記憶體物件、全域變數
設備記憶體 通過特定視圖系結到渲染管線
組內共享記憶體 較快 較小 僅支持計算著色器,在全域變數前面加groupshared

執行緒同步

由于大量執行緒同時運行,并且執行緒能夠通過組內共享記憶體或通過無序訪問視圖對應的資源進行互動,因此需要能夠同步執行緒之間的記憶體訪問,與傳統的多執行緒編程一樣,許多執行緒可用讀取和寫入相同的記憶體位置,存在寫后讀(Read After Write,簡稱RAW)導致記憶體損壞的危險,如何在不損失GPU并行性帶來的性能的情況下還能夠高效地同步這么多執行緒?幸運的是,有幾種不同的機制可用用于同步執行緒組內的執行緒,

記憶體屏障(Memory Barriers)

這是一種最高級的同步技術,HLSL提供了許多內置函式,可用于同步執行緒組中所有執行緒的記憶體訪問,需要注意的是,它只同步執行緒組中的執行緒,而不是整個調度,這些函式有兩個不同的屬性,第一個是呼叫函式時執行緒正在同步的記憶體類別(設備記憶體、組內共享記憶體,還是兩者都有),第二個則指定給定執行緒組中的所有執行緒是否同步到其執行程序中的同一處,根據這兩個屬性,衍生出了下面這些不同版本的內置函式:

不帶組內同步 帶組內同步
GroupMemoryBarrier GroupMemoryBarrierWithGroupSync
DeviceMemoryBarrier DeviceMemoryBarrierWithGroupSync
AllMemoryBarrier AllMemoryBarrierWithGroupSync

這些函式中都會阻止執行緒繼續,直到滿足該函式的特定條件位置,其中第一個函式GroupMemoryBarrior()阻塞執行緒的執行,直到執行緒組中的所有執行緒對組內共享記憶體的所有寫入都完成,這用于確保當執行緒在組內共享記憶體中彼此共享資料時,所需的值在被其他執行緒讀取之前有機會寫入組內共享記憶體,這里有一個很重要的區別,即著色器核心執行一個寫指令,而那個指令實際上是由GPU的記憶體系統執行的,并且寫入記憶體中,然后在記憶體中它將再次對其他執行緒可用,從開始寫入值到完成寫入到目標位置有一個可變的時間量,這取決于硬體實作,通過執行阻塞操作,直到這些寫操作被保證已經完成,開發人員可以確定不會有任何寫后讀錯誤引發的問題,

不過話說了那么多,總得實踐一下,個人將雙調排序專案中BitonicSort_CS.hlsl第15行的GroupMemoryBarrierWithGroupSync()修改為GroupMemoryBarrier(),執行后發現多次運行程式會出現一例排序結果不一致的情況,因此可以這樣判斷:GroupMemoryBarrier()僅在執行緒組內的所有執行緒組存在執行緒寫入操作時阻塞,因此可能會出現阻塞結束時絕大多數執行緒完成了共享資料寫入,但仍有少量執行緒甚至還沒開始寫入共享資料,因此實際上很少能夠見到他出場的機會,

然后是GroupMemoryBarriorWithGroupSync()函式,相比上一個函式,他還阻止那些先到該函式的執行緒執行,直到所有的執行緒都到達該函式才能繼續,很明顯,在所有組內共享記憶體都加載之前,我們不希望任何執行緒前進,這使它成為完美的同步方法,

而第二對同步函式也執行類似的操作,只不過它們是在設備記憶體池上操作,這意味著在繼續執行著色器程式前,可以同步通過無序訪問視圖寫入資源的所有掛起記憶體的寫入操作,這對于同步更大數目的記憶體更為有用,如果所需的共享存盤器的大小太大不適合用組內共享記憶體,則可以將資料存在更大的設備記憶體的資源中,

第三對同步函式則是一起執行前面兩種型別的同步,用于同時存在共享記憶體和設備記憶體的訪問和同步上,

原子操作

記憶體屏障對于同步執行緒中的所有執行緒非常有用,然而,在許多情況下,還需要較小規模的同步,可能一次只需要幾個執行緒,在其他情況下,執行緒應該同步的位置可能在同一個執行點,也可能不在同一個執行點(例如,當執行緒組中的不同執行緒執行異構任務時),Shader Model 5引入了許多新的原子操作,可以在執行緒之間提供更細力度的同步,這樣在多執行緒訪問共享資源時,能夠確保所有其他執行緒都不能在統一時間訪問相同資源,原子操作保證該操作一旦開始,就一直運行到結束:

原子操作
InterlockedAdd
InterlockedMin
InterlockedMax
InterlockedOr
InterlockedAnd
InterlockedXor
InterlockedCompareStore
InterlockedCompareExchange
InterlockedExchange

原子操作也可以用于組內共享記憶體和資源記憶體,這里舉個使用的例子,如果計算著色器程式希望保留遇到特定資料值的執行緒數的計數,那么總計數可以初始化為0,并且每個執行緒可以在組內共享記憶體(以最快的訪問速度)或資源(在調度呼叫之間持續存在)上執行InterLockedAdd函式,這些原子操作確保總計數正確遞增,而不會被不同執行緒重寫中間值,

每個函式都有其獨特的輸入要求和操作,因此在選擇合適的函式時應參考Direct3D 11檔案,像素著色階段也可以使用這些函式,允許它跨資源同步(注意像素著色器不支持組內共享記憶體),

順序無關透明度

現在讓我們再回顧一下正確的透明計演算法,對每一個像素來說,若當前的背景色為\(c_0\),然后待渲染的透明像素片元按深度值從大到小排序為\(c_1, c_2, ..., c_n\),透明度為\(a_1, a_2, ..., a_n\)則最終的像素顏色為:

\[c=[a_n c_n + (1 - a_n)...[a_2 c_2 + (1 - a_2)[a_1 c_1 + (1 - a_1)c_0]...] \]

在以往的繪制方式,我們無法控制透明像素片元的繪制順序,運氣好的話還能正確呈現,一旦換了視角就會出現問題,要是場景里各種透明物體交錯在一起,基本上無論你怎么換視角都無法呈現正確的混合效果,因此為了實作順序無關透明度,我們需要預先收集這些像素,然后再進行深度排序,最后再計算出正確的像素顏色,

逐像素使用鏈表(Per-Pixel Linked Lists)

Direct3D 11硬體為許多新的渲染演算法打開了大門,尤其是對PS寫入UAV、附著在Buffer的原子計數器的支持,為Per-Pixel Linked Lists帶來了可能,它可以實作諸如OIT,間接陰影,動態場景的光線追蹤等,

但是,由于著色器只有按值傳遞,沒有指標和參考,在GPU是做不到使用基于指標或參考的鏈表的,為此,我們使用的資料結構是靜態鏈表,它可以在陣列中實作,原本作為next的指標則變成了下一個元素的索引值,

因為陣列是一個連續的記憶體區域,我們還可以在一個陣列中,存放多條靜態鏈表(只要空間足夠大),基于這個思想,我們可以為每個像素創建一個鏈表,用來收集對應螢屏像素位置的待渲染的所有像素片元,

該演算法需要歷經兩個步驟:

  1. 創建靜態鏈表,通過像素著色器,利用類似頭插法的思想在一個大陣列中逐漸形成靜態鏈表,
  2. 利用靜態鏈表渲染,通過計算著色器,取出當前像素對應的鏈表元素,進行排序,然后將計算結果寫入到渲染目標,

創建靜態鏈表

首先需要強調的是,這一步需要的是像素著色器5.0而不是計算著色器5.0,因為這一步實際上是把原本要繪制到渲染目標的這些像素片元給攔截下來,放到靜態鏈表當中,而要寫入Buffer就需要允許像素著色器支持無序訪問視圖(UAV),只有5.0及更高的版本才能這樣做,

此外,我們還需要原子操作的支持,這也需要使用著色器模型5.0,

最后我們還需要創建兩個支持讀/寫的緩沖區,用于系結到無序訪問視圖:

  1. 片元/鏈接緩沖區:該緩沖區存放的是片元資料和指向下一個元素的索引(即鏈接),并且由于它需要承擔所有像素的靜態鏈表,需要預留足夠大的空間來存放這些片元(元素數目通常為渲染目標像素總數的數倍),因此該演算法的一個主要開銷是GPU記憶體空間,其次,片元/鏈接緩沖區必須要使用結構化緩沖區,而不是多個有型別的緩沖區,因為只有RWStructuredBuffer才能夠開啟隱藏的計數器,而這個計數器又是實作靜態鏈表必不可少的一部分,它用于統計緩沖區已經存放的鏈表節點數目,
  2. 首節點偏移緩沖區:該緩沖區的寬度與高度渲染目標的一致,存放的元素是對應渲染目標像素在片元/鏈接緩沖區對應的靜態鏈表的首節點偏移,而且由于采用的是頭插法,指向的通常是當前像素最后一個寫入的片元位置,在使用之前我們需要定義-1(若是uint則為0xFFFFFFFF)為達到鏈表末端,因此每次使用之前都需要初始化鏈接值為-1.該緩沖區使用的是RWByteAddressBuffer,因為它能夠支持原子操作

下圖展示了通過像素著色器創建靜態鏈表的程序:

看完這個動圖后其實應該基本上能理解了,可能你的腦海里已經有了初步的代碼構造,但現在還是需要跟著現有的代碼學習才能實作,

首先放出實作該效果需要用到的常量緩沖區、結構體和函式:

// OIT.hlsli

cbuffer CBFrame : register(b6)
{
    uint g_FrameWidth;        // 幀像素寬度
    uint g_FrameHeight;        // 幀像素高度
    uint2 g_Pad2;
}

struct FragmentData
{
    uint Color;                // 打包為R8G8B8A8的像素顏色
    float Depth;            // 深度值
};

struct FLStaticNode
{
    FragmentData Data;        // 像素片元資料
    uint Next;                // 下一個節點的索引
};

// 打包顏色
uint PackColorFromFloat4(float4 color)
{
    uint4 colorUInt4 = (uint4) (color * 255.0f);
    return colorUInt4.r | (colorUInt4.g << 8) | (colorUInt4.b << 16) | (colorUInt4.a << 24);
}

// 解包顏色
float4 UnpackColorFromUInt(uint color)
{
    uint4 colorUInt4 = uint4(color, color >> 8, color >> 16, color >> 24) & (0x000000FF);
    return (float4) colorUInt4 / 255.0f;
}

一個像素顏色的型別為float4,要是用它作為資料存盤到緩沖區會特別消耗顯存,因為最終顯示到后備緩沖區的型別為R8G8B8A8_UNORMB8G8R8A8_UNORM,要是能夠將其打包成uint型,就可以節省這部分記憶體到原來的1/4,

當然,更狠的做法是,如果已知所有透明物體的Alpha值相同(都為0.5),那我們又可以將顏色壓縮成R5G6B5_UNORM,然后再把深度值壓縮成16為規格化浮點數,這樣一個像素只需要一半的記憶體空間就能夠表達了,當然代價為:顏色和深度都是有損的,

接下來是用于存盤像素片元的著色器:

#include "Basic.hlsli"
#include "OIT.hlsli"

RWStructuredBuffer<FLStaticNode> g_FLBuffer : register(u1);
RWByteAddressBuffer g_StartOffsetBuffer : register(u2);

// 靜態鏈表創建
// 提前開啟深度/模板測驗,避免產生不符合深度的像素片元的節點
[earlydepthstencil]
void PS(VertexPosHWNormalTex pIn)
{
    // 省略常規的光照部分,最終計算得到的光照顏色為litColor
    // ...
    
    // 取得當前像素數目并自遞增計數器
    uint pixelCount = g_FLBuffer.IncrementCounter();
    
    // 在StartOffsetBuffer實作值交換
    uint2 vPos = (uint2) pIn.PosH.xy;  
    uint startOffsetAddress = 4 * (g_FrameWidth * vPos.y + vPos.x);
    uint oldStartOffset;
    g_StartOffsetBuffer.InterlockedExchange(
        startOffsetAddress, pixelCount, oldStartOffset);
    
    // 向片元/鏈接緩沖區添加新的節點
    FLStaticNode node;
    // 壓縮顏色為R8G8B8A8
    node.Data.Color = PackColorFromFloat4(litColor);
    node.Data.Depth = pIn.PosH.z;
    node.Next = oldStartOffset;
    
    g_FLBuffer[pixelCount] = node;
}

這里面多了許多有趣的部分,需要逐一仔細講解一番,

首先是UAV暫存器,這里要先留個印象,暫存器索引初值不能從0開始,具體的原因要留到講C++的某個API時才能說的明白,

來到PS,我們也可以給像素著色器添加屬性,就像上面的[earlydepthstencil]那樣,因為在繪制透明物體之前我們已經繪制了不透明的物體,而不透明的物體會阻擋它后面的透明像素片元,雖然一般情況下深度測驗是在像素著色器之后,但也希望能拒絕掉那些被遮擋的像素片元寫入到片元/鏈接緩沖區種,因此我們可以使用屬性[earlydepthstencil],把深度/模板測驗提前到光柵化后,像素著色階段之前,這樣就可以有效剔除被遮擋的像素,既減小了性能開銷,又保證了渲染的正確,

然后是RWStructuredBuffer特有的方法IncrementCounter,它會回傳當前的計數值,并給計數器+1.與之對應的逆操作為DecrementCounter,它也屬于原子操作,因為涉及到大量的執行緒要訪問一個計數器,必須要有相應的同步操作才能保證一個時刻只有一個執行緒訪問該計數器,從而確保安全性,

這里又要再提一遍SV_POSITION,在作為頂點著色器的輸出時,提供的是未經過透視除法的NDC坐標;而作為像素著色器的輸入時,它歷經了透視除法、視口變換,得到的是對應像素的坐標值,比如說第233行,154列的像素對應的xy坐標為(232.5, 153.5),拋棄小數部分正好可以用作同寬高紋理相同位置的索引,

緊接著是RWByteAddressBufferInterlockedExchange方法:

void InterlockedExchange(
  in  uint dest,            // 目的地址
  in  uint value,            // 要交換的值
  out uint original_value    // 取出來的原值
);

你可以將其看作是一個寫入緩沖區的函式,同時它又吐出原來存盤的值,唯一要注意的是一切RWByteAddressBuffer的原子操作的地址值必須為4的倍數,因為它的讀寫單位都是32位的uint

實際渲染階段

現在我們需要讓片元/鏈接緩沖區和首節點偏移緩沖區都作為著色器資源,因為還需要準備一個存放渲染了場景中不透明物體的背景圖作為混合初值,同時又還要將結果寫入到渲染目標,這樣的話我們還需要用到TextureRender類,存放與后備緩沖區等寬高的紋理,然后將場景中不透明的物體都渲染到此處,

對于頂點著色器來說,因為是渲染整個視窗,可以直接傳頂點:

// OIT_Render_VS.hlsl
#include "OIT.hlsli"

// 頂點著色器
float4 VS(float3 vPos : POSITION) : SV_Position
{
    return float4(vPos, 1.0f);
}

而到了像素著色器,我們需要對當前像素對應的鏈表進行深度排序,由于訪問設備記憶體的效率相對較低,而且排序又涉及到頻繁的記憶體操作,在UAV進行鏈表排序的效率會很低,更好的做法是將所有像素拷貝到臨時暫存器陣列,然后再做排序,這樣效率會更高,其實也就是在像素著色器開辟一個全域靜態陣列來存放這些鏈表節點的元素,由于是靜態陣列,陣列元素固定,開辟較大的空間并不是一個比較好的選擇,這不僅涉及到排序的復雜程度,還涉及到顯存開銷,因此我們需要限制排序的像素片元數目,同時也意味著只需要讀取鏈表的前面幾個元素即可,這是一種比較折中的做法,

由于排序演算法的好壞也會影響最終的效率,對于小規模的排序,可以使用插入排序,它不僅是原址操作,對于已經有序的序列不會有多余的交換操作,又因為是執行緒內的排序,不能使用雙調排序,

像素著色器的代碼如下:

// OIT_Render_PS.hlsl
#include "OIT.hlsli"

StructuredBuffer<FLStaticNode> g_FLBuffer : register(t0);
ByteAddressBuffer g_StartOffsetBuffer : register(t1);
Texture2D g_BackGround : register(t2);

#define MAX_SORTED_PIXELS 8

static FragmentData g_SortedPixels[MAX_SORTED_PIXELS];

// 使用插入排序,深度值從大到小
void SortPixelInPlace(int numPixels)
{
    FragmentData temp;
    for (int i = 1; i < numPixels; ++i)
    {
        for (int j = i - 1; j >= 0; --j)
        {
            if (g_SortedPixels[j].Depth < g_SortedPixels[j + 1].Depth)
            {
                temp = g_SortedPixels[j];
                g_SortedPixels[j] = g_SortedPixels[j + 1];
                g_SortedPixels[j + 1] = temp;
            }
            else
            {
                break;
            }
        }
    }
}



float4 PS(float4 posH : SV_Position) : SV_Target
{
    // 取出當前像素位置對應的背景色
    float4 currColor = g_BackGround.Load(int3(posH.xy, 0));
    
    // 取出當前像素位置鏈表長度
    uint2 vPos = (uint2) posH.xy;
    int startOffsetAddress = 4 * (g_FrameWidth * vPos.y + vPos.x);
    int numPixels = 0;
    uint offset = g_StartOffsetBuffer.Load(startOffsetAddress);
    
    FLStaticNode element;
    
    // 取出鏈表所有節點
    while (offset != 0xFFFFFFFF)
    {
        // 按當前索引取出像素
        element = g_FLBuffer[offset];
        // 將像素拷貝到臨時陣列
        g_SortedPixels[numPixels++] = element.Data;
        // 取出下一個節點的索引,但最多只取出前MAX_SORTED_PIXELS個
        offset = (numPixels >= MAX_SORTED_PIXELS) ?
            0xFFFFFFFF : element.Next;
    }
    
    // 對所有取出的像素片元按深度值從大到小排序
    SortPixelInPlace(numPixels);
    
    // 使用SrcAlpha-InvSrcAlpha混合
    for (int i = 0; i < numPixels; ++i)
    {
        // 將打包的顏色解包出來
        float4 pixelColor = UnpackColorFromUInt(g_SortedPixels[i].Color);
        // 進行混合
        currColor.xyz = lerp(currColor.xyz, pixelColor.xyz, pixelColor.w);
    }
    
    // 回傳手工混合的顏色
    return currColor;
}

HLSL部分結束了,但C++端還有很多棘手的問題要處理,

OITRender類

在進行OIT像素收集時,需要通過替換像素著色器的手段來完成,因此它需要依附于BasicEffect,不好作為一個獨立的Effect使用,在此先放出OITRender類的定義:

class OITRender
{
public:
    template<class T>
    using ComPtr = Microsoft::WRL::ComPtr<T>;

    OITRender() = default;
    ~OITRender() = default;
    // 不允許拷貝,允許移動
    OITRender(const OITRender&) = delete;
    OITRender& operator=(const OITRender&) = delete;
    OITRender(OITRender&&) = default;
    OITRender& operator=(OITRender&&) = default;

    HRESULT InitResource(ID3D11Device* device, 
        UINT width,            // 幀寬度
        UINT height,        // 幀高度
        UINT multiple = 1); // 用多少倍于幀像素數的緩沖區存盤像素片元

    // 開始收集透明物體像素片元
    void BeginDefaultStore(ID3D11DeviceContext* deviceContext);
    // 結束收集,還原狀態
    void EndStore(ID3D11DeviceContext* deviceContext);
    
    // 將背景與透明物體像素片元混合完成最終渲染
    void Draw(ID3D11DeviceContext * deviceContext, ID3D11ShaderResourceView* background);

    void SetDebugObjectName(const std::string& name);

private:
    struct {
        int width;
        int height;
        int pad1;
        int pad2;
    } m_CBFrame;                                                // 對應OIT.hlsli的常量緩沖區
private:
    ComPtr<ID3D11InputLayout> m_pInputLayout;                    // 繪制螢屏的頂點輸入布局

    ComPtr<ID3D11Buffer> m_pFLBuffer;                            // 片元/鏈接緩沖區
    ComPtr<ID3D11Buffer> m_pStartOffsetBuffer;                    // 起始偏移緩沖區
    ComPtr<ID3D11Buffer> m_pVertexBuffer;                        // 繪制背景用的頂點緩沖區
    ComPtr<ID3D11Buffer> m_pIndexBuffer;                        // 繪制背景用的索引緩沖區
    ComPtr<ID3D11Buffer> m_pConstantBuffer;                        // 常量緩沖區

    ComPtr<ID3D11ShaderResourceView> m_pFLBufferSRV;            // 片元/鏈接緩沖區的著色器資源視圖
    ComPtr<ID3D11ShaderResourceView> m_pStartOffsetBufferSRV;    // 起始偏移緩沖區的著色器資源視圖

    ComPtr<ID3D11UnorderedAccessView> m_pFLBufferUAV;            // 片元/鏈接緩沖區的無序訪問視圖
    ComPtr<ID3D11UnorderedAccessView> m_pStartOffsetBufferUAV;    // 起始偏移緩沖區的無序訪問視圖

    ComPtr<ID3D11VertexShader> m_pOITRenderVS;                    // 透明混合渲染的頂點著色器
    ComPtr<ID3D11PixelShader> m_pOITRenderPS;                    // 透明混合渲染的像素著色器
    ComPtr<ID3D11PixelShader> m_pOITStorePS;                    // 用于存盤透明像素片元的像素著色器
    
    ComPtr<ID3D11PixelShader> m_pCachePS;                        // 臨時快取的像素著色器

    UINT m_FrameWidth;                                            // 幀像素寬度
    UINT m_FrameHeight;                                            // 幀像素高度
    UINT m_IndexCount;                                            // 繪制索引數
};

這里不放出初始化的代碼,但在呼叫初始化的時候需要注意提供合理的幀像素的倍數,若設定的太低,則緩沖區可能不足以容納透明像素片元而渲染例外,

OITRender::BeginDefaultStore方法--在默認特效下收集像素片元

不管寫什么渲染類,渲染狀態的管理是最復雜的,一處錯誤都會導致渲染結果的不理想,

該方法首先要解決兩個主要問題:UAV的初始化、系結到像素著色階段,

ID3D11DeviceContext::ClearUnorderedAccessViewUint--使用特定值/向量設定UAV初始值

void ClearUnorderedAccessViewUint(
  ID3D11UnorderedAccessView *pUnorderedAccessView,    // [In]待清空UAV
  const UINT [4]            Values                    // [In]清空值/向量
);

該方法對任何UAV都有效,它是以二進制位的形式來清空值,若為DXGI特定型別,如R16G16_UNORM,則該方法會根據Values的前兩個元素取出各自的低16位分別復制到每個陣列元素的x分量和y分量,若為原始記憶體的視圖或結構化緩沖區的視圖,則只取Values的第一個元素來復制到緩沖區的每一個4位元組內,

ID3D11DeviceContext::OMSetRenderTargetsAndUnorderedAccessViews--輸出合并階段設定渲染目標并設定UAV

既然像素著色器能夠使用UAV,一開始找了半天都沒找到ID3D11DeviceContext::PSSetUnorderedAccessViews,結果發現居然是在OM階段的函式提供UAV系結,

void ID3D11DeviceContext::OMSetRenderTargetsAndUnorderedAccessViews(
  UINT                      NumRTVs,                        // [In]渲染目標數
  ID3D11RenderTargetView    * const *ppRenderTargetViews,    // [In]渲染目標視圖陣列
  ID3D11DepthStencilView    *pDepthStencilView,                // [In]深度/模板視圖
  UINT                      UAVStartSlot,                    // [In]UAV起始槽
  UINT                      NumUAVs,                        // [In]UAV數目
  ID3D11UnorderedAccessView * const *ppUnorderedAccessViews,    // [In]無序訪問視圖陣列
  const UINT                *pUAVInitialCounts                    // [In]各個無序訪問視圖的計數器初始值
);

前三個引數和后三個引數應該都沒什么問題,但中間的那個引數是一個大坑,對于像素著色器,UAVStartSlot應當等于已經系結的渲染目標視圖數目,渲染目標和無序訪問視圖在寫入的時候共享相同的資源槽,這意味著必須為UAV指定偏移量,以便于它們放在待系結的渲染目標視圖之后的插槽中,因此在前面的HLSL代碼中,u暫存器需要從1開始就是這里來的,

注意:RTV、DSV、UAV不能獨立設定,它們都需要同時設定,

兩個系結了同一個子資源(也因此共享同一個紋理)的RTV,或者是兩個UAV,又或者是一個UAV和RTV,都會引發沖突,

OMSetRenderTargetsAndUnorderedAccessViews在以下情況才能運行正常:

NumRTVs != D3D11_KEEP_RENDER_TARGETS_AND_DEPTH_STENCILNumUAVs != D3D11_KEEP_UNORDERED_ACCESS_VIEWS時,需要滿足下面這些條件:

  • NumRTVs <= 8
  • UAVStartSlot >= NumRTVs
  • UAVStartSlot + NumUAVs <= 8
  • 所有設定的RTVs和UAVs不能有資源沖突
  • DSV的紋理必須匹配RTV的紋理(但不是相同)

NumRTVs == D3D11_KEEP_RENDER_TARGETS_AND_DEPTH_STENCIL時,說明OMSetRenderTargetsAndUnorderedAccessViews只系結UAVs,需要滿足下面這些條件:

  • UAVStartSlot + NumUAVs <= 8
  • 所有設定的UAVs不能有資源沖突

它還會解除系結下面這些東西:

  • 所有在slots >= UAVStartSlot的RTVs
  • 所有與待系結的UAVs發生資源沖突的RTVs
  • 所有當前系結的資源(SOTargets,CS UAVs, SRVs)沖突的UAVs

提供的深度/模板緩沖區會被忽略,并且已經系結的深度/模板緩沖區并沒有被卸下,

NumUAVs == D3D11_KEEP_UNORDERED_ACCESS_VIEWS時,說明OMSetRenderTargetsAndUnorderedAccessViews只系結RTVs和DSV,需要滿足下面這些條件

  • NumRTVs <= 8

  • 這些RTVs相互沒有資源沖突

  • DSV的紋理必須匹配RTV的紋理(但不是相同)

它還會解除系結下面這些東西:

  • 所有在slots < NumRTVs的UAVs

  • 所有與待系結的RTVs發生資源沖突的UAVs

  • 所有當前系結的資源(SOTargets,CS UAVs, SRVs)沖突的RTVs

    提供的UAVStartSlot忽略,

現在可以把目光放回到OITRender::BeginDefaultStore上:

void OITRender::BeginDefaultStore(ID3D11DeviceContext* deviceContext)
{
    deviceContext->RSSetState(RenderStates::RSNoCull.Get());
    
    UINT numClassInstances = 0;
    deviceContext->PSGetShader(m_pCachePS.GetAddressOf(), nullptr, &numClassInstances);
    deviceContext->PSSetShader(m_pOITStorePS.Get(), nullptr, 0);

    // 初始化UAV
    UINT magicValue[1] = { 0xFFFFFFFF };
    deviceContext->ClearUnorderedAccessViewUint(m_pFLBufferUAV.Get(), magicValue);
    deviceContext->ClearUnorderedAccessViewUint(m_pStartOffsetBufferUAV.Get(), magicValue);
    // UAV系結到像素著色階段
    ID3D11UnorderedAccessView* pUAVs[2] = { m_pFLBufferUAV.Get(), m_pStartOffsetBufferUAV.Get() };
    UINT initCounts[2] = { 0, 0 };
    deviceContext->OMSetRenderTargetsAndUnorderedAccessViews(D3D11_KEEP_RENDER_TARGETS_AND_DEPTH_STENCIL,
        nullptr, nullptr, 1, 2, pUAVs, initCounts);

    // 關閉深度寫入
    deviceContext->OMSetDepthStencilState(RenderStates::DSSNoDepthWrite.Get(), 0);
    // 設定常量緩沖區
    deviceContext->PSSetConstantBuffers(6, 1, m_pConstantBuffer.GetAddressOf());
}

上面的代碼有兩個點要特別注意:

  1. 因為是透明物體,需要關閉背面消隱
  2. 因為沒有產生實際繪制,需要關閉深度寫入

OITRender::EndStore方法--結束收集

方法如下:

void OITRender::EndStore(ID3D11DeviceContext* deviceContext)
{
    // 恢復渲染狀態
    deviceContext->PSSetShader(m_pCachePS.Get(), nullptr, 0);
    ComPtr<ID3D11RenderTargetView> currRTV;
    ComPtr<ID3D11DepthStencilView> currDSV;
    ID3D11UnorderedAccessView* pUAVs[2] = { nullptr, nullptr };
    deviceContext->OMSetRenderTargetsAndUnorderedAccessViews(D3D11_KEEP_RENDER_TARGETS_AND_DEPTH_STENCIL,
        nullptr, nullptr, 1, 2, pUAVs, nullptr);
    m_pCachePS.Reset();
}

OITRender::Draw方法--對透明像素片元進行排序混合并完成繪制

方法如下,到這一步其實已經沒那么復雜了:

void OITRender::Draw(ID3D11DeviceContext* deviceContext, ID3D11ShaderResourceView* background)
{

    UINT strides[1] = { sizeof(VertexPos) };
    UINT offsets[1] = { 0 };
    deviceContext->IASetVertexBuffers(0, 1, m_pVertexBuffer.GetAddressOf(), strides, offsets);
    deviceContext->IASetIndexBuffer(m_pIndexBuffer.Get(), DXGI_FORMAT_R32_UINT, 0);

    deviceContext->IASetInputLayout(m_pInputLayout.Get());
    deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);

    deviceContext->VSSetShader(m_pOITRenderVS.Get(), nullptr, 0);
    deviceContext->PSSetShader(m_pOITRenderPS.Get(), nullptr, 0);

    deviceContext->GSSetShader(nullptr, nullptr, 0);
    deviceContext->RSSetState(nullptr);

    ID3D11ShaderResourceView* pSRVs[3] = {
        m_pFLBufferSRV.Get(), m_pStartOffsetBufferSRV.Get(), background};
    deviceContext->PSSetShaderResources(0, 3, pSRVs);
    deviceContext->PSSetConstantBuffers(6, 1, m_pConstantBuffer.GetAddressOf());

    deviceContext->OMSetDepthStencilState(nullptr, 0);
    deviceContext->OMSetBlendState(nullptr, nullptr, 0xFFFFFFFF);

    deviceContext->DrawIndexed(m_IndexCount, 0, 0);

    // 繪制完成后卸下系結的資源即可
    pSRVs[0] = pSRVs[1] = pSRVs[2] = nullptr;
    deviceContext->PSSetShaderResources(0, 3, pSRVs);

}

場景繪制

現在場景中除了山體、波浪,還有兩個透明相交的立方體,只考慮開啟OIT的GameApp::DrawScene方法如下:

void GameApp::DrawScene()
{
    assert(m_pd3dImmediateContext);
    assert(m_pSwapChain);

    m_pd3dImmediateContext->ClearRenderTargetView(m_pRenderTargetView.Get(), reinterpret_cast<const float*>(&Colors::Silver));
    m_pd3dImmediateContext->ClearDepthStencilView(m_pDepthStencilView.Get(), D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);
    
    // 渲染到臨時背景
    m_pTextureRender->Begin(m_pd3dImmediateContext.Get(), reinterpret_cast<const float*>(&Colors::Silver));
    {
        // ******************
        // 1. 繪制不透明物件
        //
        m_BasicEffect.SetRenderDefault(m_pd3dImmediateContext.Get(), BasicEffect::RenderObject);
        m_BasicEffect.SetTexTransformMatrix(XMMatrixIdentity());
        m_Land.Draw(m_pd3dImmediateContext.Get(), m_BasicEffect);
    
        // ******************
        // 2. 存放透明物體的像素片元
        //
        m_pOITRender->BeginDefaultStore(m_pd3dImmediateContext.Get());
        {
            m_RedBox.Draw(m_pd3dImmediateContext.Get(), m_BasicEffect);
            m_YellowBox.Draw(m_pd3dImmediateContext.Get(), m_BasicEffect);
            m_pGpuWavesRender->Draw(m_pd3dImmediateContext.Get(), m_BasicEffect);
        }
        m_pOITRender->EndStore(m_pd3dImmediateContext.Get());
    }
    m_pTextureRender->End(m_pd3dImmediateContext.Get());
    
    // 渲染到后備緩沖區
    m_pOITRender->Draw(m_pd3dImmediateContext.Get(), m_pTextureRender->GetOutputTexture());

    // ******************
    // 繪制Direct2D部分
    //
    // ...

    HR(m_pSwapChain->Present(0, 0));
}

演示

下面演示了關閉OIT和深度寫入、關閉OIT但開啟深度寫入、開啟OIT下的場景渲染效果:

開啟OIT的平均幀數為2700,而默認平均幀數為4200,可見影響渲染性能的主要因素有:RTT的使用、場景中透明像素的復雜程度、排序演算法的選擇和n的限制,因此要保證渲染效率,最好是能夠減少透明物體的復雜程度、場景中透明物體的數目,必要時甚至是避免透明混合,

除錯問題

該專案無法圖形除錯,就和DirectX SDK Samples中OIT樣例一樣,遇到了未知問題,如果要除錯,需要把OIT相關的代碼撤走才能除錯,

練習題

  1. 嘗試改動HLSL代碼,將顏色壓縮成R5G6B5_UNORM(規定透明物體Alpha統一為0.5),然后再把深度值壓縮成16為規格化浮點數,同時也要改動C++端代碼來適配,

參考資料

  1. DirectX SDK Samples中的OIT
  2. OIT-and-Indirect-Illumination-using-DX11-Linked-Lists 演示檔案

DirectX11 With Windows SDK完整目錄

Github專案原始碼

歡迎加入QQ群: 727623616 可以一起探討DX11,以及有什么問題也可以在這里匯報,

轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/5306.html

標籤:其他

上一篇:【Unity|C#】基礎篇(1)——基礎入門

下一篇:Unreal Engine 4 藍圖完全學習教程(五)—— 關于陣列

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 網閘典型架構簡述

    網閘架構一般分為兩種:三主機的三系統架構網閘和雙主機的2+1架構網閘。 三主機架構分別為內端機、外端機和仲裁機。三機無論從軟體和硬體上均各自獨立。首先從硬體上來看,三機都用各自獨立的主板、記憶體及存盤設備。從軟體上來看,三機有各自獨立的作業系統。這樣能達到完全的三機獨立。對于“2+1”系統,“2”分為 ......

    uj5u.com 2020-09-10 02:00:44 more
  • 如何從xshell上傳檔案到centos linux虛擬機里

    如何從xshell上傳檔案到centos linux虛擬機里及:虛擬機CentOs下執行 yum -y install lrzsz命令,出現錯誤:鏡像無法找到軟體包 前言 一、安裝lrzsz步驟 二、上傳檔案 三、遇到的問題及解決方案 總結 前言 提示:其實很簡單,往虛擬機上安裝一個上傳檔案的工具 ......

    uj5u.com 2020-09-10 02:00:47 more
  • 一、SQLMAP入門

    一、SQLMAP入門 1、判斷是否存在注入 sqlmap.py -u 網址/id=1 id=1不可缺少。當注入點后面的引數大于兩個時。需要加雙引號, sqlmap.py -u "網址/id=1&uid=1" 2、判斷文本中的請求是否存在注入 從文本中加載http請求,SQLMAP可以從一個文本檔案中 ......

    uj5u.com 2020-09-10 02:00:50 more
  • Metasploit 簡單使用教程

    metasploit 簡單使用教程 浩先生, 2020-08-28 16:18:25 分類專欄: kail 網路安全 linux 文章標簽: linux資訊安全 編輯 著作權 metasploit 使用教程 前言 一、Metasploit是什么? 二、準備作業 三、具體步驟 前言 Msfconsole ......

    uj5u.com 2020-09-10 02:00:53 more
  • 游戲逆向之驅動層與用戶層通訊

    驅動層代碼: #pragma once #include <ntifs.h> #define add_code CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ANY_ACCESS) /* 更多游戲逆向視頻www.yxfzedu.com ......

    uj5u.com 2020-09-10 02:00:56 more
  • 北斗電力時鐘(北斗授時服務器)讓網路資料更精準

    北斗電力時鐘(北斗授時服務器)讓網路資料更精準 北斗電力時鐘(北斗授時服務器)讓網路資料更精準 京準電子科技官微——ahjzsz 近幾年,資訊技術的得了快速發展,互聯網在逐漸普及,其在人們生活和生產中都得到了廣泛應用,并且取得了不錯的應用效果。計算機網路資訊在電力系統中的應用,一方面使電力系統的運行 ......

    uj5u.com 2020-09-10 02:01:03 more
  • 【CTF】CTFHub 技能樹 彩蛋 writeup

    ?碎碎念 CTFHub:https://www.ctfhub.com/ 筆者入門CTF時時剛開始刷的是bugku的舊平臺,后來才有了CTFHub。 感覺不論是網頁UI設計,還是題目質量,賽事跟蹤,工具軟體都做得很不錯。 而且因為獨到的金幣制度的確讓人有一種想去刷題賺金幣的感覺。 個人還是非常喜歡這個 ......

    uj5u.com 2020-09-10 02:04:05 more
  • 02windows基礎操作

    我學到了一下幾點 Windows系統目錄結構與滲透的作用 常見Windows的服務詳解 Windows埠詳解 常用的Windows注冊表詳解 hacker DOS命令詳解(net user / type /md /rd/ dir /cd /net use copy、批處理 等) 利用dos命令制作 ......

    uj5u.com 2020-09-10 02:04:18 more
  • 03.Linux基礎操作

    我學到了以下幾點 01Linux系統介紹02系統安裝,密碼啊破解03Linux常用命令04LAMP 01LINUX windows: win03 8 12 16 19 配置不繁瑣 Linux:redhat,centos(紅帽社區版),Ubuntu server,suse unix:金融機構,證券,銀 ......

    uj5u.com 2020-09-10 02:04:30 more
  • 05HTML

    01HTML介紹 02頭部標簽講解03基礎標簽講解04表單標簽講解 HTML前段語言 js1.了解代碼2.根據代碼 懂得挖掘漏洞 (POST注入/XSS漏洞上傳)3.黑帽seo 白帽seo 客戶網站被黑帽植入劫持代碼如何處理4.熟悉html表單 <html><head><title>TDK標題,描述 ......

    uj5u.com 2020-09-10 02:04:36 more
最新发布
  • 2023年最新微信小程式抓包教程

    01 開門見山 隔一個月發一篇文章,不過分。 首先回顧一下《微信系結手機號資料庫被脫庫事件》,我也是第一時間得知了這個訊息,然后跟蹤了整件事情的經過。下面是這起事件的相關截圖以及近日流出的一萬條資料樣本: 個人認為這件事也沒什么,還不如關注一下之前45億快遞資料查詢渠道疑似在近日復活的訊息。 訊息是 ......

    uj5u.com 2023-04-20 08:48:24 more
  • web3 產品介紹:metamask 錢包 使用最多的瀏覽器插件錢包

    Metamask錢包是一種基于區塊鏈技術的數字貨幣錢包,它允許用戶在安全、便捷的環境下管理自己的加密資產。Metamask錢包是以太坊生態系統中最流行的錢包之一,它具有易于使用、安全性高和功能強大等優點。 本文將詳細介紹Metamask錢包的功能和使用方法。 一、 Metamask錢包的功能 數字資 ......

    uj5u.com 2023-04-20 08:47:46 more
  • vulnhub_Earth

    前言 靶機地址->>>vulnhub_Earth 攻擊機ip:192.168.20.121 靶機ip:192.168.20.122 參考文章 https://www.cnblogs.com/Jing-X/archive/2022/04/03/16097695.html https://www.cnb ......

    uj5u.com 2023-04-20 07:46:20 more
  • 從4k到42k,軟體測驗工程師的漲薪史,給我看哭了

    清明節一過,盲猜大家已經無心上班,在數著日子準備過五一,但一想到銀行卡里的余額……瞬間心情就不美麗了。最近,2023年高校畢業生就業調查顯示,本科畢業月平均起薪為5825元。調查一出,便有很多同學表示自己又被平均了。看著這一資料,不免讓人想到前不久中國青年報的一項調查:近六成大學生認為畢業10年內會 ......

    uj5u.com 2023-04-20 07:44:00 more
  • 最新版本 Stable Diffusion 開源 AI 繪畫工具之中文自動提詞篇

    🎈 標簽生成器 由于輸入正向提示詞 prompt 和反向提示詞 negative prompt 都是使用英文,所以對學習母語的我們非常不友好 使用網址:https://tinygeeker.github.io/p/ai-prompt-generator 這個網址是為了讓大家在使用 AI 繪畫的時候 ......

    uj5u.com 2023-04-20 07:43:36 more
  • 漫談前端自動化測驗演進之路及測驗工具分析

    隨著前端技術的不斷發展和應用程式的日益復雜,前端自動化測驗也在不斷演進。隨著 Web 應用程式變得越來越復雜,自動化測驗的需求也越來越高。如今,自動化測驗已經成為 Web 應用程式開發程序中不可或缺的一部分,它們可以幫助開發人員更快地發現和修復錯誤,提高應用程式的性能和可靠性。 ......

    uj5u.com 2023-04-20 07:43:16 more
  • CANN開發實踐:4個DVPP記憶體問題的典型案例解讀

    摘要:由于DVPP媒體資料處理功能對存放輸入、輸出資料的記憶體有更高的要求(例如,記憶體首地址128位元組對齊),因此需呼叫專用的記憶體申請介面,那么本期就分享幾個關于DVPP記憶體問題的典型案例,并給出原因分析及解決方法。 本文分享自華為云社區《FAQ_DVPP記憶體問題案例》,作者:昇騰CANN。 DVPP ......

    uj5u.com 2023-04-20 07:43:03 more
  • msf學習

    msf學習 以kali自帶的msf為例 一、msf核心模塊與功能 msf模塊都放在/usr/share/metasploit-framework/modules目錄下 1、auxiliary 輔助模塊,輔助滲透(埠掃描、登錄密碼爆破、漏洞驗證等) 2、encoders 編碼器模塊,主要包含各種編碼 ......

    uj5u.com 2023-04-20 07:42:59 more
  • Halcon軟體安裝與界面簡介

    1. 下載Halcon17版本到到本地 2. 雙擊安裝包后 3. 步驟如下 1.2 Halcon軟體安裝 界面分為四大塊 1. Halcon的五個助手 1) 影像采集助手:與相機連接,設定相機引數,采集影像 2) 標定助手:九點標定或是其它的標定,生成標定檔案及內參外參,可以將像素單位轉換為長度單位 ......

    uj5u.com 2023-04-20 07:42:17 more
  • 在MacOS下使用Unity3D開發游戲

    第一次發博客,先發一下我的游戲開發環境吧。 去年2月份買了一臺MacBookPro2021 M1pro(以下簡稱mbp),這一年來一直在用mbp開發游戲。我大致分享一下我的開發工具以及使用體驗。 1、Unity 官網鏈接: https://unity.cn/releases 我一般使用的Apple ......

    uj5u.com 2023-04-20 07:40:19 more