DirectX11 With Windows SDK--17 利用幾何著色器實現公告板效果

来源:https://www.cnblogs.com/X-Jun/archive/2018/08/19/9495616.html
-Advertisement-
Play Games

前言 上一章我們知道瞭如何使用幾何著色器將頂點通過流輸出階段輸出到綁定的頂點緩衝區。接下來我們繼續利用它來實現一些新的效果,在這一章,你將瞭解: 1. 實現公告板效果 2. Alpha To Coverage 3. 對GPU資源進行讀/寫操作 4. 紋理數組 5. 實現霧效 "DirectX11 W ...


前言

上一章我們知道瞭如何使用幾何著色器將頂點通過流輸出階段輸出到綁定的頂點緩衝區。接下來我們繼續利用它來實現一些新的效果,在這一章,你將瞭解:

  1. 實現公告板效果
  2. Alpha-To-Coverage
  3. 對GPU資源進行讀/寫操作
  4. 紋理數組
  5. 實現霧效

DirectX11 With Windows SDK完整目錄

Github項目源碼

實現霧效

雖然這部分與幾何著色器並沒有什麼關係,但是霧的效果在該Demo中會用到,並且前面也沒有講過這部分內容,故先在這裡提出來。

有時候我們需要在游戲中模擬一些特定的天氣條件,比如說大霧。它可以讓物體平滑出現而不是突然蹦出來那樣(物體的一部分留在視錐體內使得只能看到該部分,然後在逐漸靠近該物體的時候,該物體就像經過了一個無形的掃描門被逐漸構造出來那樣)。通過讓霧在某一範圍內具有一定的層次(讓不可見區域比視錐體裁剪區域還近),我們可以避免上面所說的情況。但即便是晴朗的天氣,你可能仍希望包含一個較廣範圍的霧效,即距離達到很遠的地方纔逐漸看不清物體。

我們可以使用這種方式來實現霧效:指定霧的顏色,以攝像機為原點的霧開始的最小距離,霧效範圍值(超過起始距離+霧效範圍值的範圍外的顏色皆被指定的霧色取代)。在需要繪製的三角形內,某一像素片元的顏色如下:
\(\begin{align} foggedColor &= litColor + s(fogColor - litColor)\\ &= (1-s) \cdot litColor + s \cdot fogColor\\ \end{align}\)

該函數對應HLSL中的lerp函數,s取0的時候最終顏色為litColor,然後逐漸增大並逼近1的時候,最終顏色就逐漸趨近於fogColor。然後參數s的值取決於下麵的函數:
\(s = saturate(\frac{dist(\mathbf{p},\mathbf{E}) - fogStart}{fogRange})\)
\(saturate(x) = \begin{cases} x, 0 \le x \le 1\\ 0, x < 0\\ 1, x > 1\\ \end{cases}\)

其中dist(p,E)指的是兩點之間的距離值。配合下麵的圖去理解:

還有註意一點,在每次清空重新繪製的時候,要用霧的顏色進行清空。

HLSL代碼

與霧效相關的值存儲在下麵的常量緩衝區中,並且繪製3D物體的頂點沒有發生變化:

// Basic.fx
// ...

cbuffer CBDrawingStates : register(b2)
{
    float4 gFogColor;
    int gFogEnabled;
    float gFogStart;
    float gFogRange;
}

// ...
struct Vertex3DIn
{
    float3 PosL : POSITION;
    float3 NormalL : NORMAL;
    float2 Tex : TEXCOORD;
};

struct Vertex3DOut
{
    float4 PosH : SV_POSITION;
    float3 PosW : POSITION; // 在世界中的位置
    float3 NormalW : NORMAL; // 法向量在世界中的方向
    float2 Tex : TEXCOORD;
};

Basic_VS_3D.hlsl也與之前一樣,沒有什麼變動:

// Basic_VS_3D.hlsl
#include "Basic.fx"

// 頂點著色器(3D)
Vertex3DOut VS_3D(Vertex3DIn pIn)
{
    Vertex3DOut pOut;
    
    row_major matrix worldViewProj = mul(mul(gWorld, gView), gProj);
    pOut.PosH = mul(float4(pIn.PosL, 1.0f), worldViewProj);
    pOut.PosW = mul(float4(pIn.PosL, 1.0f), gWorld).xyz;
    pOut.NormalW = mul(pIn.NormalL, (float3x3) gWorldInvTranspose);
    pOut.Tex = mul(float4(pIn.Tex, 0.0f, 1.0f), gTexTransform).xy;
    return pOut;
}

Basic_PS_3D.hlsl現在使用了4盞方向光以保證4種不同方向的光能夠均勻照射,並添加了霧效部分的處理:

// Basic_PS_3D.hlsl
#include "Basic.fx"

// 像素著色器(3D)
float4 PS_3D(Vertex3DOut pIn) : SV_Target
{
    // 提前進行裁剪,對不符合要求的像素可以避免後續運算
    float4 texColor = tex.Sample(sam, pIn.Tex);
    clip(texColor.a - 0.05f);

    // 標準化法向量
    pIn.NormalW = normalize(pIn.NormalW);

    // 求出頂點指向眼睛的向量,以及頂點與眼睛的距離
    float3 toEyeW = normalize(gEyePosW - pIn.PosW);
    float distToEye = distance(gEyePosW, pIn.PosW);

    // 初始化為0 
    float4 ambient = float4(0.0f, 0.0f, 0.0f, 0.0f);
    float4 diffuse = float4(0.0f, 0.0f, 0.0f, 0.0f);
    float4 spec = float4(0.0f, 0.0f, 0.0f, 0.0f);
    float4 A = float4(0.0f, 0.0f, 0.0f, 0.0f);
    float4 D = float4(0.0f, 0.0f, 0.0f, 0.0f);
    float4 S = float4(0.0f, 0.0f, 0.0f, 0.0f);

    [unroll]
    for (int i = 0; i < 4; ++i)
    {
        ComputeDirectionalLight(gMaterial, gDirLight[i], pIn.NormalW, toEyeW, A, D, S);
        ambient += A;
        diffuse += D;
        spec += S;
    }
    
    float4 litColor = texColor * (ambient + diffuse) + spec;

    // 霧效部分
    [flatten]
    if (gFogEnabled)
    {
        // 限定在0.0f到1.0f範圍
        float fogLerp = saturate((distToEye - gFogStart) / gFogRange);
        // 根據霧色和光照顏色進行線性插值
        litColor = lerp(litColor, gFogColor, fogLerp);
    }

    litColor.a = texColor.a * gMaterial.Diffuse.a;
    return litColor;
}

對於白天來說,我們可以使用RGBA=(0.75f, 0.75f, 0.75f, 1.0f)來作為霧的顏色。

而對於黑夜來說,這個霧效更像是戰爭迷霧的效果,我們使用RGBA=(0.0f, 0.0f, 0.0f, 1.0f)來作為霧的顏色,這樣遠處的物體我們就讓它看不見,而在可視範圍內,距離越遠的物體能見度越低。

具體的演示效果在最後可以看到。

樹的公告板效果

當一棵樹離攝像機太遠的話,我們可以使用公告板技術,用一張樹的貼圖來進行繪製,取代原來繪製3D樹模型的方式。首先我們給出樹的紋理貼圖組成:

關註Alpha通道部分,白色區域指代Alpha值為1.0(完全不透明),而黑色區域指代Alpha值0.0(完全透明)。所以在渲染樹紋理的時候,我們只需要對Alpha值為0.0的像素區域進行裁剪即可。

實現公告板的關鍵點在於:公告板要永遠正向攝像機(即視線要與公告板錶面垂直),使得用戶的視線在x0z面上的投影一直與貼圖錶面垂直。這樣做省去了大量頂點的輸入和處理,顯得更加高效,並且這個小技巧還能夠欺騙玩家讓人誤以為還是原來的3D模型(眼尖的玩家還是有可能認得出來),只要你別一開始就告訴人家這棵樹的繪製用了公告板原理就行了(→_→)。

現在不考慮坐標系的Y軸部分(即從上方俯視),從下麵的圖可以看到,公告板投影的中心部分的法向量是直接指向攝像機的。

因此我們可以得到公告板的u軸, v軸和w軸單位向量以及根據公告板構建的局部坐標系:
\(\mathbf{w}=\frac{(E_x-C_x,0,E_z-C_z)}{E_x-C_x,0,E_z-C_z}\)
\(\mathbf{v}=(0,1,0)\)
\(\mathbf{u}=\mathbf{v}\times\mathbf{w}\)

然後已知中心頂點位置、樹寬度和高度,就可以求得2D樹矩形的四個頂點了:

// 計算出公告板矩形的四個頂點
//            up
//       v1___|___v3
//        |   |   |
// right__|___|   |
//        |__/____|
//       v0 /     v2
//        look  
v[0] = float4(center + halfWidth * right - halfHeight * up, 1.0f);
v[1] = float4(center + halfWidth * right + halfHeight * up, 1.0f);
v[2] = float4(center - halfWidth * right - halfHeight * up, 1.0f);
v[3] = float4(center - halfWidth * right + halfHeight * up, 1.0f);

註意上面的加減運算是針對float3進行的,然後用1.0f填充成4D向量。並且由於每個公告板所處的局部坐標系不一樣,我們需要對它們分別計算出對應的坐標軸向量。

若現在我們需要繪製公告板,則在輸入的時候僅提供對應的中心頂點,然後圖元類型選擇D3D11_PRIMITIVE_TOPOLOGY_POINTLIST,在幾何著色階段我們直接將頂點直傳到幾何著色階段,這些頂點傳遞給幾何著色器後就會解釋成一個個矩形(兩個三角形),產生公告板。

HLSL代碼

下麵是Basic.fx的完整代碼:

// Basic.fx

#include "LightHelper.hlsli"

Texture2D tex : register(t0);
Texture2DArray texArray : register(t1);
SamplerState sam : register(s0);


cbuffer CBChangesEveryDrawing : register(b0)
{
    row_major matrix gWorld;
    row_major matrix gWorldInvTranspose;
    row_major matrix gTexTransform;
    Material gMaterial;
}

cbuffer CBChangesEveryFrame : register(b1)
{
    row_major matrix gView;
    float3 gEyePosW;
}

cbuffer CBDrawingStates : register(b2)
{
    float4 gFogColor;
    int gFogEnabled;
    float gFogStart;
    float gFogRange;
}

cbuffer CBChangesOnResize : register(b3)
{
    row_major matrix gProj;
}

cbuffer CBNeverChange : register(b4)
{
    DirectionalLight gDirLight[4];
}



struct Vertex3DIn
{
    float3 PosL : POSITION;
    float3 NormalL : NORMAL;
    float2 Tex : TEXCOORD;
};

struct Vertex3DOut
{
    float4 PosH : SV_POSITION;
    float3 PosW : POSITION; // 在世界中的位置
    float3 NormalW : NORMAL; // 法向量在世界中的方向
    float2 Tex : TEXCOORD;
};

struct PointSprite
{
    float3 PosW : POSITION;
    float2 SizeW : SIZE;
};

struct BillboardVertex
{
    float4 PosH : SV_POSITION;
    float3 PosW : POSITION;
    float3 NormalW : NORMAL;
    float2 Tex : TEXCOORD;
    uint PrimID : SV_PrimitiveID;
};

對於頂點著色器,僅負責頂點的直傳:

// Billboard_VS.hlsl

#include "Basic.fx"

PointSprite VS(PointSprite pIn)
{
    return pIn;
}

而幾何著色器的代碼如下:

// Billboard_GS.hlsl

#include "Basic.fx"

// 節省記憶體資源,先用float4向量聲明。
static const float4 gVec[2] = { float4(0.0f, 1.0f, 0.0f, 0.0f), float4(1.0f, 1.0f, 1.0f, 0.0f) };
static const float2 gTex[4] = (float2[4])gVec;

[maxvertexcount(4)]
void GS(point PointSprite input[1], uint primID : SV_PrimitiveID, 
    inout TriangleStream<BillboardVertex> output)
{
    // 計算公告板所處的局部坐標系,其中公告板相當於
    // 被投影在了局部坐標系的xy平面,z軸

    float3 up = float3(0.0f, 1.0f, 0.0f);
    float3 look = gEyePosW - input[0].PosW;
    look.y = 0.0f;  // look向量只取投影到xz平面的向量
    look = normalize(look);
    float3 right = cross(up, look);

    // 計算出公告板矩形的四個頂點
    //            up
    //      v1 ___|___ v3
    //        |   |   |
    // right__|___|   |
    //        |  /    |
    //        |_/_____|
    //      v0 /       v2
    //       look  
    float4 v[4];
    float3 center = input[0].PosW;
    float halfWidth = 0.5f * input[0].SizeW.x;
    float halfHeight = 0.5f * input[0].SizeW.y;
    v[0] = float4(center + halfWidth * right - halfHeight * up, 1.0f);
    v[1] = float4(center + halfWidth * right + halfHeight * up, 1.0f);
    v[2] = float4(center - halfWidth * right - halfHeight * up, 1.0f);
    v[3] = float4(center - halfWidth * right + halfHeight * up, 1.0f);

    // 對頂點位置進行矩陣變換,並以TriangleStrip形式輸出
    BillboardVertex gOut;
    row_major matrix viewProj = mul(gView, gProj);
    [unroll]
    for (int i = 0; i < 4; ++i)
    {
        gOut.PosW = v[i].xyz;
        gOut.PosH = mul(v[i], viewProj);
        gOut.NormalW = look;
        gOut.Tex = gTex[i];
        gOut.PrimID = primID;
        output.Append(gOut);
    }

}

首先一開始不用float2數組是因為每個float2元素會單獨打包,浪費了一半的空間,因此這裡採取一種特殊的語法形式使得記憶體可以得到充分利用。

然後要註意maxvertexcount的值要設為4,儘管Append的次數為4,但實際上輸出的三角形頂點數為6。

圖元ID

現在講述系統值SV_PrimitiveID,我們可以將它作為函數的額外形參進行提供。它告訴我們在輸入裝配階段下自動分配的圖元ID值。當我們調用了一個draw方法,需要繪製n個圖元,那麼第一個圖元對應的ID值為0,第二個為1,直到最後一個為n-1.當前的所有圖元ID僅在當前的單次調用繪製是唯一的。其中該系統值的寫入操作允許在幾何著色器和像素著色器進行,而讀取操作則允許在幾何/像素/外殼/域著色器中進行。

在上面的例子中,我們將一個頂點產生的矩形四個頂點都標記為同一個圖元ID,是因為到後續的像素著色器中,我們用該圖元ID映射到紋理數組的索引值,來對應到要繪製的樹的紋理。

註意: 如果幾何著色器沒有提供圖元ID,在像素著色器中也可以將它加進參數列表中以使用:

float4 PS(Vertex3DOut pin, uint primID : SV_PrimitiveID) : SV_Target
{
// Pixel shader body…
}

但如果像素著色器提供了圖元ID,渲染管線又綁定了幾何著色器,則幾何著色器必須提供該參數。在幾何著色器中你可以使用或修改圖元ID值。

頂點ID

緊接著是系統值SV_VertexID,在輸入裝配階段的時候渲染管線就會為這些輸入的頂點分配頂點ID值。若使用的是Draw方法,則這些頂點將會按順序從0到n-1被標記(n為頂點數目);若使用的是DrawIndexed方法,則頂點ID對應到的是該頂點所處的索引值。該參數僅能在頂點著色器的參數列表中提供:

VertexOut VS(VertexIn vin, uint vertID : SV_VertexID)
{
// vertex shader body…
}

最後給出像素著色器的代碼:

// Billboard_PS.hlsl

#include "Basic.fx"

float4 PS(BillboardVertex pIn) : SV_Target
{
    // 每4棵樹一個迴圈,儘量保證出現不同的樹
    float4 texColor = texArray.Sample(sam, float3(pIn.Tex, pIn.PrimID % 4));
    // 提前進行裁剪,對不符合要求的像素可以避免後續運算
    clip(texColor.a - 0.05f);

    // 標準化法向量
    pIn.NormalW = normalize(pIn.NormalW);

    // 求出頂點指向眼睛的向量,以及頂點與眼睛的距離
    float3 toEyeW = normalize(gEyePosW - pIn.PosW);
    float distToEye = distance(gEyePosW, pIn.PosW);

    // 初始化為0 
    float4 ambient = float4(0.0f, 0.0f, 0.0f, 0.0f);
    float4 diffuse = float4(0.0f, 0.0f, 0.0f, 0.0f);
    float4 spec = float4(0.0f, 0.0f, 0.0f, 0.0f);
    float4 A = float4(0.0f, 0.0f, 0.0f, 0.0f);
    float4 D = float4(0.0f, 0.0f, 0.0f, 0.0f);
    float4 S = float4(0.0f, 0.0f, 0.0f, 0.0f);

    [unroll]
    for (int i = 0; i < 4; ++i)
    {
        ComputeDirectionalLight(gMaterial, gDirLight[i], pIn.NormalW, toEyeW, A, D, S);
        ambient += A;
        diffuse += D;
        spec += S;
    }

    float4 litColor = texColor * (ambient + diffuse) + spec;

    // 霧效部分
    [flatten]
    if (gFogEnabled)
    {
        // 限定在0.0f到1.0f範圍
        float fogLerp = saturate((distToEye - gFogStart) / gFogRange);
        // 根據霧色和光照顏色進行線性插值
        litColor = lerp(litColor, gFogColor, fogLerp);
    }

    litColor.a = texColor.a * gMaterial.Diffuse.a;
    return litColor;
}

這裡加上了剛纔的霧效,並使用了紋理數組。

紋理數組

之前在C++代碼層中,我們的每一張紋理使用ID3D11Texture2D的介面對象去單獨存儲。但實際上在我們創建ID3D11Texture2D對象的時候,我們可以設置它的ArraySize來指定該對象可以存放的紋理數目。

但是我們創建紋理並不是使用D3DX系列的函數,因為我們根本就不使用DirectX SDK。在之前我們創建紋理使用的是DDSTextureLoader.hWICTextureLoader.h中的函數。這裡再提及一下,這兩個頭文件對應的庫可以在下麵兩個途徑找到:

DirectXTex

DirectXTK

回到HLSL代碼,我們之所以不使用下麵的這種形式創建紋理數組:

Texture2D TexArray[4];

float4 PS(GeoOut pin) : SV_Target
{
    float4 c = TexArray[pin.PrimID%4].Sample(samLinear, pin.Tex);

是因為這樣做的話HLSL編譯器會報錯:sampler array index must be a literal experssion,即pin.PrimID的值也必須是個字面值,而不是變數。但我們還是想要能夠根據變數取對應紋理的能力。

正確的做法應當是聲明一個Texture2DArray的數組:

Texture2DArray texArray : register(t1);

這裡使用的是索引為1的紋理寄存器是因為前面還有一個紋理已經綁定了t0.

紋理數組的採樣

Texture2DArray同樣也具有Sample方法:

// 每4棵樹一個迴圈,儘量保證出現不同的樹
float4 texColor = texArray.Sample(sam, float3(pIn.Tex, pIn.PrimID % 4));

第一個參數依然是採樣器

而第二個參數則是一個3D向量,其中x與y的值對應的還是紋理坐標,而z分量即便是個float,主要是用於作為索引值選取紋理數組中的某一個具體紋理。同理索引值0對應紋理數組的第一張紋理,1對應的是第二張紋理等等...

在我們的這個demo中,紋理數組存放了4張不同樣式的樹的紋理貼圖,然後用SV_Primitive模4的值來決定哪張樹紋理貼圖將被繪製。

使用紋理數組的優勢是,我們可以一次性預先創建好所有需要用到的紋理,並綁定到HLSL的紋理數組中,而不需要每次都重新綁定一個紋理。然後我們再使用索引值來訪問紋理數組中的某一紋理。

紋理數組的載入

現在我們手頭上僅有的就是DDSTextureLoader.hWICTextureLoader.h中的函數,但這裡面的函數每次都只能載入一張紋理。我們還需要修改龍書樣例中讀取紋理的函數,具體的操作順序如下:

  1. 一個個讀取存有紋理的文件,創建出一系列ID3D11Texture2D對象,這裡的每個對象單獨包含一張紋理;
  2. 創建一個ID3D11Texture2D對象,它同時也是一個紋理數組;
  3. 將之前讀取的所有紋理有條理地複製到剛創建的紋理數組對象中;
  4. 為該紋理數組對象創建創建一個紋理資源視圖(Shader Resource View)。

首先我們需要瞭解增強版的紋理創建函數。

CreateDDSTextureFromFileEx函數--使用更多的參數,從文件中讀取DDS紋理

HRESULT CreateDDSTextureFromFileEx(
    ID3D11Device* d3dDevice,                // [In]D3D設備
    ID3D11DeviceContext* d3dContext,        // [In]D3D設備上下文(可選)
    const wchar_t* szFileName,              // [In].dds文件名
    size_t maxsize,                         // [In]最大允許mipmap等級,預設0
    D3D11_USAGE usage,                      // [In]D3D11_USAGE枚舉值類型,指定CPU/GPU讀寫許可權
    unsigned int bindFlags,                 // [In]綁定標簽,指定它可以被綁定到什麼對象上
    unsigned int cpuAccessFlags,            // [In]CPU訪問許可權標簽
    unsigned int miscFlags,                 // [In]雜項標簽,忽略
    bool forceSRGB,                         // [In]強制使用SRGB,預設false
    ID3D11Resource** texture,               // [Out]獲取創建好的紋理(可選)
    ID3D11ShaderResourceView** textureView, // [Out]獲取創建好的紋理資源視圖(可選)
    DDS_ALPHA_MODE* alphaMode = nullptr);   // [Out]忽略(可選)

也就是說,圖片的數據格式、寬度、高度等信息都是隨文件讀取的時候獲得的,我們無法在這裡指定。所以我們要求提供的所有DDS紋理寬度、高度、數據格式都應當一致。對於數據格式不一致的,我們可以使用DirectX Texture Tool來修改,但是該程式包含在DirectX SDK中,這裡我在Github上嘗試提供單獨的DxTex.exe程式看能不能直接使用。現在我預先確保Demo中的4張樹紋理都設置為同樣的數據格式。

第一步,讀取一系列紋理的代碼如下:

//
// 1. 讀取所有紋理
//
size_t size = filenames.size();
std::vector<ComPtr<ID3D11Texture2D>> srcTex(size);
UINT mipLevel = maxMipMapSize;
UINT width, height;
DXGI_FORMAT format;
for (size_t i = 0; i < size; ++i)
{
    // 由於這些紋理並不會被GPU使用,我們使用D3D11_USAGE_STAGING枚舉值
    // 使得CPU可以讀取資源
    HR(CreateDDSTextureFromFileEx(device.Get(),
        deviceContext.Get(),
        filenames[i].c_str(),
        maxMipMapSize,
        D3D11_USAGE_STAGING,                            // Usage
        0,                                              // BindFlags
        D3D11_CPU_ACCESS_WRITE | D3D11_CPU_ACCESS_READ, // CpuAccessFlags
        0,                                              // MiscFlags
        false,
        (ID3D11Resource**)srcTex[i].GetAddressOf(),
        nullptr));

    // 讀取創建好的紋理Mipmap等級, 寬度和高度
    D3D11_TEXTURE2D_DESC texDesc;
    srcTex[i]->GetDesc(&texDesc);
    if (i == 0)
    {
        mipLevel = texDesc.MipLevels;
        width = texDesc.Width;
        height = texDesc.Height;
        format = texDesc.Format;
    }
    // 這裡斷言所有紋理的MipMap等級,寬度和高度應當一致
    assert(mipLevel == texDesc.MipLevels);
    assert(texDesc.Width == width && texDesc.Height == height);
    // 這裡要求所有提供的圖片數據格式應當是一致的,若存在不一致的情況,請
    // 使用dxtex.exe(DirectX Texture Tool)將所有的圖片轉成一致的數據格式
    assert(texDesc.Format == format);
        
}

接下來的第二步就是創建紋理數組,我們使用第一個紋理的描述去填充紋理數組的一部分描述:

//
// 2.創建紋理數組
//
D3D11_TEXTURE2D_DESC texDesc, texArrayDesc;
srcTex[0]->GetDesc(&texDesc);
texArrayDesc.Width = texDesc.Width;
texArrayDesc.Height = texDesc.Height;
texArrayDesc.MipLevels = texDesc.MipLevels;
texArrayDesc.ArraySize = size;
texArrayDesc.Format = texDesc.Format;
texArrayDesc.SampleDesc.Count = 1;
texArrayDesc.SampleDesc.Quality = 0;
texArrayDesc.Usage = D3D11_USAGE_DEFAULT;
texArrayDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
texArrayDesc.CPUAccessFlags = 0;
texArrayDesc.MiscFlags = 0;

ComPtr<ID3D11Texture2D> texArray;
HR(device->CreateTexture2D(&texArrayDesc, nullptr, texArray.GetAddressOf()));

在第三步進行複製之前,我們還需要瞭解紋理的子資源

紋理子資源(Texture Subresources)

WICTextureLoader或者DDSTextureLoader讀取出來的紋理數據實際上並不是由單純的一個二維數組構成,而是多個不同大小的二維數組,不同的mipmap等級對應不同的二維數組,這些二維數組都是該紋理的子資源。比如512x512的紋理載入進來包含的mipmap等級數(Mipmap Levels)為10,包含了從512x512, 256x256, 128x128...到1x1的10個二維數組顏色數據,而Direct3D API使用Mip切片(Mip slice)來指定某一mipmap等級的紋理,也有點像索引。比如mip slice值為0時,對應的是512x512的紋理,而mip slice值1對應的是256x256,以此類推。

對於紋理數組,每個元素就上面說的單個紋理對應的mipmap鏈,Direct3D API使用數組切片(array slice)來訪問不同紋理,也是相當於索引.這樣我們就可以把所有的紋理資源用下麵的圖來表示,假定下圖有4個紋理,每個紋理包含3個子資源,則當前指定的是Array Slice為2,Mip Slice為1的子資源。

D3D11CalcSubresource函數--計運算元資源的索引值

對於紋理數組的每一個子資源都可以用一個一維的索引值訪問,索引值的增減是以Mip切片值為主遞增的。

然後給定當前紋理數組的mipmap等級數(Mipmap Levels),數組切片(Array Slice)和Mip切片(Mip Slice),我們就可以用下麵的函數來求得指定子資源的索引值:

inline UINT D3D11CalcSubresource(UINT MipSlice, UINT ArraySlice, UINT MipLevels )
{ return MipSlice + ArraySlice * MipLevels; }

然後是映射相關的兩個函數

ID3D11DeviceContext::Map函數--獲取指向子資源中數據的指針並拒絕GPU對該子資源的訪問

HRESULT ID3D11DeviceContext::Map(
    ID3D11Resource           *pResource,          // [In]包含ID3D11Resource介面的資源對象
    UINT                     Subresource,         // [In]子資源索引
    D3D11_MAP                MapType,             // [In]D3D11_MAP枚舉值,指定讀寫相關操作
    UINT                     MapFlags,            // [In]填0,忽略
    D3D11_MAPPED_SUBRESOURCE *pMappedResource     // [Out]獲取到的已經映射到記憶體的子資源
);

D3D11_MAP枚舉值類型的成員如下:

D3D11_MAP成員 含義
D3D11_MAP_READ 映射到記憶體的資源用於讀取。該資源在創建的時候必須綁定了
D3D11_CPU_ACCESS_READ標簽
D3D11_MAP_WRITE 映射到記憶體的資源用於寫入。該資源在創建的時候必須綁定了
D3D11_CPU_ACCESS_WRITE標簽
D3D11_MAP_READ_WRITE 映射到記憶體的資源用於讀寫。該資源在創建的時候必須綁定了
D3D11_CPU_ACCESS_READ和D3D11_CPU_ACCESS_WRITE標簽
D3D11_MAP_WRITE_DISCARD 映射到記憶體的資源用於寫入,之前的資源數據將會被拋棄。該
資源在創建的時候必須綁定了D3D11_CPU_ACCESS_WRITE和
D3D11_USAGE_DYNAMIC標簽
D3D11_MAP_WRITE_NO_OVERWRITE 映射到記憶體的資源用於寫入,但不能覆寫已經存在的資源。
該枚舉值只能用於頂點/索引緩衝區。該資源在創建的時候需要
有D3D11_CPU_ACCESS_WRITE標簽,在Direct3D 11不能用於
設置了D3D11_BIND_CONSTANT_BUFFER標簽的資源,但在
11.1後可以。具體可以查閱MSDN文檔

獲取到的結構體D3D11_MAPPED_SUBRESOURCE成員如下:

typedef struct D3D11_MAPPED_SUBRESOURCE {
    void *pData;
    UINT RowPitch;  
    UINT DepthPitch;
};

首先pData指向的是映射到記憶體上的子資源首元素地址,即對應Row和Depth Slice值都為0的位置

其次RowPitch通常是一行元素占用的位元組數,對於512x512的紋理來說,若它使用DXGI_R8G8B8A8_UNORM數據類型,則一行占用了512像素*4位元組/像素=2048位元組。它的每個子資源的RowPitch都是一致的,這樣方便其進行下一行跳轉,即便在記憶體上會有些許的浪費,在mipmap等級越高時也有所體現,但一個完整的mipmap鏈消耗的記憶體也就逼近原來單張紋理所占位元組數的2倍而已。

DepthPitch在2D紋理的含義則是當前子資源占用的位元組數。像剛纔說的那樣,你在調試器上觀察每個mipmap等級對應的DepthPitch值,可以發現mip slice增加1,子資源占用的位元組數是原來的1/2,而不是原來的1/4。這可以說明每行占用的位元組數是不會改變的。

ID3D11DeviceContext::UpdateSubresource函數[2]--將記憶體數據拷貝到不可進行映射的子資源中

這個函數在之前我們主要是用來將記憶體數據拷貝到常量緩衝區中,現在我們也可以用它將記憶體數據拷貝到紋理的子資源當中:

void ID3D11DeviceContext::UpdateSubresource(
  ID3D11Resource  *pDstResource,    // [In]目標資源對象
  UINT            DstSubresource,   // [In]對於2D紋理來說,該參數為指定Mipmap等級的子資源
  const D3D11_BOX *pDstBox,         // [In]這裡填nullptr   
  const void      *pSrcData,        // [In]用於拷貝的記憶體數據
  UINT            SrcRowPitch,      // [In]該2D紋理的 寬度*數據格式的位數
  UINT            SrcDepthPitch     // [In]對於2D紋理來說並不需要用到該參數,因此可以任意設置
);

ID3D11DeviceContext::UnMap函數--讓指向資源的指針無效並重新啟用GPU對該資源的訪問許可權

void ID3D11DeviceContext::Unmap(
    ID3D11Resource *pResource,      // [In]包含ID3D11Resource介面的資源對象
    UINT           Subresource      // [In]需要取消的子資源索引
);

第三步的具體代碼如下:

//
// 3.將所有的紋理子資源賦值到紋理數組中
//

// 每個紋理元素
for (size_t i = 0; i < size; ++i)
{
    // 紋理中的每個mipmap等級
    for (UINT j = 0; j < mipLevel; ++j)
    {
        D3D11_MAPPED_SUBRESOURCE mappedTex2D;
        // 允許映射索引i紋理中,索引j的mipmap等級的2D紋理
        HR(deviceContext->Map(srcTex[i].Get(),
            j, D3D11_MAP_READ, 0, &mappedTex2D));
        deviceContext->UpdateSubresource(
            texArray.Get(),
            D3D11CalcSubresource(j, i, mipLevel),   // i * mipLevel + j
            nullptr,
            mappedTex2D.pData,
            mappedTex2D.RowPitch,
            mappedTex2D.DepthPitch);
        // 停止映射
        deviceContext->Unmap(srcTex[i].Get(), j);
    }
}

最後一步就是要創建著色器資源視圖。

ID3D11Device::CreateShaderResourceView--創建著色器資源視圖

HRESULT ID3D11Device::CreateShaderResourceView(
    ID3D11Resource                        *pResource,   // [In]待綁定資源
    const D3D11_SHADER_RESOURCE_VIEW_DESC *pDesc,       // [In]著色器資源視圖描述
    ID3D11ShaderResourceView              **ppSRView    // [Out]獲取創建的著色器資源視圖
);

所以還需要填充D3D11_SHADER_RESOURCE_VIEW_DESC結構體:

typedef struct D3D11_SHADER_RESOURCE_VIEW_DESC
{
    DXGI_FORMAT Format;     // 數據格式
    D3D11_SRV_DIMENSION ViewDimension;  // 視圖維度,決定下麵需要填充哪個共用體成員
    union 
    {
        D3D11_BUFFER_SRV Buffer;
        D3D11_TEX1D_SRV Texture1D;
        D3D11_TEX1D_ARRAY_SRV Texture1DArray;
        D3D11_TEX2D_SRV Texture2D;
        D3D11_TEX2D_ARRAY_SRV Texture2DArray;
        D3D11_TEX2DMS_SRV Texture2DMS;
        D3D11_TEX2DMS_ARRAY_SRV Texture2DMSArray;
        D3D11_TEX3D_SRV Texture3D;
        D3D11_TEXCUBE_SRV TextureCube;
        D3D11_TEXCUBE_ARRAY_SRV TextureCubeArray;
        D3D11_BUFFEREX_SRV BufferEx;
    };
}   D3D11_SHADER_RESOURCE_VIEW_DESC;

最後一步的代碼如下:

//
// 4.創建紋理數組的SRV
//
D3D11_SHADER_RESOURCE_VIEW_DESC viewDesc;
viewDesc.Format = texArrayDesc.Format;
viewDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2DARRAY;
viewDesc.Texture2DArray.MostDetailedMip = 0;
viewDesc.Texture2DArray.MipLevels = texArrayDesc.MipLevels;
viewDesc.Texture2DArray.FirstArraySlice = 0;
viewDesc.Texture2DArray.ArraySize = size;

ComPtr<ID3D11ShaderResourceView> texArraySRV;
HR(device->CreateShaderResourceView(texArray.Get(), &viewDesc, texArraySRV.GetAddressOf()));

// 已經確保所有資源由ComPtr管理,無需手動釋放

return texArraySRV;

CreateDDSTexture2DArrayShaderResourceView函數--創建用於DDS紋理的數組著色器資源視圖

該函數放到了GameApp類中,你也可以單獨抽離出來。

ComPtr<ID3D11ShaderResourceView> CreateDDSTexture2DArrayShaderResourceView(
    ComPtr<ID3D11Device> device,                    // [In]D3D設備
    ComPtr<ID3D11DeviceContext> deviceContext,      // [In]D3D設備上下文
    const std::vector<std::wstring>& filenames,     // [In]文件名數組
    int maxMipMapSize);    // [In]最大允許mipmap等級,若為0,則使用預設紋理mipmap等級

具體的函數實現就是上面四步的所有代碼。

Alpha-To-Coverage

在Demo運行的時候,仔細觀察可以發現樹公告板的某些邊緣部分有一些比較突出的黑邊。

這是因為當前預設使用的是Alpha Test,即HLSL中使用clip函數將Alpha值為0的像素點給剔除掉,這些像素也不是樹的一部分。該函數決定某一像素是留下還是拋棄,這會導致不平滑的過渡現象,在攝像機逐漸靠近該紋理時,圖片本身也在不斷放大,硬邊部分也會被放大,就像下麵那張圖:

當然,你也可以使用透明混合的方式,但是透明混合對繪製的順序是有要求的,要求透明物體按從後到前的順序進行繪製,即需要在繪製透明物體前先對物體按到攝像機的距離排個序。當然如果需要繪製大量的草叢的話,這種方法所需要的開銷會變得非常大,操作起來也十分麻煩。

當然,我們可以考慮下使用MSAA(多重採樣抗鋸齒),並配合Alpha Test進行。MSAA可以用於將多邊形的鋸齒邊緣平滑處理,然後讓Direct3D開啟alpha-to-coverage技術,標記邊緣部分。

首先在創建後備緩衝區、深度/模板緩衝區的時候需要打開4倍多重採樣的支持,我們只需要在GameApp的構造函數中這樣寫即可:

GameApp::GameApp(HINSTANCE hInstance)
    : D3DApp(hInstance)
{
    // 開啟4倍多重採樣
    mEnable4xMsaa = true;
}

然後在之前的例子里,我們已經在RenderStates類中預先創建好了混合狀態:

D3D11_BLEND_DESC blendDesc;
ZeroMemory(&blendDesc, sizeof(blendDesc));
auto& rtDesc = blendDesc.RenderTarget[0];
// Alpha-To-Coverage模式
blendDesc.AlphaToCoverageEnable = true;
blendDesc.IndependentBlendEnable = false;
rtDesc.BlendEnable = false;
rtDesc.RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL;
HR(device->CreateBlendState(&blendDesc, BSAlphaToCoverage.ReleaseAndGetAddressOf()));

然後只需要在需要的時候綁定該狀態即可。

BasicFX.h的變化

常量緩衝區對應的結構體和BasicFX類的變化如下:

#ifndef BASICFX_H
#define BASICFX_H

#include <wrl/client.h>
#include <d3d11_1.h>
#include <d3dcompiler.h>
#include <directxmath.h>
#include <vector>
#include "LightHelper.h"
#include "RenderStates.h"
#include "Vertex.h"

// 由於常量緩衝區的創建需要是16位元組的倍數,該函數可以返回合適的位元組大小
inline UINT Align16Bytes(UINT size)
{
    return (size + 15) & (UINT)(-16);
}

struct CBChangesEveryDrawing
{
    DirectX::XMMATRIX world;
    DirectX::XMMATRIX worldInvTranspose;
    DirectX::XMMATRIX texTransform;
    Material material;
};

struct CBChangesEveryFrame
{
    DirectX::XMMATRIX view;
    DirectX::XMFLOAT4 eyePos;
};

struct CBDrawingStates
{
    DirectX::XMFLOAT4 fogColor;
    int fogEnabled;
    float fogStart;
    float fogRange;
    float pad;
};

struct CBChangesOnResize
{
    DirectX::XMMATRIX proj;
};

struct CBNeverChange
{
    DirectionalLight dirLight[4];
};

class BasicFX
{
public:
    // 使用模板別名(C++11)簡化類型名
    template <class T>
    using ComPtr = Microsoft::WRL::ComPtr<T>;

    // 初始化Basix.fx所需資源並初始化光柵化狀態
    bool InitAll(ComPtr<ID3D11Device> device);
    // 是否已經初始化
    bool IsInit() const;

    template <class T>
    void UpdateConstantBuffer(const T& cbuffer);

    // 預設狀態繪製
    void SetRenderDefault();

    // 公告板繪製
    void SetRenderBillboard(bool enableAlphaToCoverage);

private:
    // objFileNameInOut為編譯好的著色器二進位文件(.*so),若有指定則優先尋找該文件並讀取
    // hlslFileName為著色器代碼,若未找到著色器二進位文件則編譯著色器代碼
    // 編譯成功後,若指定了objFileNameInOut,則保存編譯好的著色器二進位信息到該文件
    // ppBlobOut輸出著色器二進位信息
    HRESULT CreateShaderFromFile(const WCHAR* objFileNameInOut, const WCHAR* hlslFileName, LPCSTR entryPoint, LPCSTR shaderModel, ID3DBlob** ppBlobOut);

private:
    ComPtr<ID3D11VertexShader> mBasicVS;
    ComPtr<ID3D11PixelShader> mBasicPS;

    ComPtr<ID3D11VertexShader> mBillboardVS;
    ComPtr<ID3D11GeometryShader> mBillboardGS;
    ComPtr<ID3D11PixelShader> mBillboardPS;


    ComPtr<ID3D11InputLayout> mVertexPosSizeLayout;         // 點精靈輸入佈局
    ComPtr<ID3D11InputLayout> mVertexPosNormalTexLayout;    // 3D頂點輸入佈局

    ComPtr<ID3D11DeviceContext> md3dImmediateContext;       // 設備上下文

    std::vector<ComPtr<ID3D11Buffer>> mConstantBuffers;     // 常量緩衝區
};

#endif

初始化函數和SetRenderDeafult方法這裡就不贅述了。

BasicFX::SetRenderBillboard方法--公告板繪製

實現如下:

void BasicFX::SetRenderBillboard(bool enableAlphaToCoverage)
{
    md3dImmediateContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_POINTLIST);
    md3dImmediateContext->IASetInputLayout(mVertexPosSizeLayout.Get());
    md3dImmediateContext->VSSetShader(mBillboardVS.Get(), nullptr, 0);
    md3dImmediateContext->GSSetShader(mBillboardGS.Get(), nullptr, 0);
    md3dImmediateContext->RSSetState(RenderStates::RSNoCull.Get());
    md3dImmediateContext->PSSetShader(mBillboardPS.Get(), nullptr, 0);
    md3dImmediateContext->PSSetSamplers(0, 1, RenderStates::SSLinearWrap.GetAddressOf());
    md3dImmediateContext->OMSetDepthStencilState(nullptr, 0);
    md3dImmediateContext->OMSetBlendState(
        (enableAlphaToCoverage ? RenderStates::BSAlphaToCoverage.Get() : nullptr),
        nullptr, 0xFFFFFFFF);
}

參數enableAlphaToCoverage決定是否要綁定渲染狀態對象RenderStates::BSAlphaToCoverage

GameApp類的變化

類成員相關聲明如下:

class GameApp : public D3DApp
{
public:
    // 攝像機模式
    enum class CameraMode { FirstPerson, ThirdPerson, Free };
    
public:
    GameApp(HINSTANCE hInstance);
    ~GameApp();

    bool Init();
    void OnResize();
    void UpdateScene(float dt);
    void DrawScene();

private:
    bool InitResource();
    void InitPointSpritesBuffer();

    // 根據給定的DDS紋理文件集合,創建2D紋理數組
    // 要求所有紋理的寬度和高度都一致
    // 若maxMipMapSize為0,使用預設mipmap等級
    // 否則,mipmap等級將不會超過maxMipMapSize
    ComPtr<ID3D11ShaderResourceView> CreateDDSTexture2DArrayShaderResourceView(
        ComPtr<ID3D11Device> device,
        ComPtr<ID3D11DeviceContext> deviceContext,
        const std::vector<std::wstring>& filenames,
        int maxMipMapSize = 0);


private:
    
    ComPtr<ID2D1SolidColorBrush> mColorBrush;               // 單色筆刷
    ComPtr<IDWriteFont> mFont;                              // 字體
    ComPtr<IDWriteTextFormat> mTextFormat;                  // 文本格式

    ComPtr<ID3D11Buffer> mPointSpritesBuffer;               // 點精靈頂點緩衝區
    ComPtr<ID3D11ShaderResourceView> mTreeTexArray;         // 樹的紋理數組
    Material mTreeMat;                                      // 樹的材質

    GameObject mGround;                                     // 地面
    
    BasicFX mBasicFX;                                       // Basic特效管理類

    CameraMode mCameraMode;                                 // 攝像機模式
    std::shared_ptr<Camera> mCamera;                        // 攝像機

    bool mIsNight;                                          // 是否黑夜
    bool mEnableAlphaToCoverage;                            // 是否開啟Alpha-To-Coverage

    CBChangesEveryDrawing mCBChangesEveryDrawing;           // 該緩衝區存放每次繪製更新的變數
    CBChangesEveryFrame mCBChangesEveryFrame;               // 該緩衝區存放每幀更新的變數
    CBDrawingStates mCBDrawingStates;                       // 該緩衝區存放繪製狀態
    CBChangesOnResize mCBChangesOnReSize;                   // 該緩衝區存放僅在視窗大小變化時更新的變數
    CBNeverChange mCBNeverChange;                           // 該緩衝區存放不會再進行修改的變數
};

GameApp::InitPointSpritesBuffer方法--初始化存放點精靈的緩衝區

該方法會生成20個頂點,均勻並略帶隨機性地環繞在原點周圍。這些頂點一經創建就不可以被修改了,它們將會被用於公告板的創建:

void GameApp::InitPointSpritesBuffer()
{
    srand((unsigned)time(nullptr));
    VertexPosSize vertexes[16];
    float theta = 0.0f;
    for (int i = 0; i < 16; ++i)
    {
        // 取20-50的半徑放置隨機的樹
        float radius = (float)(rand() % 31 + 20);
        float randomRad = rand() % 256 / 256.0f * XM_2PI / 16;
        vertexes[i].pos = XMFLOAT3(radius * cosf(theta + randomRad), 8.0f, radius * sinf(theta + randomRad));
        vertexes[i].size = XMFLOAT2(30.0f, 30.0f);
        theta += XM_2PI / 16;
    }

    // 設置頂點緩衝區描述
    D3D11_BUFFER_DESC vbd;
    ZeroMemory(&vbd, sizeof(vbd));
    vbd.Usage = D3D11_USAGE_IMMUTABLE;  // 數據不可修改
    vbd.ByteWidth = sizeof (vertexes);
    vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER;
    vbd.CPUAccessFlags = 0;
    // 新建頂點緩衝區
    D3D11_SUBRESOURCE_DATA InitData;
    ZeroMemory(&InitData, sizeof(InitData));
    InitData.pSysMem = vertexes;
    HR(md3dDevice->CreateBuffer(&vbd, &InitData, mPointSpritesBuffer.GetAddressOf()));
}

GameApp::InitResource方法--初始化資源

該方法集成了所有資源的初始化,註意樹的紋理數組要提供到輸入槽1,對應紋理寄存器t1的Texture2DArray

bool GameApp::InitResource()
{
    // 預設白天,開啟AlphaToCoverage
    mIsNight = false;
    mEnableAlphaToCoverage = true;
    // ******************
    // 初始化各種物體

    // 初始化樹紋理資源
    mTreeTexArray = CreateDDSTexture2DArrayShaderResourceView(
        md3dDevice,
        md3dImmediateContext,
        std::vector<std::wstring>{
        L"Texture\\tree0.dds",
            L"Texture\\tree1.dds",
            L"Texture\\tree2.dds",
            L"Texture\\tree3.dds"});
    
    // 初始化點精靈緩衝區
    InitPointSpritesBuffer();

    // 初始化樹的材質
    mTreeMat.Ambient = XMFLOAT4(0.5f, 0.5f, 0.5f, 1.0f);
    mTreeMat.Diffuse = XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f);
    mTreeMat.Specular = XMFLOAT4(0.2f, 0.2f, 0.2f, 16.0f);

    ComPtr<ID3D11ShaderResourceView> texture;
    // 初始化地板
    mGround.SetBuffer(md3dDevice, Geometry::CreatePlane(XMFLOAT3(0.0f, -5.0f, 0.0f), XMFLOAT2(100.0f, 100.0f), XMFLOAT2(10.0f, 10.0f)));
    HR(CreateDDSTextureFromFile(md3dDevice.Get(), L"Texture\\Grass.dds", nullptr, texture.GetAddressOf()));
    mGround.SetTexture(texture);
    Material material;
    material.Ambient = XMFLOAT4(0.5f, 0.5f, 0.5f, 1.0f);
    material.Diffuse = XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f);
    material.Specular = XMFLOAT4(0.2f, 0.2f, 0.2f, 16.0f);
    mGround.SetMaterial(material);
    mGround.SetWorldMatrix(XMMatrixIdentity());
    mGround.SetTexTransformMatrix(XMMatrixIdentity());

    // ******************
    // 初始化常量緩衝區的值

    mCBChangesEveryDrawing.material = mTreeMat;
    mCBChangesEveryDrawing.world = mCBChangesEveryDrawing.worldInvTranspose = XMMatrixIdentity();
    mCBChangesEveryDrawing.texTransform = XMMatrixIdentity();


    // 方向光
    mCBNeverChange.dirLight[0].Ambient = XMFLOAT4(0.1f, 0.1f, 0.1f, 1.0f);
    mCBNeverChange.dirLight[0].Diffuse = XMFLOAT4(0.25f, 0.25f, 0.25f, 1.0f);
    mCBNeverChange.dirLight[0].Specular = XMFLOAT4(0.1f, 0.1f, 0.1f, 1.0f);
    mCBNeverChange.dirLight[0].Direction = XMFLOAT3(-0.577f, -0.577f, 0.577f);
    mCBNeverChange.dirLight[1] = mCBNeverChange.dirLight[0];
    mCBNeverChange.dirLight[1].Direction = XMFLOAT3(0.577f, -0.577f, 0.577f);
    mCBNeverChange.dirLight[2] = mCBNeverChange.dirLight[0];
    mCBNeverChange.dirLight[2].Direction = XMFLOAT3(0.577f, -0.577f, -0.577f);
    mCBNeverChange.dirLight[3] = mCBNeverChange.dirLight[0];
    mCBNeverChange.dirLight[3].Direction = XMFLOAT3(-0.577f, -0.577f, -0.577f);

    // 攝像機相關
    mCameraMode = CameraMode::Free;
    auto camera = std::shared_ptr<FirstPersonCamera>(new FirstPersonCamera);
    mCamera = camera;
    camera->SetPosition(XMFLOAT3());
    camera->SetFrustum(XM_PI / 3, AspectRatio(), 1.0f, 1000.0f);
    camera->LookTo(
        XMVectorSet(0.0f, 0.0f, 0.0f, 1.0f),
        XMVectorSet(0.0f, 0.0f, 1.0f, 1.0f),
        XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f));
    camera->UpdateViewMatrix();


    mCBChangesEveryFrame.view = camera->GetView();
    XMStoreFloat4(&mCBChangesEveryFrame.eyePos, camera->GetPositionXM());

    mCBChangesOnReSize.proj = camera->GetProj();
    
    // 霧狀態預設開啟
    mCBDrawingStates.fogEnabled = 1;
    mCBDrawingStates.fogColor = XMFLOAT4(0.75f, 0.75f, 0.75f, 1.0f);    // 銀色
    mCBDrawingStates.fogRange = 75.0f;
    mCBDrawingStates.fogStart = 15.0f;
    // 更新常量緩衝區資源
    mBasicFX.UpdateConstantBuffer(mCBChangesEveryDrawing);
    mBasicFX.UpdateConstantBuffer(mCBChangesEveryFrame);
    mBasicFX.UpdateConstantBuffer(mCBChangesOnReSize);
    mBasicFX.UpdateConstantBuffer(mCBDrawingStates);
    mBasicFX.UpdateConstantBuffer(mCBNeverChange);

    // 直接綁定樹的紋理
    md3dImmediateContext->PSSetShaderResources(1, 1, mTreeTexArray.GetAddressOf());
    
    return true;
}

其餘方法限於篇幅就不放在這裡了,讀者可以查看源碼觀察剩餘部分的代碼實現。現在來看實現效果吧。

實現效果

可以觀察到,在與公告版近距離接觸時可以很明顯地看到公告板在跟著攝像機旋轉。如果距離很遠的話轉動的幅度就會很小,用戶才會比較難以分辨出遠處物體是否為公告板或3D模型了。

下麵演示了白天和黑夜的霧效

最後則是Alpha-To-Coverage的開啟/關閉效果對比

DirectX11 With Windows SDK完整目錄

Github項目源碼


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Vue3:腳手架配置 https://blog.csdn.net/weixin_41424247/article/details/80867351 與原來的vue cli 2.x版本不同的是:如果使用最新版本的@vue/cli初始化vue項目時,通常看不到webpack的配製文件。而在原來的2.x版 ...
  • 媒體: { 圖片: [ "從本地相冊選擇圖片或使用相機拍照", "預覽圖片", "獲取圖片信息,倘若為網路圖片,需先配置download功能變數名稱才能生效", "保存圖片到系統相冊", // 需要用戶授權 ], 錄音: [ "開始錄音", // 最長一分鐘 "結束錄音", ], 錄音管理: "獲取全局唯一 ...
  • 在jQuery選擇器的基礎下我們實現一個全選,反選,全不選功能! <script type="text/javascript"> $(function () { //全選 $("#selectAll").click(function () { //attr在HTML推薦非固有(用戶自定義)屬性是使用 ...
  • 庫>包>模塊 模塊 py文件 包package 是一個文件夾,包含很多的模塊 庫library 是包的集合,比如python安裝目錄下的lib文件夾 ...
  • 冒泡排序是一種基礎排序演算法,在python中,我們利用列表的的方式來完成,它對列表中的元素進行重覆的遍歷,在遍歷的同時進行比較,如果兩個數沒有按照我們規定的順序進行排列,就按照我們預先設定好的是順序或者逆序輸出,類似於燒開水時的氣泡,主要操作如下:比較相鄰的元素。如果第一個比第二個大(升序),就交換... ...
  • 設置 vim ~/.zshrc設置 vim ~/.bash_profile ...
  • c/c++ 淺拷貝 編譯器合成的 拷貝構造函數 和 =重載函數 ,只是做入下處理: 如果成員變數a是指針,執行完 拷貝構造函數 或者 =重載函數 後,對象1和對象2的成員變數就指向了一個地址了,所以當改變對象1的a的值,對象2的a的值也變化了, 而且,如果在析構函數里寫了free(a)的話,就相當於 ...
  • c/c++ 編譯器提供的預設6個函數 1,構造函數 2,拷貝構造函數 3,析構函數 4,=重載函數 5,&重載函數 6,const&重載函數 c++ include using namespace std; class Test{ public: Test(int d = 0):data(d){ c ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...