DirectX11 With Windows SDK--16 利用幾何着色器可選的流輸出階段幫助繪製多種分形

前言

在上一章,我們知道了如何使用幾何着色器來重新組裝圖元,比如從一個三角形分裂成三個三角形。但是爲了實現更高階的分形,我們必須要從幾何着色器拿到輸出的頂點。這裏我們可以使用可選的流輸出階段來拿到頂點集合。

注意: 本章末尾有大量的GIF動圖!

DirectX11 With Windows SDK完整目錄

Github項目源碼

流輸出階段

現在我們知道GPU可以寫入紋理(textures),例如深度/模板緩衝區以及後備緩衝區。當然,我們也可以通過渲染管線的流輸出階段讓GPU將幾何着色器輸出的頂點集合寫入到指定的頂點緩衝區(vertex buffer)。除此之外,我們還能夠指定不進行光柵化以及後續的所有階段,僅讓頂點數據經過流輸出階段。

在幾何着色器中,最多四個流輸出對象可以被設置,即幾何着色器的入口函數中只允許設置四個流輸出對象的參數。當多個流輸出對象存在時,它們必須都要爲PointStream類模板,但允許模板參數不同。輸出的頂點回流到頂點緩衝區後可以再次進行一遍新的渲染管線流程。

上一章也提到,幾何着色器的單次調用不能產出超過1024個標量。因此分配給所有流輸出對象的標量總和不能超過1024。比如現在我有2個流輸出對象,它們的結構體相同,容納512個標量,那最多僅允許輸出2個這樣的頂點來分配給這2個流輸出對象。

流輸出狀態的配置

ID3D11DeviceContext::SOSetTargets方法--綁定流輸出對應用於接收數據的頂點緩衝區

void ID3D11DeviceContext::SOSetTargets(
  UINT         NumBuffers,              // [In]頂點緩衝區數目
  ID3D11Buffer * const *ppSOTargets,    // [In]頂點緩衝區數組
  const UINT   *pOffsets                // [In]一個數組包含對每個頂點緩衝區的字節偏移量
);

該方法多允許設置4個頂點緩衝區。

每個要綁定到流輸出階段的緩衝區資源必須要在創建的時候額外設置D3D11_BIND_STREAM_OUTPUT綁定標籤。

若偏移值設爲-1,則會引起流輸出緩衝區被追加到最後一個緩衝區的後面

頂點緩衝區綁定到流輸出階段的輸出槽0操作如下:

UINT offset = 0;
md3dImmediateContext->SOSetTargets(1, vertexBufferOut.GetAddressOf(), &offset);

如果我們需要恢復默認的狀態,則可以這樣調用:

ID3D11Buffer* nullBuffer = nullptr;
UINT offset = 0;
md3dImmediateContext->SOSetTargets(1, &nullBuffer, &offset);

注意: 如果使用的是當前綁定到輸入裝配階段的頂點緩衝區,則綁定會失效。因爲頂點緩衝區不可以同時被綁定到輸入裝配階段和流輸出階段。

因爲後續我們是將每一階輸出的頂點都保存下來,即便不需要交換頂點緩衝區,但也有可能出現同時綁定輸入/輸出的情況。一種合理的綁定順序如下:

// 先恢復流輸出默認設置,防止頂點緩衝區同時綁定在輸入和輸出階段
UINT stride = sizeof(VertexPosColor);
UINT offset = 0;
ID3D11Buffer * nullBuffer = nullptr;
md3dImmediateContext->SOSetTargets(1, &nullBuffer, &offset);
// ...
md3dImmediateContext->IASetInputLayout(mVertexPosColorLayout.Get());
// ...
md3dImmediateContext->SOSetTargets(1, vertexBufferOut.GetAddressOf(), &offset);

當渲染管線完成一次流輸出後,我們就可以用下面的方法來獲取綁定在流輸出階段上的頂點緩衝區(當然你本身持有該緩衝區的指針的話就不需要了)

ID3D11DeviceContext::SOGetTargets方法--獲取綁定在流輸出階段的頂點緩衝區

void ID3D11DeviceContext::SOGetTargets(
  UINT         NumBuffers,          // [In]緩衝區數目
  ID3D11Buffer **ppSOTargets        // [Out]獲取綁定流輸出階段的頂點緩衝區
);

輸出的頂點緩衝區引用數會加1,最好是能夠使用ComPtr來承接頂點緩衝區,否則就要在結束的時候手工調用Release方法,若忘記調用則會引發內存泄漏。

ID3D11Device::CreateGeometryShaderWithStreamOutput方法--創建帶流輸出階段的幾何着色器

接下來我們需要指定數據會流向哪個輸出槽,首先我們需要填充結構體D3D11_SO_DECLARATION_ENTRY,結構體聲明如下:

typedef struct D3D11_SO_DECLARATION_ENTRY {
  UINT   Stream;            // 輸出流索引,從0開始
  LPCSTR SemanticName;      // 語義名
  UINT   SemanticIndex;     // 語義索引
  BYTE   StartComponent;    // 從第幾個分量(xyzw)開始,只能取0-3
  BYTE   ComponentCount;    // 分量的輸出數目,只能取1-4
  BYTE   OutputSlot;        // 輸出槽索引,只能取0-3
};

其中,語義名SemanticName用於指定在幾何着色器的流輸出對象對應的結構體中該語義描述的成員,然後用語義索引SemanticIndex指定存在同名語義下用索引值標記的唯一成員。

然後StartComponentComponentCount用於控制該向量需要輸出哪些分量。若StartComponent爲1,ComponentCount爲2,則輸出的分量爲(y, z),而要輸出全部分量,則指定StartCompnent爲0, ComponentCount爲4.

輸出槽索引OutputSlot用於指定選擇綁定流輸出的緩衝區數組中的某一元素。

由於這裏一個結構體只能指定某個輸出流中的某一向量,所以通常我們需要像頂點輸入佈局那樣傳遞一個數組來取出組合成特定頂點。

比如說現在頂點着色器輸入的頂點和流輸出的頂點是一致的:

struct VertexPosColor
{
    DirectX::XMFLOAT3 pos;
    DirectX::XMFLOAT4 color;
    static const D3D11_INPUT_ELEMENT_DESC inputLayout[2];
};

輸入佈局描述如下:

const D3D11_INPUT_ELEMENT_DESC VertexPosColor::inputLayout[2] = {
    { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
    { "COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 }
};

HLSL中的結構體如下:

struct VertexPosColor
{
    float3 PosL : POSITION;
    float4 Color : COLOR;
};

流輸出的入口描述如下:

const D3D11_SO_DECLARATION_ENTRY posColorLayout[2] = {
    { 0, "POSITION", 0, 0, 3, 0 },
    { 0, "COLOR", 0, 0, 4, 0 }
};

這裏對應的是索引爲0的流輸出對象,輸出給綁定在索引爲0的輸出槽的頂點緩衝區,先輸出語義爲POSITION的向量中的xyz分量,然後輸出COLOR整個向量。這樣一個輸出的頂點就和原來的頂點一致了。

接下來給出ID3D11Device::CreateGeometryShaderWithStreamOutput方法的原型:

HRESULT ID3D11Device::CreateGeometryShaderWithStreamOutput(
  const void                       *pShaderBytecode,    // [In]編譯好的着色器字節碼
  SIZE_T                           BytecodeLength,      // [In]字節碼長度
  const D3D11_SO_DECLARATION_ENTRY *pSODeclaration,     // [In]D3D11_SO_DECLARATION_ENTRY的數組
  UINT                             NumEntries,          // [In]入口總數
  const UINT                       *pBufferStrides,     // [In]一個數組包含了每個綁定到流輸出的緩衝區中頂點字節大小
  UINT                             NumStrides,          // [In]上面數組的元素數目
  UINT                             RasterizedStream,    // [In]按索引指定哪個流輸出對象用於傳遞到光柵化階段
  ID3D11ClassLinkage               *pClassLinkage,      // [In]忽略
  ID3D11GeometryShader             **ppGeometryShader   // [Out]創建好的幾何着色器
);

如果不需要有流輸出對象提供數據給光柵化階段,則RasterizedStream應當指定爲D3D11_SO_NO_RASTERIZED_STREAM。即便某一流輸出對象傳遞了數據給光柵化階段,它仍可以提供數據給某一綁定的緩衝區。

下面是一個調用的例子:

const D3D11_SO_DECLARATION_ENTRY posColorLayout[2] = {
    { 0, "POSITION", 0, 0, 3, 0 },
    { 0, "COLOR", 0, 0, 4, 0 }
};

HR(device->CreateGeometryShaderWithStreamOutput(blob->GetBufferPointer(), blob->GetBufferSize(), posColorLayout, ARRAYSIZE(posColorLayout),
    &stridePosColor, 1, D3D11_SO_NO_RASTERIZED_STREAM, nullptr, mTriangleSOGS.GetAddressOf()));

當該着色器被綁定到渲染管線上,流輸出階段就會被激活,我們可以使用ID3D11DeviceContext::Draw方法來進行繪製。然後當渲染管線開始執行的時候,任何傳遞給幾何着色器中的流輸出對象的數據,都會基於語義名和語義索引嘗試匹配輸出佈局。一旦發現有匹配的語義,該數據就會流向對應的緩衝區來創建完整的輸出頂點集。

繪製各種酷炫的分形

由於現在的着色器多到令人髮指,而且有沒有很好的辦法歸類整合,故在下面用一張表列出所有繪製流程用到的着色器hlsl文件名稱:

操作 VS GS PS
通過流輸出得到分裂的三角形 TriangleSO_VS TriangleSO_GS X
通過流輸出得到分形雪花 SnowSO_VS SnowSO_GS X
通過流輸出得到分形球體 SphereSO_VS SphereSO_GS X
繪製分形三角形 Triangle_VS X Triangle_PS
繪製分形雪花 Snow_VS X Snow_PS
繪製分形球體 Sphere_VS X Sphere_PS
繪製法向量 Normal_VS Normal_GS Normal_PS

首先給出Basic.fx文件的內容,要注意裏面的常量緩衝區和之前有所變化:

#include "LightHelper.hlsli"

cbuffer CBChangesEveryFrame : register(b0)
{
    row_major matrix gWorld;
    row_major matrix gWorldInvTranspose;
}

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

cbuffer CBNeverChange : register(b2)
{
    DirectionalLight gDirLight;
    Material gMaterial;
    row_major matrix gView;
    float3 gSphereCenter;
    float gSphereRadius;
    float3 gEyePosW;
}


struct VertexPosColor
{
    float3 PosL : POSITION;
    float4 Color : COLOR;
};

struct VertexPosHColor
{
    float4 PosH : SV_POSITION;
    float4 Color : COLOR;
};

struct VertexPosHLColor
{
    float4 PosH : SV_POSITION;
    float3 PosL : POSITION;
    float4 Color : COLOR;
};


struct VertexPosNormalColor
{
    float3 PosL : POSITION;
    float3 NormalL : NORMAL;
    float4 Color : COLOR;
};

struct VertexPosHWNormalColor
{
    float4 PosH : SV_POSITION;
    float3 PosW : POSITION;
    float3 NormalW : NORMAL;
    float4 Color : COLOR;
};

實戰1: 繪製分形三角形

通過流輸出階段,一個三角形就分裂出了三個三角形,頂點的數目翻了3倍。若規定1階分形三角形的頂點數爲3,則N階分形三角形的頂點數爲\(3^{N}\)

HLSL代碼

首先是TriangleSO_VS.hlsl,它負責將頂點直接傳遞給幾何着色器。

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

VertexPosColor VS(VertexPosColor pIn)
{
    return pIn;
}

然後和上一章一樣,TriangleSO_GS.hlsl中的幾何着色器將一個三角形分裂成三個三角形,並且輸出的頂點類型和輸入的頂點是一致的。

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

[maxvertexcount(9)]
void GS(triangle VertexPosColor input[3], inout TriangleStream<VertexPosColor> output)
{
    //
    // 將一個三角形分裂成三個三角形,即沒有v3v4v5的三角形
    //       v1
    //       /\
    //      /  \
    //   v3/____\v4
    //    /\xxxx/\
    //   /  \xx/  \
    //  /____\/____\
    // v0    v5    v2


    VertexPosColor vertexes[6];
    int i;
    [unroll]
    for (i = 0; i < 3; ++i)
    {
        vertexes[i] = input[i];
        vertexes[i + 3].Color = (input[i].Color + input[(i + 1) % 3].Color) / 2.0f;
        vertexes[i + 3].PosL = (input[i].PosL + input[(i + 1) % 3].PosL) / 2.0f;
    }

    [unroll]
    for (i = 0; i < 3; ++i)
    {
        output.Append(vertexes[i]);
        output.Append(vertexes[3 + i]);
        output.Append(vertexes[(i + 2) % 3 + 3]);

        output.RestartStrip();
    }
}

接下來的Triangle_VS.hlslTriangle_PS.hlsl則是常規的三角形繪製:

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

VertexPosHColor VS(VertexPosColor pIn)
{
    row_major matrix worldViewProj = mul(mul(gWorld, gView), gProj);
    VertexPosHColor pOut;
    pOut.Color = pIn.Color;
    pOut.PosH = mul(float4(pIn.PosL, 1.0f), worldViewProj);
    return pOut;
}
// Triangle_PS.hlsl
#include "Basic.fx"

float4 PS(VertexPosHColor pIn) : SV_Target
{
    return pIn.Color;
}

實戰2: 繪製分形雪花

現在規定第一張圖爲一階分形雪花,第二張爲二階分形雪花。觀察二者之間的變化,可以發現前者的每一條直線變成了四條折線。其中每個尖銳角的度數都在60度,並且每條邊的長度都應該是一致的。

HLSL代碼

和之前一樣,SnowSO_VS.hlsl中的頂點着色器階段只用於頂點直傳:

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

VertexPosNormalColor VS(VertexPosNormalColor pIn)
{
    return pIn;
}

然後重點就在於SnowSO_GS.hlsl的幾何着色器了。這裏先放出代碼:

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

[maxvertexcount(5)]
void GS(line VertexPosColor input[2], inout LineStream<VertexPosColor> output)
{
    // 要求分形線段按順時針排布
    // z分量必須相等,因爲頂點沒有提供法向量無法判斷垂直上方向
    //                       v1
    //                       /\
    // ____________ =>  ____/  \____
    // i0         i1   i0  v0  v2  i1
    
    VertexPosColor v0, v1, v2;
    v0.Color = lerp(input[0].Color, input[1].Color, 0.25f);
    v1.Color = lerp(input[0].Color, input[1].Color, 0.5f);
    v2.Color = lerp(input[0].Color, input[1].Color, 0.75f);

    v0.PosL = lerp(input[0].PosL, input[1].PosL, 1.0f / 3.0f);
    v2.PosL = lerp(input[0].PosL, input[1].PosL, 2.0f / 3.0f);

    // xy平面求出它的垂直單位向量
    //     
    //     |
    // ____|_____
    float2 upDir = normalize(input[1].PosL - input[0].PosL).yx;
    float len = length(input[1].PosL.xy - input[0].PosL.xy);
    upDir.x = -upDir.x;

    v1.PosL = lerp(input[0].PosL, input[1].PosL, 0.5f);
    v1.PosL.xy += sqrt(3) / 6.0f * len * upDir;

    output.Append(input[0]);
    output.Append(v0);
    output.Append(v1);
    output.Append(v2);
    output.Append(input[1]);

}

可以發現分形雪花每升一階,需要繪製的頂點數就變成了上一階的4倍。

這裏要求了z分量必須相等,因爲使用的着色器仍把一切的頂點仍當做3D頂點來對待出來(當然你也可以寫成2D的着色器)。

然後開始具體分析從直線變折線的過程,可以看到因爲頂點v1所在角的度數在60度,且v0, v1, v2構成等邊三角形,故v0v2,
v0v1和v1v2的邊長是一致的。而且4條折線要求邊長相等,故這裏的i0v0和v2i1應當各佔線段i0i1的1/3.

其中lerp函數是線性插值函數,數學公式如下:
\[ \mathbf{p} = \mathbf{p}_0 + t(\mathbf{p}_1 - \mathbf{p}_0) \]

其中t的取值範圍在[0.0f, 1.0f],並且操作對象p0和p1可以是標量,也可以是矢量,對矢量來說則是對每個分量都進行線性插值。

當t = 0.5f時,描述的就是p0和p1的中值或中點。

該函數很容易描述兩點之間某一相對位置。

由於我們規定了連續線段必須按順時針排布,我們就可以利用向量i0i1逆時針旋轉90度得到對應的突出方向向量,然後標準化,乘上相應的高度值即可得到頂點v1的位置。

最後就是用於繪製的着色器代碼:

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

VertexPosHColor VS(VertexPosColor pIn)
{
    row_major matrix worldViewProj = mul(mul(gWorld, gView), gProj);
    VertexPosHColor pOut;
    pOut.Color = pIn.Color;
    pOut.PosH = mul(float4(pIn.PosL, 1.0f), worldViewProj);
    return pOut;
}
// Snow_PS.hlsl
#include "Basic.fx"

float4 PS(VertexPosHColor pIn) : SV_Target
{
    return pIn.Color;
}

實戰3: 繪製分形圓球

以下是一階和二階的分形圓球:

仔細觀察可以看到,原先的一個三角形分裂出了四個三角形,即每升一階,需要繪製的頂點數就變成了上一階的4倍。

HLSL代碼

SphereSO_VS.hlsl代碼和SphereSO_GS.hlsl代碼如下:

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

VertexPosNormalColor VS(VertexPosNormalColor pIn)
{
    return pIn;
}
// SphereSO_GS.hlsl

#include "Basic.fx"

[maxvertexcount(12)]
void GS(triangle VertexPosNormalColor input[3], inout TriangleStream<VertexPosNormalColor> output)
{
    //
    // 將一個三角形分裂成四個三角形,但同時頂點v3, v4, v5也需要在球面上
    //       v1
    //       /\
    //      /  \
    //   v3/____\v4
    //    /\xxxx/\
    //   /  \xx/  \
    //  /____\/____\
    // v0    v5    v2
    
    VertexPosNormalColor vertexes[6];

    matrix viewProj = mul(gView, gProj);

    [unroll]
    for (int i = 0; i < 3; ++i)
    {
        vertexes[i] = input[i];
        vertexes[i + 3].Color = lerp(input[i].Color, input[(i + 1) % 3].Color, 0.5f);
        vertexes[i + 3].NormalL = normalize(input[i].NormalL + input[(i + 1) % 3].NormalL);
        vertexes[i + 3].PosL = gSphereCenter + gSphereRadius * vertexes[i + 3].NormalL;
    }
        
    output.Append(vertexes[0]);
    output.Append(vertexes[3]);
    output.Append(vertexes[5]);
    output.RestartStrip();

    output.Append(vertexes[3]);
    output.Append(vertexes[4]);
    output.Append(vertexes[5]);
    output.RestartStrip();

    output.Append(vertexes[5]);
    output.Append(vertexes[4]);
    output.Append(vertexes[2]);
    output.RestartStrip();

    output.Append(vertexes[3]);
    output.Append(vertexes[1]);
    output.Append(vertexes[4]);
}

由於v3, v4, v5也需要在球面上,我們還需要額外知道球的半徑和球心位置。雖然說通過三角形三個頂點位置和法向量可以算出圓心和半徑,但直接從常量緩衝區提供這兩個信息會更方便一些。

要計算諸如v3頂點所在位置,我們可以先求出它的法向量,將v0和v1的法向量相加取其單位向量即爲v3的法向量,然後從圓心開始加上半徑長度的法向量即可得到頂點v3的位置。

剩下繪製圓的着色器代碼如下:

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

VertexPosHWNormalColor VS(VertexPosNormalColor pIn)
{
    VertexPosHWNormalColor pOut;
    row_major matrix viewProj = mul(gView, gProj);
    pOut.PosW = mul(float4(pIn.PosL, 1.0f), gWorld).xyz;
    pOut.PosH = mul(float4(pOut.PosW, 1.0f), viewProj);
    pOut.NormalW = mul(pIn.NormalL, (float3x3) gWorldInvTranspose);
    pOut.Color = pIn.Color;
    return pOut;
}
// Sphere_PS.hlsl
#include "Basic.fx"

float4 PS(VertexPosHWNormalColor pIn) : SV_Target
{
    // 標準化法向量
    pIn.NormalW = normalize(pIn.NormalW);

    // 頂點指向眼睛的向量
    float3 toEyeW = normalize(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);

    // 只計算方向光
    ComputeDirectionalLight(gMaterial, gDirLight, pIn.NormalW, toEyeW, ambient, diffuse, spec);

    return pIn.Color * (ambient + diffuse) + spec;
}

C++代碼的變化

BasicFX.h的變化

首先是常量緩衝區的變化,新增了存儲球心位置和半徑信息:

struct CBChangesEveryFrame
{
    DirectX::XMMATRIX world;
    DirectX::XMMATRIX worldInvTranspose;
};

struct CBChangesOnResize
{
    DirectX::XMMATRIX proj;
};

struct CBNeverChange
{
    DirectionalLight dirLight;
    Material material;
    DirectX::XMMATRIX view;
    DirectX::XMFLOAT3 sphereCenter;
    float sphereRadius;
    DirectX::XMFLOAT3 eyePos;
    float pad;
};

然後是BasicFX類的變化,其中法向量的繪製可以用於球體繪製的時候,具體實現可以回顧上一章。

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 SetRenderSplitedTriangle();
    // 繪製雪花
    void SetRenderSplitedSnow();
    // 繪製球體
    void SetRenderSplitedSphere();
    // 通過流輸出階段獲取三角形分裂的下一階分形
    void SetStreamOutputSplitedTriangle(ComPtr<ID3D11Buffer> vertexBufferIn, ComPtr<ID3D11Buffer> vertexBufferOut);
    // 通過流輸出階段獲取雪花的下一階分形
    void SetStreamOutputSplitedSnow(ComPtr<ID3D11Buffer> vertexBufferIn, ComPtr<ID3D11Buffer> vertexBufferOut);
    // 通過流輸出階段獲取球的下一階分形
    void SetStreamOutputSplitedSphere(ComPtr<ID3D11Buffer> vertexBufferIn, ComPtr<ID3D11Buffer> vertexBufferOut);

    // 繪製所有頂點的法向量
    void SetRenderNormal();

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

private:
    ComPtr<ID3D11VertexShader> mTriangleSOVS;
    ComPtr<ID3D11GeometryShader> mTriangleSOGS;

    ComPtr<ID3D11VertexShader> mTriangleVS;
    ComPtr<ID3D11PixelShader> mTrianglePS;

    ComPtr<ID3D11VertexShader> mSphereSOVS;
    ComPtr<ID3D11GeometryShader> mSphereSOGS;

    ComPtr<ID3D11VertexShader> mSphereVS;
    ComPtr<ID3D11PixelShader> mSpherePS;
    
    ComPtr<ID3D11VertexShader> mSnowSOVS;
    ComPtr<ID3D11GeometryShader> mSnowSOGS;

    ComPtr<ID3D11VertexShader> mSnowVS;
    ComPtr<ID3D11PixelShader> mSnowPS;

    ComPtr<ID3D11VertexShader> mNormalVS;
    ComPtr<ID3D11GeometryShader> mNormalGS;
    ComPtr<ID3D11PixelShader> mNormalPS;

    ComPtr<ID3D11InputLayout> mVertexPosColorLayout;        // VertexPosColor輸入佈局
    ComPtr<ID3D11InputLayout> mVertexPosNormalColorLayout;  // VertexPosNormalColor輸入佈局

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

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

BasicFX::InitAll方法的變化

現在着色器的創建按繪製類別進行分組:

bool BasicFX::InitAll(ComPtr<ID3D11Device> device)
{
    if (!device)
        return false;

    const D3D11_SO_DECLARATION_ENTRY posColorLayout[2] = {
        { 0, "POSITION", 0, 0, 3, 0 },
        { 0, "COLOR", 0, 0, 4, 0 }
    };

    const D3D11_SO_DECLARATION_ENTRY posNormalColorLayout[3] = {
        { 0, "POSITION", 0, 0, 3, 0 },
        { 0, "NORMAL", 0, 0, 3, 0 },
        { 0, "COLOR", 0, 0, 4, 0 }
    };

    UINT stridePosColor = sizeof(VertexPosColor);
    UINT stridePosNormalColor = sizeof(VertexPosNormalColor);

    ComPtr<ID3DBlob> blob;

    //
    // 流輸出分裂三角形
    //
    HR(CreateShaderFromFile(L"HLSL\\TriangleSO_VS.vso", L"HLSL\\TriangleSO_VS.hlsl", "VS", "vs_5_0", blob.ReleaseAndGetAddressOf()));
    HR(device->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, mTriangleSOVS.GetAddressOf()));
    // 創建頂點輸入佈局
    HR(device->CreateInputLayout(VertexPosColor::inputLayout, ARRAYSIZE(VertexPosColor::inputLayout), blob->GetBufferPointer(),
        blob->GetBufferSize(), mVertexPosColorLayout.GetAddressOf()));
    HR(CreateShaderFromFile(L"HLSL\\TriangleSO_GS.gso", L"HLSL\\TriangleSO_GS.hlsl", "GS", "gs_5_0", blob.ReleaseAndGetAddressOf()));
    HR(device->CreateGeometryShaderWithStreamOutput(blob->GetBufferPointer(), blob->GetBufferSize(), posColorLayout, ARRAYSIZE(posColorLayout),
        &stridePosColor, 1, D3D11_SO_NO_RASTERIZED_STREAM, nullptr, mTriangleSOGS.GetAddressOf()));
    
    //
    // 繪製分形三角形
    //
    HR(CreateShaderFromFile(L"HLSL\\Triangle_VS.vso", L"HLSL\\Triangle_VS.hlsl", "VS", "vs_5_0", blob.ReleaseAndGetAddressOf()));
    HR(device->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, mTriangleVS.GetAddressOf()));
    HR(CreateShaderFromFile(L"HLSL\\Triangle_PS.pso", L"HLSL\\Triangle_PS.hlsl", "PS", "ps_5_0", blob.ReleaseAndGetAddressOf()));
    HR(device->CreatePixelShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, mTrianglePS.GetAddressOf()));


    //
    // 流輸出分形球體
    //
    HR(CreateShaderFromFile(L"HLSL\\SphereSO_VS.vso", L"HLSL\\SphereSO_VS.hlsl", "VS", "vs_5_0", blob.ReleaseAndGetAddressOf()));
    HR(device->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, mSphereSOVS.GetAddressOf()));
    // 創建頂點輸入佈局
    HR(device->CreateInputLayout(VertexPosNormalColor::inputLayout, ARRAYSIZE(VertexPosNormalColor::inputLayout), blob->GetBufferPointer(),
        blob->GetBufferSize(), mVertexPosNormalColorLayout.GetAddressOf()));
    HR(CreateShaderFromFile(L"HLSL\\SphereSO_GS.gso", L"HLSL\\SphereSO_GS.hlsl", "GS", "gs_5_0", blob.ReleaseAndGetAddressOf()));
    HR(device->CreateGeometryShaderWithStreamOutput(blob->GetBufferPointer(), blob->GetBufferSize(), posNormalColorLayout, ARRAYSIZE(posNormalColorLayout),
        &stridePosNormalColor, 1, D3D11_SO_NO_RASTERIZED_STREAM, nullptr, mSphereSOGS.GetAddressOf()));
    
    //
    // 繪製球體
    //
    HR(CreateShaderFromFile(L"HLSL\\Sphere_VS.vso", L"HLSL\\Sphere_VS.hlsl", "VS", "vs_5_0", blob.ReleaseAndGetAddressOf()));
    HR(device->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, mSphereVS.GetAddressOf()));
    HR(CreateShaderFromFile(L"HLSL\\Sphere_PS.pso", L"HLSL\\Sphere_PS.hlsl", "PS", "ps_5_0", blob.ReleaseAndGetAddressOf()));
    HR(device->CreatePixelShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, mSpherePS.GetAddressOf()));
    

    //
    // 流輸出分形雪花
    //
    HR(CreateShaderFromFile(L"HLSL\\SnowSO_VS.vso", L"HLSL\\SnowSO_VS.hlsl", "VS", "vs_5_0", blob.ReleaseAndGetAddressOf()));
    HR(device->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, mSnowSOVS.GetAddressOf()));
    HR(CreateShaderFromFile(L"HLSL\\SnowSO_GS.gso", L"HLSL\\SnowSO_GS.hlsl", "GS", "gs_5_0", blob.ReleaseAndGetAddressOf()));
    HR(device->CreateGeometryShaderWithStreamOutput(blob->GetBufferPointer(), blob->GetBufferSize(), posColorLayout, ARRAYSIZE(posColorLayout),
        &stridePosColor, 1, D3D11_SO_NO_RASTERIZED_STREAM, nullptr, mSnowSOGS.GetAddressOf()));

    //
    // 繪製雪花
    //
    HR(CreateShaderFromFile(L"HLSL\\Snow_VS.vso", L"HLSL\\Snow_VS.hlsl", "VS", "vs_5_0", blob.ReleaseAndGetAddressOf()));
    HR(device->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, mSnowVS.GetAddressOf()));
    HR(CreateShaderFromFile(L"HLSL\\Snow_PS.pso", L"HLSL\\Snow_PS.hlsl", "PS", "ps_5_0", blob.ReleaseAndGetAddressOf()));
    HR(device->CreatePixelShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, mSnowPS.GetAddressOf()));


    //
    // 繪製法向量
    //
    HR(CreateShaderFromFile(L"HLSL\\Normal_VS.vso", L"HLSL\\Normal_VS.hlsl", "VS", "vs_5_0", blob.ReleaseAndGetAddressOf()));
    HR(device->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, mNormalVS.GetAddressOf()));
    HR(CreateShaderFromFile(L"HLSL\\Normal_GS.gso", L"HLSL\\Normal_GS.hlsl", "GS", "gs_5_0", blob.ReleaseAndGetAddressOf()));
    HR(device->CreateGeometryShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, mNormalGS.GetAddressOf()));
    HR(CreateShaderFromFile(L"HLSL\\Normal_PS.pso", L"HLSL\\Normal_PS.hlsl", "PS", "ps_5_0", blob.ReleaseAndGetAddressOf()));
    HR(device->CreatePixelShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, mNormalPS.GetAddressOf()));

    

    RenderStates::InitAll(device);
    device->GetImmediateContext(md3dImmediateContext.GetAddressOf());

    // ******************
    // 設置常量緩衝區描述
    mConstantBuffers.assign(3, nullptr);
    D3D11_BUFFER_DESC cbd;
    ZeroMemory(&cbd, sizeof(cbd));
    cbd.Usage = D3D11_USAGE_DEFAULT;
    cbd.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
    cbd.CPUAccessFlags = 0;

    cbd.ByteWidth = Align16Bytes(sizeof(CBChangesEveryFrame));
    HR(device->CreateBuffer(&cbd, nullptr, mConstantBuffers[0].GetAddressOf()));
    cbd.ByteWidth = Align16Bytes(sizeof(CBChangesOnResize));
    HR(device->CreateBuffer(&cbd, nullptr, mConstantBuffers[1].GetAddressOf()));
    cbd.ByteWidth = Align16Bytes(sizeof(CBNeverChange));
    HR(device->CreateBuffer(&cbd, nullptr, mConstantBuffers[2].GetAddressOf()));

    // 預先綁定各自所需的緩衝區
    md3dImmediateContext->VSSetConstantBuffers(0, 1, mConstantBuffers[0].GetAddressOf());
    md3dImmediateContext->VSSetConstantBuffers(1, 1, mConstantBuffers[1].GetAddressOf());
    md3dImmediateContext->VSSetConstantBuffers(2, 1, mConstantBuffers[2].GetAddressOf());

    md3dImmediateContext->GSSetConstantBuffers(0, 1, mConstantBuffers[0].GetAddressOf());
    md3dImmediateContext->GSSetConstantBuffers(1, 1, mConstantBuffers[1].GetAddressOf());
    md3dImmediateContext->GSSetConstantBuffers(2, 1, mConstantBuffers[2].GetAddressOf());

    md3dImmediateContext->PSSetConstantBuffers(2, 1, mConstantBuffers[2].GetAddressOf());
    return true;
}

BasicFX::SetRenderSplitedTriangle方法--繪製分形三角形

由於新增了流輸出的階段,這裏開始接下來的每一個用於繪製的方法都需要把流輸出綁定的頂點緩衝區都解除綁定。

void BasicFX::SetRenderSplitedTriangle()
{
    md3dImmediateContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
    md3dImmediateContext->IASetInputLayout(mVertexPosColorLayout.Get());
    md3dImmediateContext->VSSetShader(mTriangleVS.Get(), nullptr, 0);
    // 關閉流輸出
    md3dImmediateContext->GSSetShader(nullptr, nullptr, 0);
    ID3D11Buffer* bufferArray[1] = { nullptr };
    UINT offset = 0;
    md3dImmediateContext->SOSetTargets(1, bufferArray, &offset);
    md3dImmediateContext->RSSetState(nullptr);
    md3dImmediateContext->PSSetShader(mTrianglePS.Get(), nullptr, 0);
}

BasicFX::SetRenderSplitedSnow方法--繪製分形雪花

void BasicFX::SetRenderSplitedSnow()
{
    md3dImmediateContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_LINELIST);
    md3dImmediateContext->IASetInputLayout(mVertexPosColorLayout.Get());
    md3dImmediateContext->VSSetShader(mSnowVS.Get(), nullptr, 0);
    // 關閉流輸出
    md3dImmediateContext->GSSetShader(nullptr, nullptr, 0);
    ID3D11Buffer* bufferArray[1] = { nullptr };
    UINT offset = 0;
    md3dImmediateContext->SOSetTargets(1, bufferArray, &offset);
    md3dImmediateContext->RSSetState(nullptr);
    md3dImmediateContext->PSSetShader(mSnowPS.Get(), nullptr, 0);
}

BasicFX::SetRenderSplitedSphere方法--繪製分形球體

void BasicFX::SetRenderSplitedSphere()
{
    md3dImmediateContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
    md3dImmediateContext->IASetInputLayout(mVertexPosNormalColorLayout.Get());
    md3dImmediateContext->VSSetShader(mSphereVS.Get(), nullptr, 0);
    // 關閉流輸出
    md3dImmediateContext->GSSetShader(nullptr, nullptr, 0);
    ID3D11Buffer* bufferArray[1] = { nullptr };
    UINT offset = 0;
    md3dImmediateContext->SOSetTargets(1, bufferArray, &offset);
    md3dImmediateContext->RSSetState(nullptr);
    md3dImmediateContext->PSSetShader(mSpherePS.Get(), nullptr, 0);
}

BasicFX::SetStreamOutputSplitedTriangle方法--經過流輸出保存下一階分形三角形的頂點

爲了簡化設置,這裏還需要提供額外的輸入緩衝區和輸出緩衝區。爲了防止出現頂點緩衝區同時被綁定到輸入裝配和流輸出階段的情況,需要先清空流輸出綁定的頂點緩衝區,然後將用於輸入的頂點緩衝區綁定到輸入裝配階段,最後纔是把輸出的頂點緩衝區綁定到流輸出階段。

void BasicFX::SetStreamOutputSplitedTriangle(ComPtr<ID3D11Buffer> vertexBufferIn, ComPtr<ID3D11Buffer> vertexBufferOut)
{
    // 先恢復流輸出默認設置,防止頂點緩衝區同時綁定在輸入和輸出階段
    UINT stride = sizeof(VertexPosColor);
    UINT offset = 0;
    ID3D11Buffer * nullBuffer = nullptr;
    md3dImmediateContext->SOSetTargets(1, &nullBuffer, &offset);

    md3dImmediateContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
    md3dImmediateContext->IASetInputLayout(mVertexPosColorLayout.Get());

    md3dImmediateContext->IASetVertexBuffers(0, 1, vertexBufferIn.GetAddressOf(), &stride, &offset);

    md3dImmediateContext->VSSetShader(mTriangleSOVS.Get(), nullptr, 0);
    md3dImmediateContext->GSSetShader(mTriangleSOGS.Get(), nullptr, 0);

    md3dImmediateContext->SOSetTargets(1, vertexBufferOut.GetAddressOf(), &offset);
;
    md3dImmediateContext->RSSetState(nullptr);
    md3dImmediateContext->PSSetShader(nullptr, nullptr, 0);
}

BasicFX::SetStreamOutputSplitedSnow方法--經過流輸出保存下一階分形雪花的頂點

注意這裏是用LineList而不是LineStrip方式。

void BasicFX::SetStreamOutputSplitedSnow(ComPtr<ID3D11Buffer> vertexBufferIn, ComPtr<ID3D11Buffer> vertexBufferOut)
{
    // 先恢復流輸出默認設置,防止頂點緩衝區同時綁定在輸入和輸出階段
    UINT stride = sizeof(VertexPosColor);
    UINT offset = 0;
    ID3D11Buffer * nullBuffer = nullptr;
    md3dImmediateContext->SOSetTargets(1, &nullBuffer, &offset);

    md3dImmediateContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_LINELIST);
    md3dImmediateContext->IASetInputLayout(mVertexPosColorLayout.Get());
    md3dImmediateContext->IASetVertexBuffers(0, 1, vertexBufferIn.GetAddressOf(), &stride, &offset);

    md3dImmediateContext->VSSetShader(mSnowSOVS.Get(), nullptr, 0);
    md3dImmediateContext->GSSetShader(mSnowSOGS.Get(), nullptr, 0);

    md3dImmediateContext->SOSetTargets(1, vertexBufferOut.GetAddressOf(), &offset);

    md3dImmediateContext->RSSetState(nullptr);
    md3dImmediateContext->PSSetShader(nullptr, nullptr, 0);
}

BasicFX::SetStreamOutputSplitedSphere方法--經過流輸出保存下一階分形球體的頂點

void BasicFX::SetStreamOutputSplitedSphere(ComPtr<ID3D11Buffer> vertexBufferIn, ComPtr<ID3D11Buffer> vertexBufferOut)
{
    // 先恢復流輸出默認設置,防止頂點緩衝區同時綁定在輸入和輸出階段
    UINT stride = sizeof(VertexPosNormalColor);
    UINT offset = 0;
    ID3D11Buffer * nullBuffer = nullptr;
    md3dImmediateContext->SOSetTargets(1, &nullBuffer, &offset);

    md3dImmediateContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
    md3dImmediateContext->IASetInputLayout(mVertexPosNormalColorLayout.Get());
    md3dImmediateContext->IASetVertexBuffers(0, 1, vertexBufferIn.GetAddressOf(), &stride, &offset);

    md3dImmediateContext->VSSetShader(mSphereSOVS.Get(), nullptr, 0);
    md3dImmediateContext->GSSetShader(mSphereSOGS.Get(), nullptr, 0);

    md3dImmediateContext->SOSetTargets(1, vertexBufferOut.GetAddressOf(), &offset);

    md3dImmediateContext->RSSetState(nullptr);
    md3dImmediateContext->PSSetShader(nullptr, nullptr, 0);
}

GameApp類的變化

GameApp類的變化如下:

class GameApp : public D3DApp
{
public:
    enum class Mode { SplitedTriangle, SplitedSnow, SplitedSphere };
    
public:
    GameApp(HINSTANCE hInstance);
    ~GameApp();

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

private:
    bool InitResource();

    void ResetSplitedTriangle();
    void ResetSplitedSnow();
    void ResetSplitedSphere();


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

    ComPtr<ID3D11Buffer> mVertexBuffers[7];                 // 頂點緩衝區數組
    int mVertexCounts[7];                                   // 頂點數目
    int mCurrIndex;                                         // 當前索引
    Mode mShowMode;                                         // 當前顯示模式
    bool mIsWireFrame;                                      // 是否爲線框模式
    bool mShowNormal;                                       // 是否顯示法向量
    BasicFX mBasicFX;                                       // Basic特效管理類

    CBChangesEveryFrame mCBChangeEveryFrame;                // 該緩衝區存放每幀更新的變量
    CBChangesOnResize mCBOnReSize;                          // 該緩衝區存放僅在窗口大小變化時更新的變量
    CBNeverChange mCBNeverChange;                           // 該緩衝區存放不會再進行修改的變量
};

#endif

GameApp::ResetSplitedTriangle方法--重新建立包含1-7階的分形三角形頂點的緩衝區

首先我們只需要給1階的頂點緩衝區使用指定三角形的三個頂點,然後後續階數的頂點緩衝區就根據上一階產出的頂點緩衝區進行"繪製"。經過6次Draw方法的調用後,裏面的7個頂點緩衝區都應該被初始化完畢,後續繪製的時候只需要直接綁定某一個頂點緩衝區到輸入即可。

注意頂點緩衝區在創建的時候一定要加上D3D11_BIND_STREAM_OUTPUT標籤。

void GameApp::ResetSplitedTriangle()
{
    // ******************
    // 初始化三角形
    // 設置三角形頂點
    VertexPosColor vertices[] =
    {
        { XMFLOAT3(-1.0f * 3, -0.866f * 3, 0.0f), XMFLOAT4(1.0f, 0.0f, 0.0f, 1.0f) },
        { XMFLOAT3(0.0f * 3, 0.866f * 3, 0.0f), XMFLOAT4(0.0f, 1.0f, 0.0f, 1.0f) },
        { XMFLOAT3(1.0f * 3, -0.866f * 3, 0.0f), XMFLOAT4(0.0f, 0.0f, 1.0f, 1.0f) }
    };
    // 設置頂點緩衝區描述
    D3D11_BUFFER_DESC vbd;
    ZeroMemory(&vbd, sizeof(vbd));
    vbd.Usage = D3D11_USAGE_DEFAULT;
    vbd.ByteWidth = sizeof vertices;
    vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER | D3D11_BIND_STREAM_OUTPUT;    // 需要額外添加流輸出標籤
    vbd.CPUAccessFlags = 0;
    // 新建頂點緩衝區
    D3D11_SUBRESOURCE_DATA InitData;
    ZeroMemory(&InitData, sizeof(InitData));
    InitData.pSysMem = vertices;
    HR(md3dDevice->CreateBuffer(&vbd, &InitData, mVertexBuffers[0].ReleaseAndGetAddressOf()));

    
    // 三角形頂點數
    mVertexCounts[0] = 3;
    // 初始化所有頂點緩衝區
    for (int i = 1; i < 7; ++i)
    {
        vbd.ByteWidth *= 3;
        mVertexCounts[i] = mVertexCounts[i - 1] * 3;
        HR(md3dDevice->CreateBuffer(&vbd, nullptr, mVertexBuffers[i].ReleaseAndGetAddressOf()));
        mBasicFX.SetStreamOutputSplitedTriangle(mVertexBuffers[i - 1], mVertexBuffers[i]);
        md3dImmediateContext->Draw(mVertexCounts[i - 1], 0);
    }
}

GameApp::ResetSplitedSnow方法--重新建立包含1-7階的分形雪花頂點的緩衝區

由於繪製方式統一用LineList,初始階段應當提供3條線段的6個頂點,雖然說每個頂點都被重複使用了2次。

void GameApp::ResetSplitedSnow()
{
    // ******************
    // 雪花分形從初始化三角形開始,需要6個頂點
    // 設置三角形頂點
    float sqrt3 = sqrt(3.0f);
    VertexPosColor vertices[] =
    {
        { XMFLOAT3(-3.0f / 4, -sqrt3 / 4, 0.0f), XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f) },
        { XMFLOAT3(0.0f, sqrt3 / 2, 0.0f), XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f) },
        { XMFLOAT3(0.0f, sqrt3 / 2, 0.0f), XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f) },
        { XMFLOAT3(3.0f / 4, -sqrt3 / 4, 0.0f), XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f) },
        { XMFLOAT3(3.0f / 4, -sqrt3 / 4, 0.0f), XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f) },
        { XMFLOAT3(-3.0f / 4, -sqrt3 / 4, 0.0f), XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f) }
    };
    // 將三角形寬度和高度都放大3倍
    for (VertexPosColor& v : vertices)
    {
        v.pos.x *= 3;
        v.pos.y *= 3;
    }

    // 設置頂點緩衝區描述
    D3D11_BUFFER_DESC vbd;
    ZeroMemory(&vbd, sizeof(vbd));
    vbd.Usage = D3D11_USAGE_DEFAULT;
    vbd.ByteWidth = sizeof vertices;
    vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER | D3D11_BIND_STREAM_OUTPUT;    // 需要額外添加流輸出標籤
    vbd.CPUAccessFlags = 0;
    // 新建頂點緩衝區
    D3D11_SUBRESOURCE_DATA InitData;
    ZeroMemory(&InitData, sizeof(InitData));
    InitData.pSysMem = vertices;
    HR(md3dDevice->CreateBuffer(&vbd, &InitData, mVertexBuffers[0].ReleaseAndGetAddressOf()));

    // 頂點數
    mVertexCounts[0] = 6;
    // 初始化所有頂點緩衝區
    for (int i = 1; i < 7; ++i)
    {
        vbd.ByteWidth *= 4;
        mVertexCounts[i] = mVertexCounts[i - 1] * 4;
        HR(md3dDevice->CreateBuffer(&vbd, nullptr, mVertexBuffers[i].ReleaseAndGetAddressOf()));
        mBasicFX.SetStreamOutputSplitedSnow(mVertexBuffers[i - 1], mVertexBuffers[i]);
        md3dImmediateContext->Draw(mVertexCounts[i - 1], 0);
    }
}

GameApp::ResetSplitedSphere方法--重新建立包含1-7階的分形圓球頂點的緩衝區

這裏不使用Geometry類來構造一階圓球,而是僅提供與外接正方體相交的六個頂點,包含八個三角形對應的24個頂點。

void GameApp::ResetSplitedSphere()
{
    VertexPosNormalColor basePoint[] = {
        { XMFLOAT3(0.0f, 2.0f, 0.0f), XMFLOAT3(0.0f, 1.0f, 0.0f), XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f) },
        { XMFLOAT3(2.0f, 0.0f, 0.0f), XMFLOAT3(1.0f, 0.0f, 0.0f), XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f) },
        { XMFLOAT3(0.0f, 0.0f, 2.0f), XMFLOAT3(0.0f, 0.0f, 1.0f), XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f) },
        { XMFLOAT3(-2.0f, 0.0f, 0.0f), XMFLOAT3(-1.0f, 0.0f, 0.0f), XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f) },
        { XMFLOAT3(0.0f, 0.0f, -2.0f), XMFLOAT3(0.0f, 0.0f, -1.0f), XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f) },
        { XMFLOAT3(0.0f, -2.0f, 0.0f), XMFLOAT3(0.0f, -1.0f, 0.0f), XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f) },
    };
    int indices[] = {0, 2, 1, 0, 3, 2, 0, 4, 3, 0, 1, 4, 1, 2, 5, 2, 3, 5, 3, 4, 5, 4, 1, 5};

    std::vector<VertexPosNormalColor> vertices;
    for (int pos : indices)
    {
        vertices.push_back(basePoint[pos]);
    }


    // 設置頂點緩衝區描述
    D3D11_BUFFER_DESC vbd;
    ZeroMemory(&vbd, sizeof(vbd));
    vbd.Usage = D3D11_USAGE_DEFAULT;
    vbd.ByteWidth = (UINT)(vertices.size() * sizeof(VertexPosNormalColor));
    vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER | D3D11_BIND_STREAM_OUTPUT;    // 需要額外添加流輸出標籤
    vbd.CPUAccessFlags = 0;
    // 新建頂點緩衝區
    D3D11_SUBRESOURCE_DATA InitData;
    ZeroMemory(&InitData, sizeof(InitData));
    InitData.pSysMem = vertices.data();
    HR(md3dDevice->CreateBuffer(&vbd, &InitData, mVertexBuffers[0].ReleaseAndGetAddressOf()));

    // 頂點數
    mVertexCounts[0] = 24;
    // 初始化所有頂點緩衝區
    for (int i = 1; i < 7; ++i)
    {
        vbd.ByteWidth *= 4;
        mVertexCounts[i] = mVertexCounts[i - 1] * 4;
        HR(md3dDevice->CreateBuffer(&vbd, nullptr, mVertexBuffers[i].ReleaseAndGetAddressOf()));
        mBasicFX.SetStreamOutputSplitedSphere(mVertexBuffers[i - 1], mVertexBuffers[i]);
        md3dImmediateContext->Draw(mVertexCounts[i - 1], 0);
    }
}

由於篇幅過大,該類的其它方法就在此省略,具體細節可以查閱該章對應源碼。來看一下動圖感受一些這些酷炫的效果吧:

分形三角形繪製效果

分形雪花繪製效果

分形圓球繪製效果

由於文件大小限制,這裏分成兩個部分:

下面是帶法向量的:

遺留問題

由於我現在找不到ID3D11DeviceContext::DrawAuto方法無法輸出的原因,故在這裏使用的依然是原來的Draw方法進行繪製。若有人能夠在不使用FX11框架的情況下能夠調用DrawAuto方法繪製到流輸出,可以在下面評論我,並告訴我解決方案。

除此之外,該項目使用圖形調試器並退出的時候,會引發內存泄漏,而具體的泄漏對象估計是ID3D11Query,然而我也沒有辦法直接拿到該接口對象來釋放。

DirectX11 With Windows SDK完整目錄

Github項目源碼

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章