第六章 使用Direct3D繪製

第五章主要關注渲染管道的概念和數學方面。本章重點介紹配置渲染管道所需的Direct3D API接口和方法,定義頂點和像素着色器,並將幾何圖形提交給繪製管道進行繪製。學完本章,您將能夠繪製各種幾何形狀的着色或線框模式。

學習目標:
1.發現用於定義,存儲和繪製幾何數據的Direct3D接口方法。
2.學習如何編寫基本的頂點和像素着色器。
3.瞭解如何使用渲染狀態配置渲染管道。
4.瞭解如何將效果框架用於將着色器和渲染狀態邏輯分組到渲染技術中,以及如何使用將效果框架用作“着色器生成器”。

6.1 定點與輸入佈局

回顧§5.5.1,Direct3D中的頂點除空間位置之外還可以附加數據。要創建自定義頂點格式,我們首先創建一個保存我們選擇的頂點數據的結構。下面說明兩種不同種類的頂點格式; 一個由位置和顏色組成,第二個由位置,normal和紋理座標組成。

struct Vertex1
{
    XMFLOAT3 Pos;
    XMFLOAT4 Color;
};

struct Vertex2
{
    XMFLOAT3 Pos;
    XMFLOAT3 Normal;
    XMFLOAT2 Tex0;
    XMFLOAT2 Tex1;
};

一旦我們定義了一個頂點結構,我們需要向Direct3D提供我們的頂點結構的描述,以便它知道如何處理每個組件。該描述以輸入佈局(ID3D11InputLayout)的形式提供給Direct3D。輸入佈局由D3D11_INPUT_ELEMENT_DESC元素的數組指定。D3D11_INPUT_ELEMENT_DESC數組中的每個元素都描述並對應於頂點結構中的一個組件。所以如果頂點結構有兩個組件,對應的D3D11_INPUT_ELEMENT_DESC數組將有兩個元素。我們將D3D11_INPUT_ELEMENT_DESC數組稱爲輸入佈局描述。D3D11_INPUT_ELEMENT_DESC結構定義爲:

typedef struct D3D11_INPUT_ELEMENT_DESC {
    LPCSTR SemanticName;
    UINT SemanticIndex;
    DXGI_FORMAT Format;
    UINT InputSlot;
    UINT AlignedByteOffset;
    D3D11_INPUT_CLASSIFICATION InputSlotClass;
    UINT InstanceDataStepRate;
} D3D11_INPUT_ELEMENT_DESC;

1 . SemanticName:與元素關聯的字符串。可以是任何有效的變量名。用於將頂點結構中的元素映射到頂點着色器輸入簽名中的元素; 見圖6.1。
2 . SemanticIndex:附加到語義的索引。 圖6.1中也說明了這一點,其中頂點結構可能具有多於一組的紋理座標; 所以不是引入一個新的語義名稱,我們可以附加一個索引來區分紋理座標。 在着色器代碼中指定的沒有索引的語義默認爲索引零; 例如,POSITIO N相當於圖6.1中的POSITION 0

6-1
圖6.1 頂點結構中的每個元素都由D3D11_INPUT_ELEMENT_DESC數組中的相應元素描述。語義名稱和索引提供了一種將頂點元素映射到頂點着色器的相應參數的方法。

3.Format:DXGI_FORMAT枚舉類型的一個成員,它將該頂點元素的格式(t.i.e.,數據類型)指定爲Direct3D; 這裏有一些常用的格式示例:

DXGI_FORMAT_R32_FLOAT // 1D 32-bit float scalar
DXGI_FORMAT_R32G32_FLOAT // 2D 32-bit float vector
DXGI_FORMAT_R32G32B32_FLOAT // 3D 32-bit float vector
DXGI_FORMAT_R32G32B32A32_FLOAT // 4D 32-bit float vector

DXGI_FORMAT_R8_UINT // 1D 8-bit unsigned integer scalar
DXGI_FORMAT_R16G16_SINT // 2D 16-bit signed integer vector
DXGI_FORMAT_R32G32B32_UINT // 3D 32-bit unsigned integer vector
DXGI_FORMAT_R8G8B8A8_SINT // 4D 8-bit signed integer vector
DXGI_FORMAT_R8G8B8A8_UINT // 4D 8-bit unsigned integer vector

4 . InputSlot:指定此元素來自的輸入槽索引。D3D支持十六個輸入插槽(索引從0-15),您可以通過這些插槽提供頂點數據。例如,如果一個頂點由位置和顏色元素組成,那麼您可以通過單個輸入插槽傳輸兩個元素,也可以將元素分開,並通過第一個輸入插槽饋送位置元素,並通過第二個插槽輸入顏色元素。然後,D3D將使用來自不同輸入槽的元素來組合頂點。在這裏,我們只使用一個輸入插槽,但練習2要求用兩個。

5 .AlignedByteOffset:對於單個輸入插槽,這是從C ++頂點結構開始到頂點組件開始的偏移量(以字節爲單位)。例如,在以下頂點結構中,元素Pos具有0字節的偏移,因爲它的起始與頂點結構的開始重合;元素Normal具有12字節的偏移量,因爲我們必須跳過Pos的字節才能達到它的開始;元素Tex0有一個24字節的偏移量,因爲我們需要跳過Pos和Normal的字節才能達到Tex0的開始;元素Tex1具有32字節的偏移量,因爲我們需要跳過Pos,Normal和Tex0的字節以獲得Tex1的開始。

struct Vertex2
{
    XMFLOAT3 Pos; // 0-byte offset
    XMFLOAT3 Normal; // 12-byte offset
    XMFLOAT2 Tex0; // 24-byte offset
    XMFLOAT2 Tex1; // 32-byte offset
};

6 . InputSlotClass:現在指定D3D11_INPUT_PER_VERTEX_DATA; 實例化中的另一個高級技術選項。
7 . InstanceDataStepRate:目前爲0; 其他值僅用於實例的高級技術。對於前兩個示例頂點結構Vertex1Vertex2,相應的輸入佈局描述將是:

D3D11_INPUT_ELEMENT_DESC desc1[] =
{
    {"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}
};

D3D11_INPUT_ELEMENT_DESC desc2[] =
{
    {"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0,
        D3D11_INPUT_PER_VERTEX_DATA, 0},
    {"NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 12,
        D3D11_INPUT_PER_VERTEX_DATA, 0},
    {"TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 24,
        D3D11_INPUT_PER_VERTEX_DATA, 0},
    {"TEXCOORD", 1, DXGI_FORMAT_R32G32_FLOAT, 0, 32,
        D3D11_INPUT_PER_VERTEX_DATA, 0}
};

一旦指定了輸入佈局描述,我們可以使用ID3D11Device :: CreateInputLayout方法獲取指向D3D11InputLayout接口的指針,該接口代表輸入佈局。

HRESULT ID3D11Device::CreateInputLayout(
    const D3D11_INPUT_ELEMENT_DESC *pInputElementDescs,
    UINT NumElements,
    const void *pShaderBytecodeWithInputSignature,
    SIZE_T BytecodeLength,
    ID3D11InputLayout **ppInputLayout);

1.pInputElementDescs:描述頂點結構的D3D11_INPUT_ELEMENT_DESC元素數組。
2.NumElementsD3D11_INPUT_ELEMENT_DESC元素數組中的元素數。
3.pShaderBytecodeWithInputSignature:指向頂點着色器的輸入簽名的着色器字節代碼的指針。
4.BytecodeLength:傳入上一個參數的頂點着色器簽名數據的字節大小。
5.ppInputLayout:返回指向創建的輸入佈局的指針。
參數3需要一些細節。頂點着色器將頂點元素列表作爲輸入參數(即所謂的輸入簽名)。 定製頂點結構中的元素需要映射到頂點着色器中的相應輸入; 圖6.1顯示了這一點。 通過在創建輸入佈局時傳遞頂點着色器輸入簽名的描述,Direct3D可以驗證輸入佈局與輸入簽名匹配,並在創建時創建從頂點結構到着色器輸入的映射。只要輸入簽名完全相同,輸入佈局可以通過不同着色器重複使用。

考慮以下情況,您具有以下輸入簽名和頂點結構:

VertexOut VS(float3 Pos : POSITION, float4 Color : COLOR,
    float3 Normal : NORMAL) { }

struct Vertex
{
    XMFLOAT3 Pos;
    XMFLOAT4 Color;
};

這將產生錯誤,VC ++調試輸出窗口顯示以下內容:

D3D11: ERROR: ID3D11Device::CreateInputLayout: The provided input signature expects to read an element with SemanticName/Index: ‘NORMAL’/0, but the declaration doesn’t provide a matching name.

現在考慮頂點結構和輸入簽名具有匹配的頂點元素的情況,但是類型是不同的:

VertexOut VS(int3 Pos : POSITION, float4 Color : COLOR) { }

struct Vertex
{
    XMFLOAT3 Pos;
    XMFLOAT4 Color;
};

這實際上是合法的,因爲Direct3D允許重新解釋輸入寄存器中的位。但是,VC ++調試輸出窗口提供以下警告:

D3D11: WARNING: ID3D11Device::CreateInputLayout: The provided input signature expects to read an element with SemanticName/Index: ‘POSITION’/0 and component(s) of the type ‘int32’. However, the matching entry in the Input Layout declaration, element[0], specifies mismatched format: ‘R32G32B32_FLOAT’. This is not an error, since behavior is well defined: The element format determines what data conversion algorithm gets applied before it shows up in a shader register. Independently, the shader input signature defines how the shader will interpret the data that has been placed in its input registers, with no change in the bits stored. It is valid for the application to reinterpret data as a different type once it is in the vertex shader, so this warning is issued just in case reinterpretation was not intended by the author.

以下代碼提供了一個示例來說明如何調用ID3D11Device :: CreateInputLayout方法。請注意,代碼涉及我們尚未討論的一些主題(如ID3D11Effect)。本質上,效果封裝一個或多個過程,並且頂點着色器與每個遍相關聯。所以從這個效果來看,我們可以得到一個傳遞描述(D3D11_PASS_DESC),從中我們可以得到頂點着色器的輸入簽名。

ID3DX11Effect* mFX;
ID3DX11EffectTechnique* mTech;
ID3D11InputLayout* mInputLayout;

/* ...create the effect... */
mTech = mFX->GetTechniqueByName("Tech");
D3DX11_PASS_DESC passDesc;
mTech->GetPassByIndex(0)->GetDesc(&passDesc);
HR(md3dDevice->CreateInputLayout(vertexDesc, 4, passDesc.
    pIAInputSignature, passDesc.IAInputSignatureSize, &mInputLayout));

在創建輸入佈局之後,它仍然沒有綁定到設備。最後一步是綁定你想要的輸入佈局
使用該設備,如下代碼顯示:

ID3D11InputLayout* mInputLayout;
/* ...create the input layout... */
md3dImmediateContext->IASetInputLayout(mInputLayout);

如果您正在繪製的某些對象使用一個輸入佈局,而您正在繪製的其他對象需要不同的佈局,則需要結構化你的代碼如下所示:

md3dImmediateContext->IASetInputLayout(mInputLayout1);
/* ...draw objects using input layout 1... */

md3dImmediateContext->IASetInputLayout(mInputLayout2);
/* ...draw objects using input layout 2... */

換句話說,當輸入佈局被綁定到設備時,它不會改變,除非你覆蓋它。

6.2 定點緩存

爲了使GPU訪問頂點數組,它們需要被放置在一個ID3D11Buffer接口表示的稱爲緩衝區的特殊資源結構中由。

存儲頂點的緩衝區稱爲頂點緩衝區。Direct3D緩存不僅可以存儲數據,還可以描述所存儲數據的訪問方式及其被綁定到渲染管道。要創建頂點緩衝區,我們需要執行以下步驟:
1.填寫描述我們要創建的緩衝區的D3D11_BUFFER_DESC結構。
2.填寫一個D3D11_SUBRESOURCE_DATA結構,指定我們要初始化緩衝區內容的數據。
3.調用ID3D11Device :: CreateBuffer來創建緩衝區。

D3D11_BUFFER_DESC結構定義如下:

typedef struct D3D11_BUFFER_DESC {
    UINT ByteWidth;
    D3D11_USAGE Usage;
    UINT BindFlags;
    UINT CPUAccessFlags;
    UINT MiscFlags;
    UINT StructureByteStride;
} D3D11_BUFFER_DESC;

1 . ByteWidth:我們要創建的頂點緩衝區的大小(以字節爲單位)。
2 .UsageD3D11_USAGE枚舉類型的成員,指定緩衝區的使用方式。 四個值爲:
(a) D3D11_USAGE_DEFAULT:如果GPU可讀、寫該資源,請指定此用法。使用映射API(即ID3D11DeviceContext :: Map),CPU無法讀取或寫入資源。但是,它可以使用ID3D11DeviceContext :: UpdateSubresource。§6.14中將討論ID3D11DeviceContext :: Map
(b) D3D11_USAGE_IMMUTABLE:如果資源的內容在創建後不會更改,請指定此用法。這允許一些潛在的優化,因爲資源將被GPU只讀。除了在創建時初始化資源,CPU不能寫入不可變資源。CPU不能從不可變資源讀取。 我們無法映射或更新不可變資源。
(c) D3D11_USAGE_DYNAMIC:如果應用程序(CPU)需要頻繁更新資源的數據內容(例如,基於每幀),則指定此用法。具有這種用途的資源可以由GPU讀取並由CPU使用映射API(即ID3D11DeviceContext :: Map)寫入。因爲新數據必須從CPU存儲器(即系統RAM)轉移到GPU存儲器(即視頻RAM),所以從CPU動態地更新GPU資源會導致性能下降。 因此,除非必要,否則應避免動態使用。
(d) D3D11_USAGE_STAGING:如果應用程序(CPU)需要能夠讀取資源的副本(即,該資源支持將數據從視頻內存複製到系統內存),請指定此用法。從GPU複製到CPU內存是一個緩慢的操作,除非必要應該避免。可以使用ID3D11DeviceContext :: CopyResourceID3D11DeviceContext :: CopySubresourceRegion方法。 §12.3.5顯示了一個CopyResource的例子。
3 . BindFlags:對於頂點緩衝區,請指定D3D11_BIND_VERTEX_BUFFER
4 .CPUAccessFlags:指定CPU如何訪問緩衝區。如果CPU在創建緩衝區後不需要讀取或寫入權限,則指定0。如果CPU需要通過寫入來更新緩衝區,請指定D3D11_CPU_ACCESS_WRITE。具有寫訪問權限的緩衝區必須使用D3D11_USAGE_DYNAMICD3D11_USAGE_STAGING。如果CPU需要從緩衝區中讀取,請指定D3D11_CPU_ACCESS_READ。具有讀訪問權限的緩衝區必須使用D3D11_USAGE_STAGING。只有在需要時才指定這些標誌。一般來說,CPU從Direct3D資源讀取的速度較慢(GPU通過流水線進行數據抽取而不是回讀),並且可能導致GPU停止(GPU可能需要等待完成其中資源的讀取之後才能繼續工作)。寫入資源的CPU速度較快,但仍然需要將更新的數據傳輸回GPU RAM的開銷。最好不要指定任何這些標誌(除非必要),儘量讓資源在GPU RAM中,由GPU寫入並讀取它。
5 . MiscFlags:頂點緩衝區中我們不需要任何Misc標誌; 指定0,在SDK文檔中查看D3D11_RESOURCE_MISC_FLAG枚舉類型以瞭解更多。
6 . StructureByteStride:存儲在結構化緩衝區中的單個元素的大小(以字節爲單位)。此屬性僅適用於結構化緩衝區,並可爲所有其他緩衝區設置爲0。 結構緩衝區是存儲相同大小的元素的緩衝區。

D3D11_SUBRESOURCE_DATA結構定義如下:

typedef struct D3D11_SUBRESOURCE_DATA {
    const void *pSysMem;
    UINT SysMemPitch;
    UINT SysMemSlicePitch;
} D3D11_SUBRESOURCE_DATA;

1 . pSysMem:一個指向系統內存數組的指針,它包含初始化頂點緩衝區的數據。如果緩衝區可以存儲n個頂點,則系統陣列必須至少包含n個頂點,以便可以初始化整個緩衝區。
2 . SysMemPitch:不用於頂點緩衝區。
3 . SysMemSlicePitch:不用於頂點緩衝區。

以下代碼創建了一個不可變的頂點緩衝區,該緩衝區是以原點爲中心的立方體的八個頂點初始化。緩衝區是不可變的,因爲立方體一旦創建就不需要更改 - 它始終保持立方體。此外,我們將每個頂點與不同的顏色相關聯; 這些頂點顏色將用於對立方體進行着色,我們將在本章後面看到。

// Colors namespace defined in d3dUtil.h.
//
// #define XMGLOBALCONST extern CONST __declspec(selectany)
// 1. extern so there is only one copy of the variable, and not a
// separate private copy in each .obj.
// 2. __declspec(selectany) so that the compiler does not complain
// about multiple definitions in a .cpp file (it can pick anyone
// and discard the rest because they are constant--all the same).namespace Colors
{
    XMGLOBALCONST XMVECTORF32 White = {1.0f, 1.0f, 1.0f, 1.0f};
    XMGLOBALCONST XMVECTORF32 Black = {0.0f, 0.0f, 0.0f, 1.0f};
    XMGLOBALCONST XMVECTORF32 Red = {1.0f, 0.0f, 0.0f, 1.0f};
    XMGLOBALCONST XMVECTORF32 Green = {0.0f, 1.0f, 0.0f, 1.0f};
    XMGLOBALCONST XMVECTORF32 Blue = {0.0f, 0.0f, 1.0f, 1.0f};
    XMGLOBALCONST XMVECTORF32 Yellow = {1.0f, 1.0f, 0.0f, 1.0f};
    XMGLOBALCONST XMVECTORF32 Cyan = {0.0f, 1.0f, 1.0f, 1.0f};
    XMGLOBALCONST XMVECTORF32 Magenta = {1.0f, 0.0f, 1.0f, 1.0f};
} 
//define raw vertex data
Vertex vertices[] =
{
    { XMFLOAT3(-1.0f, -1.0f, -1.0f), (const float*)&Colors::White },
    { XMFLOAT3(-1.0f, +1.0f, -1.0f), (const float*)&Colors::Black },
    { XMFLOAT3(+1.0f, +1.0f, -1.0f), (const float*)&Colors::Red },
    { XMFLOAT3(+1.0f, -1.0f, -1.0f), (const float*)&Colors::Green },
    { XMFLOAT3(-1.0f, -1.0f, +1.0f), (const float*)&Colors::Blue },
    { XMFLOAT3(-1.0f, +1.0f, +1.0f), (const float*)&Colors::Yellow },
    { XMFLOAT3(+1.0f, +1.0f, +1.0f), (const float*)&Colors::Cyan },
    { XMFLOAT3(+1.0f, -1.0f, +1.0f), (const float*)&Colors::Magenta }
};
D3D11_BUFFER_DESC vbd;
vbd.Usage = D3D11_USAGE_IMMUTABLE;
vbd.ByteWidth = sizeof(Vertex) * 8;
vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER;
vbd.CPUAccessFlags = 0;
vbd.MiscFlags = 0;
vbd.StructureByteStride = 0;

D3D11_SUBRESOURCE_DATA vinitData;
vinitData.pSysMem = vertices;

ID3D11Buffer* mVB;
HR(md3dDevice->CreateBuffer(
    &vbd, // description of buffer to create
    &vinitData, // data to initialize buffer with
    & mVB)); // return the created buffer

其中頂點類型和顏色定義如下:

struct Vertex
{
    XMFLOAT3 Pos;
    XMFLOAT4 Color;
};

在創建頂點緩衝區之後,需要將其綁定到設備的輸入槽,以便將頂點作爲輸入提供給管道。 這是通過以下方法完成的:

void ID3D11DeviceContext::IASetVertexBuffers(
    UINT StartSlot,
    UINT NumBuffers,
    ID3D11Buffer *const *ppVertexBuffers,
    const UINT *pStrides,
    const UINT *pOffsets);

1 . StartSlot:開始綁定頂點緩衝區的輸入槽。從0-15開始索引16個輸入插槽。

2 . NumBuffers:我們綁定到輸入插槽的頂點緩衝區的數量。如果開始時槽有索引k,並且我們綁定了n個緩衝區,那我們綁定緩衝區到槽IkIk+1...Ik+n1

3 . ppVertexBuffers:指向頂點緩衝區數組的第一個元素的指針。

4 . pstrides:指向數組的第一個元素的指針(每個頂點緩衝區一個,第i個步長對應於第i個
頂點緩衝區)。步幅是相應頂點緩衝區中元素的大小(以字節爲單位)。

5 . pOffsets:指向偏移數組的第一個元素的指針(每個頂點緩衝區一個,第i個偏移量對應於第i個頂點緩衝區)。這是從頂點緩衝區開始到頂點緩衝區中的位置的偏移量(以字節爲單位),輸入組件應從該位置開始讀取頂點數據。如果你想跳過頂點緩衝區前面的一些頂點數據,你可以使用它。

IASetVertexBuffers方法似乎有點複雜,因爲它支持將頂點緩衝區數組設置爲各種輸入槽。但是,大多數時間我們只能使用一個輸入插槽。本章結束的練習給您一些使用兩個輸入插槽的練習。

頂點緩衝區將保持綁定到輸入插槽,直到更改它。因此,如果使用多個頂點緩衝區,則可以像這樣構建代碼:

ID3D11Buffer* mVB1; // stores vertices of type Vertex1
ID3D11Buffer* mVB2; // stores vertices of type Vertex2

/*...Create the vertex buffers...*/
UINT stride = sizeof(Vertex1);
UINT offset = 0;
md3dImmediateContext->IASetVertexBuffers(0, 1, &mVB1, &stride, &offset);

/* ...draw objects using vertex buffer 1... */

stride = sizeof(Vertex2);
offset = 0;
md3dImmediateContext->IASetVertexBuffers(0, 1, &mVB2, &stride, &offset);
/* ...draw objects using vertex buffer 2... */

6-2
圖6.2 StartVertexLocation指定頂點緩衝區中第一個頂點的索引(從零開始繪製)。VertexCount指定要繪製的頂點數。

將頂點緩衝區設置爲輸入槽不繪製; 它只使頂點準備好進入管道。 實際繪製頂點的最後一步是使用ID3D11DeviceContext :: Draw方法:

void ID3D11DeviceContext::Draw(UINT VertexCount, UINT StartVertexLocation);

這兩個參數定義了頂點緩衝區中需要繪製的連續頂點子集, 見圖6.2。

6.3 索引和索引緩衝區

由於需要由GPU訪問索引,因此需要將其放置在特殊的資源結構中:索引緩衝區。創建一個索引緩衝區與創建頂點緩衝區非常類似,只是存儲了索引而不是頂點。因此,我們只是顯示一個創建索引緩衝區的例子,而不重複類似於頂點緩衝區講解。

UINT indices[24] = {
    0, 1, 2, // Triangle 0
    0, 2, 3, // Triangle 1
    0, 3, 4, // Triangle 2
    0, 4, 5, // Triangle 3
    0, 5, 6, // Triangle 4
    0, 6, 7, // Triangle 5
    0, 7, 8, // Triangle 6
    0, 8, 1 // Triangle 7
};
// Describe the index buffer we are going to create. Observe the
// D3D11_BIND_INDEX_BUFFER bind flag
D3D11_BUFFER_DESC ibd;
ibd.Usage = D3D11_USAGE_IMMUTABLE;
ibd.ByteWidth = sizeof(UINT) * 24;
ibd.BindFlags = D3D11_BIND_INDEX_BUFFER;
ibd.CPUAccessFlags = 0;
ibd.MiscFlags = 0;
ibd.StructureByteStride = 0;

// Specify the data to initialize the index buffer.
D3D11_SUBRESOURCE_DATA iinitData;
iinitData.pSysMem = indices;

// Create the index buffer.
ID3D11Buffer* mIB;
HR(md3dDevice->CreateBuffer(&ibd, &iinitData, &mIB));

就像頂點緩衝區和其他Direct3D資源一樣,在使用它之前,我們需要將它綁定到管線。使用ID3D11DeviceContext :: IASetIndexBuffer方法將索引緩衝區綁定到輸入整合階段。以下是一個示例調用:

md3dImmediateContext->IASetIndexBuffer(mIB, DXGI_FORMAT_R32_UINT, 0);

第二個參數指定索引的格式。在我們的例子中,我們使用32位無符號整數(DWORD);因此,我們指定了DXGI_FORMAT_R32_UINT。 如果想節省內存和帶寬,也可以使用16位無符號整數,並且不需要額外的範圍。請記住,除了在IASetIndexBuffer方法中指定格式之外,D3D11_BUFFER_DESC :: ByteWidth數據成員也依賴於格式,因此請確保它們一致以避免出現問題。請注意,DXGI_FORMAT_R16_UINTDXGI_FORMAT_R32_UINT是索引緩衝區唯一支持的格式。第三個參數是從索引緩衝區開始到索引緩衝區中輸入程序集應該開始讀取數據的位置的偏移量(以字節爲單位)。如果你想跳過索引緩衝區前面的一些數據,你可以使用它。

最後,在使用索引時,我們必須使用DrawIndexed方法而不是Draw

void ID3D11DeviceContext::DrawIndexed(
    UINT IndexCount,
    UINT StartIndexLocation,
    INT BaseVertexLocation);

1 . IndexCount:在繪製中使用的索引的數量。這不一定是索引緩衝區中的每個索引; 也就是說,你可以繪製一個連續的索引子集。
2 . StartIndexLocation:指向索引緩衝區中的一個元素的索引,該元素標誌着開始讀取索引的起始點。
3 . BaseVertexLocation BaseVertexLocation:在獲取頂點之前,添加到此繪圖調用中使用的索引的整數值。

爲了說明這些參數,請考慮以下情況。假設我們有三個對象:一個球體,一個盒子和一個圓柱體。首先,每個對象都有自己的頂點緩衝區和自己的索引緩衝區。每個本地索引緩衝區中的索引都與相應的本地頂點緩衝區有關。現在假設我們把球體,盒子和圓柱體的頂點和索引連接成一個全局頂點和索引緩衝區,如圖6.3所示。(可能會連接頂點和索引緩衝區,因爲在更改頂點和索引緩衝區時會有一些API開銷,這很可能不是瓶頸,但是如果有許多小的頂點和索引緩衝區可以很容易地合併,出於性能的原因是值得這樣做的。)在這個連接之後,索引不再是正確的,因爲它們存儲索引位置相對於它們相應的本地頂點緩衝區,而不是全局索引。因此需要重新計算索引以正確地指向全局頂點緩衝區。原始的盒子索引是通過盒子的頂點索引的假設計算出來的
0, 1, …, numBoxVertices-1
但合併後,他們變爲
**firstBoxVertexPos,
firstBoxVertexPos+1,
…,
firstBoxVertexPos+numBoxVertices-1**

6-3
圖6.3 將幾個頂點緩衝區連接成一個大的頂點緩衝區,並將幾個索引緩衝區連接成一個大的索引緩衝區。

因此,爲了更新索引,我們需要爲每個框索引添加第一個BoxVertexPos。 同樣,我們需要爲每個柱面索引添加firstCylVertexPos。 請注意,球的指標不需要改變(因爲第一個球的頂點位置爲零)。 讓我們把對象的第一個頂點相對於全局頂點緩衝區的位置稱爲它的基本頂點位置。 通常,對象的新索引是通過將其基本頂點位置添加到每個索引來計算的。 我們不必自己計算新的索引,而是讓Direct3D通過將基本頂點位置傳遞給DrawIndexed的第三個參數來完成。

然後我們可以用下面的三個調用一個接一個地畫出球體,盒子和圓柱體:

md3dImmediateContext->DrawIndexed(numSphereIndices, 0, 0);
md3dImmediateContext->DrawIndexed(numBoxIndices, firstBoxIndex, firstBoxVertexPos);
md3dImmediateContext->DrawIndexed(numCylIndices, firstCylIndex, firstCylVertexPos);

本章的“形狀”演示項目使用這種技術。

6.4示例頂點着色器

下面是簡單頂點着色器的一個實現:

cbuffer cbPerObject
{
    float4x4 gWorldViewProj;
};
void VS(float3 iPosL : POSITION,
    float4 iColor : COLOR,
    out float4 oPosH : SV_POSITION,
    out float4 oColor : COLOR)
{
    // Transform to homogeneous clip space.
    oPosH = mul(float4(iPosL, 1.0f), gWorldViewProj);

    // Just pass vertex color into the pixel shader.
    oColor = iColor;
}

着色器是用一種叫做高級着色語言(HLSL)的語言編寫的,它的語法與C ++類似,因此很容易學習。附錄B提供了對HLSL的簡要參考。我們的教學HLSL和編程着色器的方法將以實例爲基礎。也就是說,當我們通讀這本書的時候,我們將介紹我們需要的任何新的HLSL概念來實現手頭的演示。着色器通常使用基於文本的文件(稱爲效果文件(.fx))編寫。我們將在本章後面討論效果文件,但現在我們只關注頂點着色器。

頂點着色器是稱爲VS的函數。請注意,您可以爲頂點着色器提供任何有效的函數名稱。這個頂點着色器有四個參數。前兩個是輸入參數,後兩個是輸出參數(用out關鍵字表示)。HLSL沒有引用或指針,所以要從一個函數返回多個值,您需要使用結構或輸出參數。

前兩個輸入參數形成頂點着色器的輸入簽名,並對應於我們自定義頂點結構中的數據成員。參數語義“:POSITION”和“:COLOR”用於將頂點結構中的元素映射到頂點着色器輸入參數,如圖6.4所示。

輸出參數也有附加的語義(“:SV_POSITION”和“:COLOR”)。 這些用於將頂點着色器輸出映射到下一階段的相應輸入(幾何着色器或像素着色器)。 請注意SV_POSITION語義是特殊的(SV代表系統值)。它用來表示保存頂點位置的頂點着色器輸出元素。頂點位置需要與其他頂點屬性不同地處理,因爲它涉及其他屬性不涉及的操作,例如剪切。輸出參數不是系統值的語義名稱可以是任何有效的語義
名稱。

6-4
圖6.4。 每個頂點元素都有一個由D3D11_INPUT_ELEMENT_DESC數組指定的關聯語義。頂點着色器的每個參數也有附加的語義。語義用於匹配頂點元素與頂點着色器參數。

第一行通過乘以4×4矩陣gWorldViewProj將頂點位置從局部空間轉換爲齊次剪輯空間:

// Transform to homogeneous clip space.
oPosH = mul(float4(iPosL, 1.0f), gWorldViewProj);

構造函數語法float4(iPosL,1.0f)構造一個4D向量,相當於float4(iPosL.x,iPosL.y,iPosL.z,1.0f);因爲我們知道頂點的位置是點而不是向量, 在第四個組件(w = 1)中放置一個1。float2float3類型分別表示2D和3D向量。矩陣變量gWorldViewProj位於一個常量緩衝區中,這將在下一節討論。內置函數mul用於向量矩陣乘法。順便提一下,mul函數對於不同大小的矩陣乘法是重載的; 例如,可以使用它乘以兩個4×4矩陣,兩個3×3矩陣,或一個1×3向量和一個3×3矩陣。着色器主體中的最後一行將輸入顏色複製到輸出參數,以便顏色將被輸入到管道的下一個階段:

oColor = iColor;

我們可以使用返回類型和輸入簽名的結構來等價地重寫以前的頂點着色器(與長參數列表相反):

cbuffer cbPerObject
{
    float4x4 gWorldViewProj;
};
struct VertexIn
{
    float3 PosL : POSITION;
    float4 Color : COLOR;
};
struct VertexOut
{
    float4 PosH : SV_POSITION;
    float4 Color : COLOR;
};
VertexOut VS(VertexIn vin)
{
    VertexOut vout;

    // Transform to homogeneous clip space.
    vout.PosH = mul(float4(vin.PosL, 1.0f), gWorldViewProj);

    // Just pass vertex color into the pixel shader.
    vout.Color = vin.Color;

    return vout;
}

NOTE:如果沒有幾何着色器,則頂點着色器必須至少進行投影變換,因爲這是硬件在離開頂點着色器時所期望的頂點所在的空間(如果沒有幾何着色器)。如果有一個幾何 着色器,投影工作可以推遲到幾何着色器。

NOTE:頂點着色器(或幾何着色器)不做透視分割; 它只是做投影矩陣的一部分。 透視分割稍後將由硬件完成。

6.5 常數緩衝區

在上一節中的示例頂點着色器代碼是:

cbuffer cbPerObject
{
    float4x4 gWorldViewProj;
};

這段代碼定義了一個名爲cbPerObject的cbuffer對象(常量緩衝區)。常量緩衝區只是可以存儲着色器可以訪問的不同變量的數據塊。在這個例子中,常量緩衝區存儲一個稱爲gWorldViewProj的4×4矩陣,表示用於將點從本地空間轉換爲同類空間的組合世界,視圖和投影矩陣。在HLSL中,內建的float4x4類型聲明瞭一個4×4矩陣;如果要聲明一個3×4矩陣和2×2矩陣,你將分別使用float3x4和float2x2類型。常量緩衝區中的數據不會在每個頂點變化,但通過效應框架(§6.9),C ++應用程序代碼可以在運行時更新常量緩衝區的內容。這爲C ++應用程序代碼和效果代碼進行通信提供了一種手段。例如,因爲每個對象的世界矩陣不同,所以組合的世界,視圖和投影矩陣每個對象都不相同;因此,在使用以前的頂點着色器繪製多個對象時,我們需要在繪製每個對象之前適當地更新gWorldViewProj變量。

一般建議是根據您需要更新內容的頻率創建常量緩衝區。例如,你可以創建下面的常量緩衝區:

cbuffer cbPerObject
{
    float4x4 gWVP;
};

cbuffer cbPerFrame
{
    float3 gLightDirection;
    float3 gLightPosition;
    float4 gLightColor;
};
cbuffer cbRarely
{
    float4 gFogColor;
    float gFogStart;
    float gFogEnd;
};

在這個例子中,我們使用了三個常量緩衝區。第一個常量緩衝區存儲組合的世界,視圖和投影矩陣。這個變量取決於對象,所以它必須在每個對象的基礎上更新。也就是說,如果我們每幀渲染100個對象,那麼我們將每幀更新這個變量100次。第二個常量緩衝區存儲場景燈光變量。在這裏,我們假定燈光是動畫的,所以每一幀動畫都需要更新一次。最後一個常量緩衝區存儲用於控制霧的變量。 在這裏,我們假定場景霧很少變化(例如,也許它只是在遊戲當中的某個時間發生變化)。

分開常量緩衝區的動機是效率。當一個常量緩衝區被更新時,所有的變量都必須被更新。因此,根據更新頻率對它們進行分組可以使冗餘更新最小化。

6.6 像素着色器示例

如§5.10.3所述,在光柵化期間,從頂點着色器(或幾何着色器)輸出的頂點屬性被插值在三角形的像素上。然後內插的值作爲輸入被饋送到像素着色器中。假設沒有幾何着色器,圖6.5說明了頂點數據到目前爲止的路徑。

像素着色器就像是一個頂點着色器,因爲它是爲每個像素片段執行的函數。給定像素着色器輸入,像素着色器的工作是計算像素片段的顏色值。我們注意到,像素片段可能無法生存,並將其放到後臺緩衝區;例如,可能會被像素着色器(HLSL包括可以從進一步處理中丟棄像素片段的剪輯函數)截斷,被深度值更小的另一個像素片段遮擋,或者像素片段可能稍後被管線測試丟棄就像模板緩衝區測試一樣。因此,後臺緩衝器上的像素可以具有多個像素片段候選;這是“像素片段”和“像素”之間的區別,儘管有時這些術語可以互換使用,但是上下文通常會明確指出是什麼意思。

NOTE:作爲一種硬件優化,像素片段在進入像素着色器之前可能被管線拒絕(例如早期的z拒絕)。這是首先進行深度測試的地方,如果像素片段被深度測試確定爲被遮擋,則像素着色器被跳過。但是,有些情況下可以禁用早期的z拒絕優化。例如,如果像素着色器修改像素的深度,則像素着色器必須被執行,因爲如果像素着色器改變像素着色器,那麼我們不知道像素着色器之前像素的深度是多少。

6-5
圖6.5 每個頂點元素都有一個由D3D11_INPUT_ELEMENT_DESC數組指定的關聯語義。頂點着色器的每個參數也都有一個附加的語義。語義用於匹配頂點元素與頂點着色器參數。同樣,來自頂點着色器的每個輸出都具有附加的語義,並且每個像素着色器輸入參數具有附加的語義。這些語義用於將頂點着色器輸出映射到像素着色器輸入參數中。

以下是一個簡單的像素着色器,對應於第6.4節給出的頂點着色器。 爲了完整性,頂點着色器被再次顯示。

cbuffer cbPerObject
{
    float4x4 gWorldViewProj;
};
void VS(float3 iPos : POSITION, float4 iColor : COLOR,
    out float4 oPosH : SV_POSITION,
    out float4 oColor : COLOR)
{
    // Transform to homogeneous clip space.
    oPosH = mul(float4(iPos, 1.0f), gWorldViewProj);

    // Just pass vertex color into the pixel shader.
    oColor = iColor;
}
float4 PS(float4 posH : SV_POSITION, float4 color : COLOR) : SV_Target
{
    return pin.Color;
}

在這個例子中,像素着色器只是返回插值的顏色值。 請注意,像素着色器輸入與頂點着色器輸出完全匹配; 這是一個要求。 像素着色器返回4D顏色值,函數參數列表後面的SV_TARGET語義指示返回值類型應與渲染目標格式相匹配。

我們可以使用輸入/輸出結構等效地重寫以前的頂點和像素着色器。 符號的不同之處在於我們將語義附加到輸入/輸出結構的成員,並且我們使用返回語句來輸出輸出而不是輸出參數。

cbuffer cbPerObject
{
    float4x4 gWorldViewProj;
};
struct VertexIn
{
    float3 Pos : POSITION;
    float4 Color : COLOR;
};
struct VertexOut
{
    float4 PosH : SV_POSITION;
    float4 Color : COLOR;
};
VertexOut VS(VertexIn vin)
{
    VertexOut vout;

    // Transform to homogeneous clip space.
    vout.PosH = mul(float4(vin.Pos, 1.0f), gWorldViewProj);

    // Just pass vertex color into the pixel shader.
    vout.Color = vin.Color;

    return vout;
}
float4 PS(VertexOut pin) : SV_Target
{
    return pin.Color;
}

6.7 着色階段

Direct3D基本上是一個狀態機。 在我們改變它們之前,事物會保持現狀。 例如,我們在§6.1,§6.2和§6.3中看到,綁定到流水線輸入整合程序階段的輸入佈局,頂點緩衝區和索引緩衝區會一直存在,直到我們綁定不同的結構爲止。同樣,當前設置的基本拓撲保持有效,直到它被改變。另外,Direct3D具有封裝可用於配置Direct3D的設置的狀態組:
1 . ID3D11RasterizerState:該接口表示用於配置流水線柵格化階段的狀態組。
2 . ID3D11BlendState:該接口表示用於配置混合操作的狀態組。我們將在關於混合的章節中討論這些狀態; 默認情況下,混合是禁用的,所以我們現在不需要擔心。
3 . ID3D11DepthStencilState:該接口表示用於配置深度和模板測試的狀態組。我們將在關於模板緩衝區的章節中討論這些狀態。默認情況下,模版被禁用,所以我們現在不需要擔心。默認的深度測試設置被設置爲執行§4.1.5中描述的標準深度測試。

現在,關注我們的唯一狀態塊接口是ID3D11RasterizerState接口。 我們可以通過填寫D3D11_RASTERIZER_DESC結構然後調用方法來創建這種類型的接口:

HRESULT ID3D11Device::CreateRasterizerState(
    const D3D11_RASTERIZER_DESC *pRasterizerDesc,
    ID3D11RasterizerState **ppRasterizerState);

第一個參數就是填充D3D11_RASTERIZER_DESC結構,描述要創建的光柵化器狀態塊; 第二個參數用於返回指向創建的ID3D11RasterizerState接口的指針。

D3D11_RASTERIZER_DESC結構定義如下:

typedef struct D3D11_RASTERIZER_DESC {
    D3D11_FILL_MODE FillMode; // Default: D3D11_FILL_SOLID
    D3D11_CULL_MODE CullMode; // Default: D3D11_CULL_BACK
    BOOL FrontCounterClockwise; // Default: false
    INT DepthBias; // Default: 0
    FLOAT DepthBiasClamp; // Default: 0.0f
    FLOAT SlopeScaledDepthBias; // Default: 0.0f
    BOOL DepthClipEnable; // Default: true
    BOOL ScissorEnable; // Default: false
    BOOL MultisampleEnable; // Default: false
    BOOL AntialiasedLineEnable; // Default: false
} D3D11_RASTERIZER_DESC;

這些成員大多數是高級或不經常使用; 因此,我們將向您介紹SDK文檔以獲取每個成員的描述。不過,只有前三個值得在這裏討論。
1 . FillMode:爲線框渲染指定D3D11_FILL_WIREFRAME,或爲實體渲染指定D3D11_FILL_SOLID。 固體渲染是默認的。
2 . CullMode:指定D3D11_CULL_NONE禁用剔除,D3D11_CULL_BACK剔除背向三角,D3D11_CULL_FRONT剔除前向三角。 背面三角形默認爲剔除。
3 . FrontCounterClockwise:如果您希望順時針(相對於相機)排列的三角形被視爲正面,逆時針(相對於相機)排列的三角形被視爲背面,則指定false。 如果您希望將逆時針(相對於相機)排列的三角形視爲正面,並將順時針(相對於相機)排列的三角形視爲背面,請指定true。 此狀態默認爲false。

一旦創建了ID3D11RasterizerState對象,我們就可以用新的狀態塊更新設備:

void ID3D11DeviceContext::RSSetState(
    ID3D11RasterizerState *pRasterizerState);

以下代碼顯示如何創建禁用背面剔除的柵格化狀態:

D3D11_RASTERIZER_DESC rsDesc;
ZeroMemory(&rsDesc, sizeof(D3D11_RASTERIZER_DESC));
rsDesc.FillMode = D3D11_FILL_SOLID;
rsDesc.CullMode = D3D11_CULL_NONE;
rsDesc.FrontCounterClockwise = false;
rsDesc.DepthClipEnable = true;

HR(md3dDevice->CreateRasterizerState(&rsDesc, &mNoCullRS));

NOTE:使用ZeroMemory可以初始化我們不設置的其他屬性,因爲它們的默認值是零或爲假。 但是,如果屬性具有非零或真實的默認值,並且您想要默認值,那麼您將不得不顯式設置屬性。

請注意,對於應用程序,您可能需要幾個不同的ID3D11RasterizerState對象。所以你要做的就是在初始化時創建它們,然後根據需要在應用程序更新/繪製代碼中進行切換。例如,假設您有兩個對象,並且您想要以線框模式繪製第一個對象,而以固定模式繪製第一個對象。然後,您將有兩個ID3D11RasterizerState對象,並在繪製對象時在它們之間切換:

// Create render state objects as initialization time.
ID3D11RasterizerState* mWireframeRS;
ID3D11RasterizerState* mSolidRS;
...

// Switch between the render state objects in the draw function.
md3dImmediateContext->RSSetState(mSolidRS);
DrawObject();

md3dImmediateContext->RSSetState(mWireframeRS);
DrawObject();

應該指出的是,Direct3D從不將狀態恢復到之前的設置。 因此,繪製對象時應始終設置所需的狀態。 對設備的當前狀態做出不正確的假設將導致錯誤的輸出。

每個狀態塊都有一個默認狀態。 通過使用null調用RSSetState方法,我們可以恢復到默認狀態:

// Restore default state.
md3dImmediateContext->RSSetState( 0 );

NOTE:通常,應用程序不需要在運行時創建額外的渲染狀態組。因此,應用程序可以在初始化時定義並創建所有必要的渲染狀態組。而且,由於渲染狀態組在運行時不需要修改,因此可以爲渲染代碼提供對它們的全局只讀訪問權限。例如,您可以將所有渲染狀態組對象放在一個靜態類中。這樣,您不會創建重複的渲染狀態組對象,渲染代碼的各個部分可以共享渲染狀態組對象。

6.8效果

effects framework是一組實用程序代碼,它提供了一個用於組織着色器程序和渲染階段的框架,這些階段一起工作來實現特定的渲染效果。例如,您可能對渲染水,雲,金屬物體和動畫角色有不同的效果。每個效果將由至少一個頂點着色器,一個像素着色器和渲染狀態組成,以實現該效果。

在以前的Direct3D版本中,一旦與D3D 10庫鏈接後,效果框架就可以使用。在Direct3D 11中,效果框架已經被移到了D3DX庫中,並且您必須包含一個單獨的頭文件(d3dx11Effect.h),並鏈接到單獨的庫(D3DX11Effects.lib用於發佈版本,D3DX11EffectsD.lib用於調試版本)。另外,在Direct3D 11中,它們爲您提供了效果庫代碼的完整源代碼(DirectX SDK \ Samples \ C ++ \ Effects11)。因此,您可以根據自己的需要修改效果框架。在這本書中,我們只會使用效果框架,而不做任何修改。爲了使用這個庫,你需要首先在release和debug模式下構建Effects11項目來生成D3DX11Effects.libD3DX11EffectsD.lib文件;除非更新了效果框架(例如,新版本的DirectX SDK可能會更新這些文件,因此您可能需要重新生成.lib文件以獲取最新版本),否則只需要執行一次該操作。d3dx11Effect.h頭文件可以在DirectX SDK \ Samples \ C ++ \ Effects11 \ Inc中找到。對於我們的示例項目,我們將d3dx11Effect.hD3DX11EffectsD.libD3DX11Effects.lib文件放置在我們所有項目共享代碼的Common目錄中(請參閱“簡介”,瞭解示例項目組織的描述)。

6.8.1 效果文件

我們已經討論了頂點着色器,像素着色器,以及較小程度的幾何和鑲嵌着色器。 我們還討論了常量緩衝區,可以用來存儲着色器可以訪問的“全局”變量。 這樣的代碼通常寫在一個效果文件(.fx)中,它只是一個文本文件(就像C ++代碼寫在.h / .cpp文件中一樣)。 除了房屋着色器和常數緩衝之外,效果還包含至少一種技術。 反過來,一種技術至少包含一個通道。
1.技術:一種技術由一個或多個用於創建特定渲染技術的通道組成。 對於每個通道,幾何體都以不同的方式呈現,並且每個通道的結果以某種方式進行組合以獲得期望的結果。例如,地形渲染技術可以使用多遍紋理化技術。請注意,多遍技術通常很昂貴,因爲幾何圖形每次都要重新繪製; 然而,多通道技術需要實現一些渲染技術。
2.通道:通過包括頂點着色器,可選的幾何着色器,可選鑲嵌相關着色器,像素着色器和渲染狀態。這些組件指示如何處理和遮擋此通道的幾何圖形。我們注意到像素着色器也可以是可選的(很少)。例如,我們可能只想渲染到深度緩衝區而不是後臺緩衝區; 在這種情況下,我們不需要使用像素着色器遮擋任何像素。

NOTE:技術也可以組合在一起,稱爲效應組。如果你沒有明確的定義一個,編譯器會創建一個匿名的來包含效果文件中的所有技術。在本書中,我們沒有明確定義任何效果組。
以下是本章演示的整個效果文件:

cbuffer cbPerObject
{
    float4x4 gWorldViewProj;
};
struct VertexIn
{
    float3 Pos : POSITION;
    float4 Color : COLOR;
};
struct VertexOut
{
    float4 PosH : SV_POSITION;
    float4 Color : COLOR;
};
VertexOut VS(VertexIn vin)
{
    VertexOut vout;

    // Transform to homogeneous clip space.
    vout.PosH = mul(float4(vin.Pos, 1.0f), gWorldViewProj);

    // Just pass vertex color into the pixel shader.
    vout.Color = vin.Color;

    return vout;
}
float4 PS(VertexOut pin) : SV_Target
{
    return pin.Color;
}
technique11 ColorTech
{
    pass P0
    {
        SetVertexShader( CompileShader( vs_5_0, VS() ) );
        SetPixelShader( CompileShader( ps_5_0, PS() ) );
    }
}

NOTE:點和矢量座標相對於許多不同的空間(例如,局部空間,世界空間,視圖空間,均勻空間)被指定。 讀取代碼時,點/矢量的座標系相對於哪個座標系可能並不明顯。 因此,我們經常使用下面的後綴表示空間:L(對於局部空間),W(對於世界空間),V(對於視圖空間)和H(對於同質剪輯空間)。 這裏有些例子:

float3 iPosL; // local space
float3 gEyePosW; // world space
float3 normalV; // view space
float4 posH; // homogeneous clip space

我們提到pass是由渲染狀態組成的。也就是說,可以創建狀態塊並直接在效果文件中進行設置。當效果需要特定的渲染狀態時,這很方便; 相反,一些效果可能與變量渲染狀態設置一起工作,在這種情況下,我們更喜歡在應用級別設置狀態以便於狀態切換。以下代碼顯示如何在效果文件中創建和設置光柵化器狀態塊。

RasterizerState WireframeRS
{
    FillMode = Wireframe;
    CullMode = Back;
    FrontCounterClockwise = false;
    // Default values used for any properties we do not set.
};
technique11 ColorTech
{
    pass P0
    {
        SetVertexShader( CompileShader( vs_5_0, VS() ) );
        SetPixelShader( CompileShader( ps_5_0, PS() ) );
        SetRasterizerState(WireframeRS);
    }
}

觀察光柵器狀態對象定義中的右側值基本上與C ++情況下的相同,除了前綴被省略(例如,省略D3D11_FILL_D3D11_CULL_)。

NOTE:因爲一個效果通常寫在一個外部.fx文件中,所以可以修改它,而不必重新編譯源代碼。

6.8.2編譯着色器

創建效果的第一步是編譯在.fx文件中定義的着色器程序。 這可以通過以下D3DX功能來完成。

HRESULT D3DX11CompileFromFile(
    LPCTSTR pSrcFile,
    CONST D3D10_SHADER_MACRO *pDefines,
    LPD3D10INCLUDE pInclude,
    LPCSTR pFunctionName,
    LPCSTR pProfile,
    UINT Flags1,
    UINT Flags2,
    ID3DX11ThreadPump *pPump,
    ID3D10Blob **ppShader,
    ID3D10Blob **ppErrorMsgs,
    HRESULT *pHResult);

1 . pFileName:包含我們要編譯的效果源代碼的.fx文件的名稱。
2 .pDefines:我們不使用的高級選項;請參閱SDK文檔。我們總是在本書中指定null。
3 .pInclude:我們不使用的高級選項;請參閱SDK文檔。我們總是在本書中指定null。
4 .pFunctionName:着色器功能名稱入口點。這僅在單獨編譯着色器程序時使用。使用效果框架時,指定爲null,因爲在效果文件內部定義的技術指定了着色器入口點。
5 .pProfile:指定我們正在使用的着色器版本的字符串。對於Direct3D 11效果,我們使用着色器版本5.0(“fx_5_0”)。
6 . Flags1:來指定應該如何編譯着色器代碼的標誌。SDK中列出了相當多的這些標誌文檔,但是我們在此使用的唯一兩個是:
(a)D3D10_SHADER_DEBUG:以調試模式編譯着色器。
(b)D3D10_SHADER_SKIP_OPTIMIZATION:指示編譯器跳過優化(用於調試)。
7 . Flags2:我們不使用的高級效果編譯選項;請參閱SDK文檔。
8 . pPump:我們不用於異步編譯着色器的高級選項;請參閱SDK文檔。我們總是指定
在這本書中爲null。
9 . ppShader:返回指向存儲編譯着色器的ID3D10Blob數據結構的指針。
10 . ppErrorMsgs:返回指向ID3D10Blob數據結構的指針,該數據結構存儲包含編譯錯誤的字符串(如果有)。
11 . pHResult:用於獲取異步編譯時返回的錯誤代碼。如果爲pPump指定了null,則指定null。

1.除了在.fx中編譯着色器之外,這個函數還可以用來編譯單獨的着色器。一些程序員不使用效果框架(或自己製作),因此他們分別定義和編譯它們的着色器。
2.上一個函數中對“D3D10”的引用不是拼寫錯誤。 D3D11編譯器是建立在D3D10編譯器上的,所以Direct3D 11團隊並不打擾重命名某些標識符。
3. ID3D10Blob類型只是一個通用的內存塊,有兩種方法:
(a)LPVOID GetBufferPointer:返回一個void *給數據,所以在使用前它必須被轉換成合適的類型(見下面的例子)。
(b)SIZE_T GetBufferSize:返回緩衝區的字節大小。
一旦效果着色器被編譯,我們可以通過如下函數創建一個效果(由ID3DXEffect11接口表示):

HRESULT D3DX11CreateEffectFromMemory(
    void *pData,
    SIZE_T DataLength,
    UINT FXFlags,
    ID3D11Device *pDevice,
    ID3DX11Effect **ppEffect);

1 . pData:指向編譯效果數據的指針。
2 . DataLength:編譯的效果數據的字節大小。
3 . FXFlags:效果標誌應與D3DX11CompileFromFile函數中爲Flags2指定的標誌匹配。
4 . pDevice:指向Direct3D 11設備的指針。
5 . ppEffect:返回一個指向創建效果的指針。
以下代碼顯示如何編譯和創建一個效果:

DWORD shaderFlags = 0;
#if defined(DEBUG) || defined(_DEBUG)
    shaderFlags |= D3D10_SHADER_DEBUG;
    shaderFlags |= D3D10_SHADER_SKIP_OPTIMIZATION;
#endif
    ID3D10Blob* compiledShader = 0;
    ID3D10Blob* compilationMsgs = 0;
    HRESULT hr = D3DX11CompileFromFile(L"color.fx", 0,
        0, 0, "fx_5_0", shaderFlags,
        0, 0, &compiledShader, &compilationMsgs, 0);
    // compilationMsgs can store errors or warnings.
    if(compilationMsgs != 0)
    {
        MessageBoxA(0, (char*)compilationMsgs->GetBufferPointer(), 0, 0);
        ReleaseCOM(compilationMsgs);
    }
    // Even if there are no compilationMsgs, check to make sure there
    // were no other errors.
    if(FAILED(hr))
    {
        DXTrace(__FILE__, (DWORD)__LINE__, hr,
        L"D3DX11CompileFromFile", true);
    }
    ID3DX11Effect* mFX;
    HR(D3DX11CreateEffectFromMemory(
        compiledShader->GetBufferPointer(),
        compiledShader->GetBufferSize(),
        0, md3dDevice, &mFX));

    // Done with compiled shader.
    ReleaseCOM(compiledShader);

NOTE:創建Direct3D資源非常昂貴,應始終在初始化時完成,而不要在運行時完成。這意味着創建輸入佈局,緩衝區,渲染狀態對象和效果應始終在初始化時完成。

6.8.3來自C ++應用程序的效果接口

C ++應用程序代碼通常需要與效果進行通信; 特別是C ++應用程序通常需要更新常量緩衝區中的變量。 例如,假設我們在效果文件中定義了以下常量緩衝區:

cbuffer cbPerObject
{
    float4x4 gWVP;
    float4 gColor;
    float gSize;
    int gIndex;
    bool gOptionOn;
};

通過ID3DX11Effect接口,我們可以獲得指向常量緩衝區中變量的指針:

ID3DX11EffectMatrixVariable* fxWVPVar;
ID3DX11EffectVectorVariable* fxColorVar;
ID3DX11EffectScalarVariable* fxSizeVar;
ID3DX11EffectScalarVariable* fxIndexVar;
ID3DX11EffectScalarVariable* fxOptionOnVar;
fxWVPVar = mFX->GetVariableByName("gWVP")->AsMatrix();
fxColorVar = mFX->GetVariableByName("gColor")->AsVector();
fxSizeVar = mFX->GetVariableByName("gSize")->AsScalar();
fxIndexVar = mFX->GetVariableByName("gIndex")->AsScalar();
fxOptionOnVar = mFX->GetVariableByName("gOptionOn")->AsScalar();

ID3DX11Effect :: GetVariableByName方法返回ID3DX11EffectVariable類型的指針。 這是一個通用的效果變量類型; 要獲得指向特定類型的指針(例如,矩陣,向量,標量),必須使用適當的As方法(例如,AsMatrix,AsVector,AsScalar)。

一旦我們有了指向變量的指針,我們可以通過C ++接口來更新它們。 這裏有些例子:

fxWVPVar->SetMatrix((float*)&M ); // assume M is of type XMMATRIX
fxColorVar->SetFloatVector( (float*)&v ); // assume v is of type XMVECTOR
fxSizeVar->SetFloat( 5.0f );
fxIndexVar->SetInt( 77 );
fxOptionOnVar->SetBool( true );

請注意,這些調用會更新效果對象中的內部緩存,在我們應用渲染過程(第6.8.4節)之前,不會傳輸到GPU內存。這確保了對GPU內存的更新,而不是許多小的更新,這將是低效的。

NOTE:效果變量不需要專門指定。例如,你可以寫:

ID3DX11EffectVariable* mfxEyePosVar;
mfxEyePosVar = mFX->GetVariableByName("gEyePosW");
…
mfxEyePosVar->SetRawValue(&mEyePos, 0, sizeof(XMFLOAT3));

這對設置任意大小的變量(例如,一般結構)很有用。 請注意,ID3DX11EffectVectorVariable接口假定爲4D矢量,因此如果您要使用3D矢量(如XMFLOAT3),您將需要使用ID3DX11EffectVariable,如前所述。

除了常量緩衝區變量之外,還需要獲取指向存儲在效果文件中的技術對象的指針。 例如:

ID3DX11EffectTechnique* mTech;
mTech = mFX->GetTechniqueByName("ColorTech");

這個方法採用的單個參數是你希望獲得指針的技術的字符串名稱。

6.8.4使用效果繪製

要使用一種技術來繪製幾何圖形,我們只需要確保常量緩衝區中的變量是最新的。 然後我們循環遍歷技術中的每個遍,應用通道,並繪製幾何:

// Set constants
XMMATRIX world = XMLoadFloat4x4(&mWorld);
XMMATRIX view = XMLoadFloat4x4(&mView);
XMMATRIX proj = XMLoadFloat4x4(&mProj);
XMMATRIX worldViewProj = world*view*proj;

mfxWorldViewProj->SetMatrix(reinterpret_cast<float*>(&worldViewProj));

D3DX11_TECHNIQUE_DESC techDesc;
mTech->GetDesc(&techDesc);
for(UINT p = 0; p < techDesc.Passes; ++p)
{
    mTech->GetPassByIndex(p)->Apply(0, md3dImmediateContext);
    // Draw some geometry.
    md3dImmediateContext->DrawIndexed(36, 0, 0);
}

當幾何體在通道中繪製時,將使用該通道設置的着色器和渲染狀態進行繪製。 ID3DX11EffectTechnique :: GetPassByIndex方法返回一個指向ID3DX11EffectPass接口的指針,該接口表示具有指定索引的傳遞。Apply方法更新存儲在GPU內存中的常量緩衝區,將着色器程序綁定到管道,並應用通道設置的任何渲染狀態。 在當前版本的Direct3D 11中,ID3DX11EffectPass :: Apply的第一個參數未使用,應指定零; 第二個參數是傳遞將使用的設備上下文的指針。

如果您需要在繪製調用之間的常量緩衝區中更改變量值,則必須在繪製幾何圖形之前調用“應用”來更新更改:

for(UINT i = 0; i < techDesc.Passes; ++i)
{
    ID3DX11EffectPass* pass = mTech->GetPassByIndex(i);
    // Set combined world-view-projection matrix for land geometry.
    worldViewProj = landWorld*view*proj;
    mfxWorldViewProj->SetMatrix(reinterpret_cast<float*>(&worldViewProj));
    pass->Apply(0, md3dImmediateContext);
    mLand.draw();
    // Set combined world-view-projection matrix for wave geometry.
    worldViewProj = wavesWorld*view*proj;
    mfxWorldViewProj->SetMatrix(reinterpret_cast<float*>(&worldViewProj));
    pass->Apply(0, md3dImmediateContext);
    mWaves.draw();
}

6.8.5在編譯時編譯一個效果

到目前爲止,我們已經展示瞭如何通過D3DX11CompileFromFile函數在運行時編譯效果。 這樣做有點讓人煩惱,因爲如果你的效果文件代碼有編譯錯誤,那麼直到你運行該程序時纔會發現它。可以使用DirectX SDK(位於DirectX SDK \ Utilities \ bin \ x86)附帶的fxc工具離線編譯效果。而且,你可以修改你的VC ++項目來調用fxc來編譯你的效果,作爲正常編譯過程的一部分。以下步驟顯示如何執行此操作:
1.確保在項目的VC ++目錄選項卡中的“可執行目錄”下列出了DirectX SDK \ Utilities \ bin \ x86的路徑,如介紹中所述。
2.將效果文件添加到您的項目。
3.對於每個效果,右鍵單擊解決方案資源管理器中的效果文件,選擇屬性,然後添加一個自定義生成工具(見圖6.6)。
調試模式:
   fxc /Fc /Od /Zi /T fx_5_0 /Fo “%(RelativeDir)\%(Filename).fxo” “%(FullPath)”
發佈模式:
   fxc /T fx_5_0 /Fo “%(RelativeDir)\%(Filename).fxo” “%(FullPath)”
您可以查看SDK文檔以獲取fxc編譯標誌的完整列表。 我們用於調試模式的三個標誌“/ Fc / Od / Zi”分別輸出彙編列表,禁用優化和啓用調試信息。

6-6
圖6.6 將自定義構建工具添加到項目

NOTE:不時查看你的着色器的編譯,檢查一下是否有你不希望生成的東西被指示出來。例如,如果在HLSL代碼中有一個條件語句,那麼可能會希望在彙編代碼中存在分支指令。在GPU上分支是相當昂貴的(或者某些DirectX 9硬件不支持),所以有時編譯器會通過評估兩個分支來將條件語句變平,然後在兩者之間進行插值以選擇正確的答案。也就是說,下面的代碼會給出相同的答案:

條件 展平
float x = 0;
// s == 1 (true) or s == 0 (false)
if( s )
{
  x = sqrt(y);
}
else
{
  x = 2*y;
}
float a=2 * y;
float b = sqrt(y);
float x = a + s * (b-a);
// s == 1:
// x = a + b – a = b = sqrt(y)
// s == 0:
// x = a + 0 * (b-a) = a = 2 * y

所以不使用分支扁平方法給了我們相同的結果,但沒有看着彙編代碼,我們不知道是否發生扁平化,或者如果生成一個真正的分支指令。 問題的關鍵在於,有時候你想看看程序到底怎樣執行的。

現在,當你建立你的項目時,fxc將會被調用,併產生一個.fxo文件的效果的編譯版本。 而且,如果有任何來自fxc的編譯警告或錯誤,它們將顯示在調試輸出窗口中。 例如,如果我們在color.fx效果文件中錯誤地命名一個變量:

// Should be gWorldViewProj, not worldViewProj!
vout.PosH = mul(float4(vin.Pos, 1.0f), worldViewProj);

然後我們從調試輸出窗口中列出的這一個錯誤(最重要的錯誤是修復的關鍵錯誤)中得到了一些錯誤:

error X3004: undeclared identifier ‘worldViewProj’
error X3013: ‘mul’: intrinsic function does not take 2 parameters
error X3013: Possible intrinsic functions are:
error X3013: mul(float, float)

在編譯時獲取錯誤消息比在運行時更方便。現在,我們已經將我們的效果文件(.fxo)編譯爲構建過程的一部分,所以我們不再需要在運行時執行它(即,我們不需要調用D3DX11CompileFromFile)。但是,我們仍然需要加載編譯的着色器 來自.fxo文件的數據,並將其提供給D3DX11CreateEffectFromMemory函數。 這可以使用標準的C ++文件輸入機制來完成,如下所示:

std::ifstream fin("fx/color.fxo", std::ios::binary);

fin.seekg(0, std::ios_base::end);
int size = (int)fin.tellg();
fin.seekg(0, std::ios_base::beg);
std::vector<char> compiledShader(size);

fin.read(&compiledShader[0], size);
fin.close();

HR(D3DX11CreateEffectFromMemory(&compiledShader[0], size,
    0, md3dDevice, &mFX));

除了在運行時編譯着色器代碼的“Box”演示例外之外,我們將所有着色器編譯爲構建過程的一部分。

6.8.6作爲“着色器生成器”的效果框架

我們在本節開頭提到,一個效應可以有多種技術。 那麼爲什麼我們會有多種渲染效果技術呢? 讓我們以陰影作爲例子,而不涉及如何完成陰影的細節。 本質上,陰影質量越高,陰影技術越昂貴。 爲了支持低端和高端顯卡的用戶,我們可能會實現一個低,中,高質量的陰影技術。 所以,即使我們有一個陰影效應,我們使用多種技術來實現效果。我們的陰影效果文件可能如下所示:

// Omit constant buffers, vertex structures, etc...
VertexOut VS(VertexIn vin) {/* Omit implementation details */}
float4 LowQualityPS(VertexOut pin) : SV_Target
{
    /* Do work common to all quality levels */
    /* Do low quality specific stuff */
    /* Do more work common to all quality levels */
}
float4 MediumQualityPS(VertexOut pin) : SV_Target
{
    /* Do work common to all quality levels */
    /* Do medium quality specific stuff */
    /* Do more work common to all quality levels */
}
float4 HighQualityPS(VertexOut pin) : SV_Target
{
    /* Do work common to all quality levels */
    /* Do high quality specific stuff */
    /* Do more work common to all quality levels */
}

technique11 ShadowsLow
{
    pass P0
    {
        SetVertexShader(CompileShader(vs_5_0, VS()));
        SetPixelShader(CompileShader(ps_5_0, LowQualityPS()));
    }
}

technique11 ShadowsMedium
{
    pass P0
    {
        SetVertexShader(CompileShader(vs_5_0, VS()));
        SetPixelShader( CompileShader(ps_5_0, MediumQualityPS()));
    }
}

technique11 ShadowsHigh
{
    pass P0
    {
        SetVertexShader(CompileShader(vs_5_0, VS()));
        SetPixelShader( CompileShader(ps_5_0, HighQualityPS()));
    }
}

然後,C ++應用程序代碼可以檢測用戶的圖形卡的能力,並選擇適當的技術來使用
渲染。

NOTE:前面的代碼假定三種陰影技術中只有像素着色器不同,所以所有的技術都共享同一個頂點着色器。 但是,如果需要的話,每種技術也可以有不同的頂點着色器。

先前實現中的一個煩人的問題是,儘管像素着色器在陰影代碼中有所不同,但它們仍然具有所有需要被複制的通用代碼。 有人可能會建議使用條件語句,這是朝正確方向邁出的一步。 在着色器中動態分支語句有一些開銷,所以如果我們真的需要,我們只應該使用它們。 我們真正想要做的是一個條件編譯,它會在編譯時生成我們需要的所有着色器變體,所以在着色器代碼中沒有分支指令。 幸運的是,效果框架提供了一個方法來做到這一點。 考慮新的實現:

// Omit constant buffers, vertex structures, etc...
VertexOut VS(VertexIn vin) {/* Omit implementation details */}
#define LowQuality 0
#define MediumQuality 1
#define HighQuality 2
float4 PS(VertexOut pin, uniform int gQuality) : SV_Target
{
    /* Do work common to all quality levels */
    if(gQuality == LowQuality)
    {
        /* Do low quality specific stuff */
    }
    else if(gQuality == MediumQuality)
    {
        /* Do medium quality specific stuff */
    }
    else
    {
        /* Do high quality specific stuff */
    }
    /* Do more work common to all quality levels */
}

technique11 ShadowsLow
{
    pass P0
    {
        SetVertexShader(CompileShader(vs_5_0, VS()));
        SetPixelShader(CompileShader(ps_5_0, PS(LowQuality)));
    }
}

technique11 ShadowsMedium
{
    pass P0
    {
        SetVertexShader(CompileShader(vs_5_0, VS()));
        SetPixelShader( CompileShader(ps_5_0, PS(MediumQuality)));
    }
}

technique11 ShadowsHigh
{
    pass P0
    {
        SetVertexShader(CompileShader(vs_5_0, VS()));
        SetPixelShader(CompileShader(ps_5_0, PS(HighQuality)));
    }
}

注意到我們已經爲像素着色器添加了一個額外的統一參數來表示質量級別。 這個參數是不同的,因爲它不會改變每個像素,而是統一/常量。 而且,我們不會在運行時更改它,就像我們更改常量緩衝區變量一樣。 相反,我們在編譯時設置它,並且由於該值在編譯時已知,因此它允許效果框架根據其值生成不同的着色器變體。 這使我們能夠創建我們的低,中,高質量的像素着色器,而無需複製代碼(效果框架基本上爲我們複製了編譯時間過程中的代碼),而無需使用分支指令。

這裏有一些其他的着色器生成常見的例子:
1.應用紋理? 應用程序可能希望將紋理應用於某些對象,但不適用於其他對象。 一種解決方案是創建兩個像素着色器,一個應用紋理,另一個不應用。 或者我們可以使用着色器生成器機制爲我們創建兩個像素着色器,然後讓C ++應用程序選擇所需的技術。

float4 PS(VertexOut pin, uniform bool gApplyTexture) : SV_Target
{
    /* Do common work */
    if(gApplyTexture)
    {
        /* Apply texture */
    }
    /* Do more common work */
}
technique11 BasicTech
{
    pass P0
    {
        SetVertexShader(CompileShader(vs_5_0, VS()));
        SetPixelShader(CompileShader(ps_5_0, PS(false)));
    }
}
technique11 TextureTech
{
    pass P0
    {
        SetVertexShader(CompileShader(vs_5_0, VS()));
        SetPixelShader(CompileShader(ps_5_0, PS(true)));
    }
}

2.有多少燈使用? 遊戲關卡可以在任何給定的時間支持1到4個活動燈光。 燈光使用越多,燈光計算的成本就越高。 我們可以根據燈光的數量實現單獨的頂點着色器,或者我們可以使用着色器生成器機制爲我們創建四個頂點着色器,然後讓C ++應用程序根據當前活動燈光的數量選擇所需的技術:

VertexOut VS(VertexOut pin, uniform int gLightCount)
{
    /* Do common work */
    for(int i = 0; i < gLightCount; ++i)
    {
        /* do lighting work */
    }
    /* Do more common work */
}
technique11 Light1
{
    pass P0
    {
        SetVertexShader(CompileShader(vs_5_0, VS(1)));
        SetPixelShader(CompileShader(ps_5_0, PS()));
    }
}
technique11 Light2
{
    pass P0
    {
        SetVertexShader(CompileShader(vs_5_0, VS(2)));
        SetPixelShader( CompileShader(ps_5_0, PS()));
    }
}
technique11 Light3
{
    pass P0
    {
        SetVertexShader(CompileShader(vs_5_0, VS(3)));
        SetPixelShader(CompileShader(ps_5_0, PS()));
    }
}
technique11 Light4
{
    pass P0
    {
        SetVertexShader(CompileShader(vs_5_0, VS(4)));
        SetPixelShader(CompileShader(ps_5_0, PS()));
    }
}

沒有任何東西將我們限制在一個參數上。 我們將需要將陰影質量,紋理和燈光的數量結合在一起,因此頂點和像素着色器將如下所示:

VertexOut VS(VertexOut pin, uniform int gLightCount)
{}
float4 PS(VertexOut pin,
uniform int gQuality,
uniform bool gApplyTexture) : SV_Target
{}

例如,爲了創建一種使用低質量陰影,兩個燈光和沒有紋理的技術,我們會寫:

technique11 LowShadowsTwoLightsNoTextures
{
    pass P0
    {
        SetVertexShader(CompileShader(vs_5_0, VS(2)));
        SetPixelShader(CompileShader(ps_5_0, PS(LowQuality, false)));
    }
}

6.8.7 彙編看起來像什麼

如果您不打算查看某個效果文件的彙編輸出,那麼我們將在此部分中顯示一個樣子。不過,我們不解釋彙編,但是如果您之前學過彙編,則可能會認出 mov指令,也許可以猜測dp4是一個4D點的產品。 即使沒有了解彙編,列表提供了一些有用的信息。 它清楚地標識了輸入和輸出簽名,並給出了大致的指令計數,這是一個有用的度量標準,以瞭解着色器的成本/複雜程度。 而且,我們看到,我們着色器的多個版本確實是基於編譯時間參數生成的,沒有分支指令。 除了我們添加一個簡單的統一布爾參數來產生兩種技術之外,我們使用的效果文件與§6.8.1中所示的效果文件相同。

float4 PS(VertexOut pin, uniform bool gUseColor) : SV_Target
{
    if(gUseColor)
    {
        return pin.Color;
    }
    else
    {
    return float4(0, 0, 0, 1);
    }
}
technique11 ColorTech
{
    pass P0
    {
        SetVertexShader(CompileShader(vs_5_0, VS()));
        SetPixelShader(CompileShader(ps_5_0, PS(true)));
    }
}
technique11 NoColorTech
{
    pass P0
    {
        SetVertexShader(CompileShader(vs_5_0, VS()));
        SetPixelShader(CompileShader(ps_5_0, PS(false)));
    }
}
//
// FX Version: fx_5_0
//
// 1 local buffer(s)
//
cbuffer cbPerObject
{
    float4x4 gWorldViewProj; // Offset: 0, size: 64
}
//
// 1 groups(s)
//
fxgroup
{
//
// 2 technique(s)
//
technique11 ColorTech
{
    pass P0
    {
        VertexShader = asm {
        //
        // Generated by Microsoft (R) HLSL Shader Compiler 9.29.952.3111
        //
        //
        // Buffer Definitions:
        //
        // cbuffer cbPerObject
        // {
        //
        // float4x4 gWorldViewProj; // Offset: 0 Size: 64
        //
        // }
        //
        //
        // Resource Bindings:
        //
        // Name Type Format Dim Slot Elements
        // ------------------- ----- ------ ----------- ---- --------
        // cbPerObject cbuffer NA NA 0 1
        //
        //
        //
        // Input signature:
        //
        // Name Index Mask Register SysValue Format Used
        // ------------- ----- ------ -------- -------- ------ ------
        // POSITION 0 xyz 0 NONE float xyz
        // COLOR 0 xyzw 1 NONE float xyzw
        //
        //
        // Output signature:
        //
        // Name Index Mask Register SysValue Format Used
        // ------------- ----- ------ -------- -------- ------ ------
        // SV_POSITION 0 xyzw 0 POS float xyzw
        // COLOR 0 xyzw 1 NONE float xyzw
        //
        vs_5_0
        dcl_globalFlags refactoringAllowed
        dcl_constantbuffer cb0[4], immediateIndexed
        dcl_input v0.xyz
        dcl_input v1.xyzw
        dcl_output_siv o0.xyzw, position
        dcl_output o1.xyzw
        dcl_temps 1
        mov r0.xyz, v0.xyzx
        mov r0.w, l(1.000000)
        dp4 o0.x, r0.xyzw, cb0[0].xyzw
        dp4 o0.y, r0.xyzw, cb0[1].xyzw
        dp4 o0.z, r0.xyzw, cb0[2].xyzw
        dp4 o0.w, r0.xyzw, cb0[3].xyzw
        mov o1.xyzw, v1.xyzw
        ret
        // Approximately 8 instruction slots used
    };
    PixelShader = asm {
        //
        // Generated by Microsoft (R) HLSL Shader Compiler 9.29.952.3111
        //
        //
        //
        // Input signature:
        //
        // Name Index Mask Register SysValue Format Used
        // ------------- ----- ------ -------- -------- ------ ------
        // SV_POSITION 0 xyzw 0 POS float
        // COLOR 0 xyzw 1 NONE float xyzw
        //
        //
        // Output signature:
        //
        // Name Index Mask Register SysValue Format Used
        // ------------- ----- ------ -------- -------- ------ ------
        // SV_Target 0 xyzw 0 TARGET float xyzw
        //
        ps_5_0
        dcl_globalFlags refactoringAllowed
        dcl_input_ps linear v1.xyzw
        dcl_output o0.xyzw
        mov o0.xyzw, v1.xyzw
        ret
        // Approximately 2 instruction slots used
        };
    }
}
t
echnique11 NoColorTech
{
pass P0
{
VertexShader = asm {
//
// Generated by Microsoft (R) HLSL Shader Compiler 9.29.952.3111
//
//
// Buffer Definitions:
//
// cbuffer cbPerObject
// {
//
// float4x4 gWorldViewProj; // Offset: 0 Size: 64
//
// }
//
//
// Resource Bindings:
//
// Name Type Format Dim Slot Elements
// ------------------- ----- ------ ----------- ---- --------
// cbPerObject cbuffer NA NA 0 1
//
//
//
// Input signature:
//
// Name Index Mask Register SysValue Format Used
// ------------- ----- ------ -------- -------- ------ ------
// POSITION 0 xyz 0 NONE float xyz
// COLOR 0 xyzw 1 NONE float xyzw
//
//
// Output signature:
//
// Name Index Mask Register SysValue Format Used
// ------------- ----- ------ -------- -------- ------ ------
// SV_POSITION 0 xyzw 0 POS float xyzw
// COLOR 0 xyzw 1 NONE float xyzw
//
vs_5_0
dcl_globalFlags refactoringAllowed
dcl_constantbuffer cb0[4], immediateIndexed
dcl_input v0.xyz
dcl_input v1.xyzw
dcl_output_siv o0.xyzw, position
dcl_output o1.xyzw
dcl_temps 1
mov r0.xyz, v0.xyzx
mov r0.w, l(1.000000)
dp4 o0.x, r0.xyzw, cb0[0].xyzw
dp4 o0.y, r0.xyzw, cb0[1].xyzw
dp4 o0.z, r0.xyzw, cb0[2].xyzw
dp4 o0.w, r0.xyzw, cb0[3].xyzw
mov o1.xyzw, v1.xyzw
ret
// Approximately 8 instruction slots used
};
PixelShader = asm {
//
// Generated by Microsoft (R) HLSL Shader Compiler 9.29.952.3111
//
//
//
// Input signature:
//
// Name Index Mask Register SysValue Format Used
// ------------- ----- ------ -------- -------- ------ ------
// SV_POSITION 0 xyzw 0 POS float
// COLOR 0 xyzw 1 NONE float
//
//
// Output signature:
//
// Name Index Mask Register SysValue Format Used
// ------------- ----- ------ -------- -------- ------ ------
// SV_Target 0 xyzw 0 TARGET float xyzw
//
ps_5_0
dcl_globalFlags refactoringAllowed
dcl_output o0.xyzw
mov o0.xyzw, l(0,0,0,1.000000)
ret
// Approximately 2 instruction slots used
};
}
}
}

6.9 BOX 示例

最後,我們已經學習了足夠的知識來做一個簡單的示例,它呈現一個彩色立方體。這個例子基本上把我們在本章中學的所有東西放到一個程序中。值得仔細研讀。注意,該程序使用§6.8.1中的“color.fx”效果。

//*********************************************************************
// BoxDemo.cpp by Frank Luna (C) 2011 All Rights Reserved.
//
// Demonstrates rendering a colored box.
//
// Controls:
// Hold the left mouse button down and move the mouse to rotate.
// Hold the right mouse button down to zoom in and out.
//
//*********************************************************************
#include "d3dApp.h"
#include "d3dx11Effect.h"
#include "MathHelper.h"
struct Vertex
{
    XMFLOAT3 Pos;
    XMFLOAT4 Color;
};
class BoxApp : public D3DApp
{
public: 
    BoxApp(HINSTANCE hInstance);
    ~BoxApp();
    bool Init();
    void OnResize();
    void UpdateScene(float dt);
    void DrawScene();
    void OnMouseDown(WPARAM btnState, int x, int y);
    void OnMouseUp(WPARAM btnState, int x, int y);
    void OnMouseMove(WPARAM btnState, int x, int y);
private:
    void BuildGeometryBuffers();
    void BuildFX();
    void BuildVertexLayout();
private:
    ID3D11Buffer* mBoxVB;
    ID3D11Buffer* mBoxIB;
    ID3DX11Effect* mFX;
    ID3DX11EffectTechnique* mTech;
    ID3DX11EffectMatrixVariable* mfxWorldViewProj;
    ID3D11InputLayout* mInputLayout;
    XMFLOAT4X4 mWorld;
    XMFLOAT4X4 mView;
    XMFLOAT4X4 mProj;
    float mTheta;
    float mPhi;
    float mRadius;
    POINT mLastMousePos;
};
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE prevInstance,
    PSTR cmdLine, int showCmd)
{
    // Enable run-time memory check for debug builds.
#if defined(DEBUG) | defined(_DEBUG)
    _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
#endif
    BoxApp theApp(hInstance);
    if(!theApp.Init())
        return 0;
    return theApp.Run();
}
BoxApp::BoxApp(HINSTANCE hInstance)
: D3DApp(hInstance), mBoxVB(0), mBoxIB(0), mFX(0), mTech(0),
    mfxWorldViewProj(0), mInputLayout(0),
    mTheta(1.5f*MathHelper::Pi), mPhi(0.25f*MathHelper::Pi), mRadius(5.0f)
{
    mMainWndCaption = L"Box Demo";
    mLastMousePos.x = 0;
    mLastMousePos.y = 0;
    XMMATRIX I = XMMatrixIdentity();
    XMStoreFloat4x4(&mWorld, I);
    XMStoreFloat4x4(&mView, I);
    XMStoreFloat4x4(&mProj, I);
}
BoxApp::~BoxApp()
{
    ReleaseCOM(mBoxVB);
    ReleaseCOM(mBoxIB);
    ReleaseCOM(mFX);
    ReleaseCOM(mInputLayout);
}
bool BoxApp::Init()
{
    if(!D3DApp::Init())
        return false;
    BuildGeometryBuffers();
    BuildFX();
    BuildVertexLayout();
    return true;
}
void BoxApp::OnResize()
{
    D3DApp::OnResize();
    // The window resized, so update the aspect ratio and recomputed
    // the projection matrix.
    XMMATRIX P = XMMatrixPerspectiveFovLH(0.25f*MathHelper::Pi,
    AspectRatio(), 1.0f, 1000.0f);
    XMStoreFloat4x4(&mProj, P);
}
void BoxApp::UpdateScene(float dt)
{
    // Convert Spherical to Cartesian coordinates.
    float x = mRadius*sinf(mPhi)*cosf(mTheta);
    float z = mRadius*sinf(mPhi)*sinf(mTheta);
    float y = mRadius*cosf(mPhi);
    // Build the view matrix.
    XMVECTOR pos = XMVectorSet(x, y, z, 1.0f);
    XMVECTOR target = XMVectorZero();
    XMVECTOR up = XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f);
    XMMATRIX V = XMMatrixLookAtLH(pos, target, up);
    XMStoreFloat4x4(&mView, V);
}
void BoxApp::DrawScene()
{
    md3dImmediateContext->ClearRenderTargetView(mRenderTargetView,
    reinterpret_cast<const float*>(&Colors::Blue));
        md3dImmediateContext->ClearDepthStencilView(mDepthStencilView,
D3D11_CLEAR_DEPTH|D3D11_CLEAR_STENCIL, 1.0f, 0);
md3dImmediateContext->IASetInputLayout(mInputLayout);
md3dImmediateContext->IASetPrimitiveTopology(
D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
UINT stride = sizeof(Vertex);
UINT offset = 0;
md3dImmediateContext->IASetVertexBuffers(0, 1, &mBoxVB,
&stride, &offset);
md3dImmediateContext->IASetIndexBuffer(mBoxIB,
DXGI_FORMAT_R32_UINT, 0);
// Set constants
XMMATRIX world = XMLoadFloat4x4(&mWorld);
XMMATRIX view = XMLoadFloat4x4(&mView);
XMMATRIX proj = XMLoadFloat4x4(&mProj);
XMMATRIX worldViewProj = world*view*proj;
mfxWorldViewProj->SetMatrix(reinterpret_cast<float*>(&worldViewProj));
D3DX11_TECHNIQUE_DESC techDesc;
mTech->GetDesc( &techDesc );
for(UINT p = 0; p < techDesc.Passes; ++p)
{
mTech->GetPassByIndex(p)->Apply(0, md3dImmediateContext);
// 36 indices for the box.
md3dImmediateContext->DrawIndexed(36, 0, 0);
}
HR(mSwapChain->Present(0, 0));
}
void BoxApp::OnMouseDown(WPARAM btnState, int x, int y)
{
mLastMousePos.x = x;
mLastMousePos.y = y;
SetCapture(mhMainWnd);
}
void BoxApp::OnMouseUp(WPARAM btnState, int x, int y)
{
ReleaseCapture();
}
void BoxApp::OnMouseMove(WPARAM btnState, int x, int y)
{
if( (btnState & MK_LBUTTON) != 0 )
{
// Make each pixel correspond to a quarter of a degree.
float dx = XMConvertToRadians(
0.25f*static_cast<float>(x - mLastMousePos.x));
float dy = XMConvertToRadians(
0.25f*static_cast<float>(y - mLastMousePos.y));
// Update angles based on input to orbit camera around box.
mTheta += dx;
mPhi += dy;
// Restrict the angle mPhi.
mPhi = MathHelper::Clamp(mPhi, 0.1f, MathHelper::Pi-0.1f);
}
else if( (btnState & MK_RBUTTON) != 0 )
{
// Make each pixel correspond to 0.005 unit in the scene.
float dx = 0.005f*static_cast<float>(x - mLastMousePos.x);
float dy = 0.005f*static_cast<float>(y - mLastMousePos.y);
// Update the camera radius based on input.
mRadius += dx - dy;
// Restrict the radius.
mRadius = MathHelper::Clamp(mRadius, 3.0f, 15.0f);
}
mLastMousePos.x = x;
mLastMousePos.y = y;
}
void BoxApp::BuildGeometryBuffers()
{
// Create vertex buffer
Vertex vertices[] =
{
{ XMFLOAT3(-1.0f, -1.0f, -1.0f), (const float*)&Colors::White },
{ XMFLOAT3(-1.0f, +1.0f, -1.0f), (const float*)&Colors::Black },
{ XMFLOAT3(+1.0f, +1.0f, -1.0f), (const float*)&Colors::Red },
{ XMFLOAT3(+1.0f, -1.0f, -1.0f), (const float*)&Colors::Green },
{ XMFLOAT3(-1.0f, -1.0f, +1.0f), (const float*)&Colors::Blue },
{ XMFLOAT3(-1.0f, +1.0f, +1.0f), (const float*)&Colors::Yellow },
{ XMFLOAT3(+1.0f, +1.0f, +1.0f), (const float*)&Colors::Cyan },
{ XMFLOAT3(+1.0f, -1.0f, +1.0f), (const float*)&Colors::Magenta }
};
D3D11_BUFFER_DESC vbd;
vbd.Usage = D3D11_USAGE_IMMUTABLE;
vbd.ByteWidth = sizeof(Vertex) * 8;
vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER;
vbd.CPUAccessFlags = 0;
vbd.MiscFlags = 0;
vbd.StructureByteStride = 0;
D3D11_SUBRESOURCE_DATA vinitData;
vinitData.pSysMem = vertices;
HR(md3dDevice->CreateBuffer(&vbd, &vinitData, &mBoxVB));
// Create the index buffer
UINT indices[] = {
// front face
0, 1, 2,
0, 2, 3,
// back face
4, 6, 5,
4, 7, 6,
// left face
4, 5, 1,
4, 1, 0,
// right face
3, 2, 6,
3, 6, 7,
// top face
1, 5, 6,
1, 6, 2,
// bottom face
4, 0, 3,
4, 3, 7
};
D3D11_BUFFER_DESC ibd;
ibd.Usage = D3D11_USAGE_IMMUTABLE;
ibd.ByteWidth = sizeof(UINT) * 36;
ibd.BindFlags = D3D11_BIND_INDEX_BUFFER;
ibd.CPUAccessFlags = 0;
ibd.MiscFlags = 0;
ibd.StructureByteStride = 0;
D3D11_SUBRESOURCE_DATA iinitData;
iinitData.pSysMem = indices;
HR(md3dDevice->CreateBuffer(&ibd, &iinitData, &mBoxIB));
}
void BoxApp::BuildFX()
{
DWORD shaderFlags = 0;
#if defined(DEBUG) || defined(_DEBUG)
shaderFlags |= D3D10_SHADER_DEBUG;
shaderFlags |= D3D10_SHADER_SKIP_OPTIMIZATION;
#endif
ID3D10Blob* compiledShader = 0;
ID3D10Blob* compilationMsgs = 0;
HRESULT hr = D3DX11CompileFromFile(L"FX/color.fx", 0, 0, 0,
"fx_5_0", shaderFlags,
0, 0, &compiledShader, &compilationMsgs, 0);
// compilationMsgs can store errors or warnings.
if(compilationMsgs != 0)
{
MessageBoxA(0, (char*)compilationMsgs->GetBufferPointer(), 0, 0);
ReleaseCOM(compilationMsgs);
}
// Even if there are no compilationMsgs, check to make sure there
// were no other errors.
if(FAILED(hr))
{
DXTrace(__FILE__, (DWORD)__LINE__, hr,
L"D3DX11CompileFromFile", true);
}
HR(D3DX11CreateEffectFromMemory(
compiledShader->GetBufferPointer(),
compiledShader->GetBufferSize(),
0, md3dDevice, &mFX));
// Done with compiled shader.
ReleaseCOM(compiledShader);
mTech = mFX->GetTechniqueByName("ColorTech");
mfxWorldViewProj = mFX->GetVariableByName(
"gWorldViewProj")->AsMatrix();
}
void BoxApp::BuildVertexLayout()
{
// Create the vertex input layout.
D3D11_INPUT_ELEMENT_DESC vertexDesc[] =
{
{"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}
};
// Create the input layout
D3DX11_PASS_DESC passDesc;
mTech->GetPassByIndex(0)->GetDesc(&passDesc);
HR(md3dDevice->CreateInputLayout(vertexDesc, 2,
passDesc.pIAInputSignature,
passDesc.IAInputSignatureSize, &mInputLayout));
}

6-7
圖6.7 “盒子”演示的屏幕截圖。

6.10 HILLS DEMO

本章還包括一個“山”演示。 它使用與Box演示相同的Direct3D方法,除了繪製更復雜的幾何圖形。具體說明如何構造一個三角網格網格; 這種幾何結構特別適用於地形和水面渲染等等。

一個“好的”實值函數y = f(x,z)的圖形是一個曲面。我們可以通過在xz平面中構造一個網格來近似表面,其中每個四邊形由兩個三角形構建,然後將函數應用到每個網格點; 見圖6.8。

6-8
圖 6.8 (上)在xz平面放下一個網格。 (底部)對於每個網格點,應用函數f(x,z)來獲得y座標。 的情節點(x,f(x,z),z)給出曲面圖。

6.10.1生成網格頂點

所以主要的任務是如何在xz平面中建立網格。 如圖6.9所示,m×n個頂點的網格包含(m-1)×(n-1)個四元組(或單元)。 每個單元格將被兩個三角形覆蓋,所以總共有2·(m-1)×(n-1)個三角形。 如果網格具有寬度w和深度d,則沿着x軸的單元間距是dx = w /(n-1),沿z軸的單元間距是dz = d /(m-1)。 爲了生成頂點,我們從左上角開始逐行遞增地計算頂點座標。 第i個網格頂點在xz平面中的座標由下式給出:

vij=[0.5w+j·dx,0.0,0.5di·dz)]

以下代碼生成網格頂點:
void GeometryGenerator::CreateGrid(float width, float depth,
        UINT m, UINT n, MeshData& meshData)
{
    UINT vertexCount = m*n;
    UINT faceCount = (m-1)*(n-1)*2;
    //
    // Create the vertices.
    //
    float halfWidth = 0.5f*width;
    float halfDepth = 0.5f*depth;

    float dx = width / (n-1);
    float dz = depth / (m-1);
    float du = 1.0f / (n-1);
    float dv = 1.0f / (m-1);

    meshData.Vertices.resize(vertexCount);
    for(UINT i = 0; i < m; ++i)
    {
        float z = halfDepth - i*dz;
        for(UINT j = 0; j < n; ++j)
        {
            float x = -halfWidth + j*dx;
            meshData.Vertices[i*n+j].Position = XMFLOAT3(x, 0.0f, z);

            meshData.Vertices[i*n+j].Normal = XMFLOAT3(0.0f, 1.0f, 0.0f);
            meshData.Vertices[i*n+j].TangentU = XMFLOAT3(1.0f, 0.0f, 0.0f);
            // Ignore for now, used for texturing.
            meshData.Vertices[i*n+j].TexC.x = j*du;
            meshData.Vertices[i*n+j].TexC.y = i*dv;
        }
    }
}

6-9
圖6.9 網格構建

GeometryGenerator是一個實用工具類,用於生成簡單的幾何形狀,如網格,球體,圓柱體和盒子,我們在本書中使用這些簡單幾何形狀來演示程序。該類在系統內存中生成數據,然後我們必須將我們想要的數據複製到頂點和索引緩衝區。GeometryGenerator創建一些將在後面的章節中使用的頂點數據。我們目前的演示中不需要這些數據,因此我們不會將這些數據複製到頂點緩衝區中。MeshData結構是一個嵌套在GeometryGenerator中的簡單結構,它存儲頂點和索引列表:

class GeometryGenerator
{
public:
struct Vertex
{
Vertex(){}
Vertex(const XMFLOAT3& p,
const XMFLOAT3& n,
const XMFLOAT3& t,
const XMFLOAT2& uv)
: Position(p), Normal(n), TangentU(t), TexC(uv){}
Vertex(
float px, float py, float pz,
float nx, float ny, float nz,
float tx, float ty, float tz,
float u, float v)
: Position(px,py,pz), Normal(nx,ny,nz),
TangentU(tx, ty, tz), TexC(u,v){}
XMFLOAT3 Position;
XMFLOAT3 Normal;
XMFLOAT3 TangentU;
XMFLOAT2 TexC;
};
struct MeshData
{
std::vector<Vertex> Vertices;
std::vector<UINT> Indices;
};
...
};

6.10.2 生成網格索引

在計算頂點後,我們需要通過指定索引來定義網格三角形。爲此,我們在每個四元組上重複遍歷,從左上角開始逐行重複計算索引,以定義四元組的兩個三角形; 參考圖6.10,對於m×n頂點網格,兩個三角形的線性陣列索引計算如下:
ΔABC = (i · n + j, i · n + j + 1, (i + 1) · n + j)
ΔCBD = ((i + 1) ·n + j, i ·n + j + 1, (i + 1) · n + j + 1)

6-10
圖6.10 第ij個四元組頂點的索引。

相應的代碼:

meshData.Indices.resize(faceCount*3); // 3 indices per face
    // Iterate over each quad and compute indices.
    UINT k = 0;
    for(UINT i = 0; i < m-1; ++i)
    {
        for(UINT j = 0; j < n-1; ++j)
        {
            meshData.Indices[k] = i*n+j;
            meshData.Indices[k+1] = i*n+j+1;
            meshData.Indices[k+2] = (i+1)*n+j;
            meshData.Indices[k+3] = (i+1)*n+j;
            meshData.Indices[k+4] = i*n+j+1;
            meshData.Indices[k+5] = (i+1)*n+j+1;
            k += 6; // next quad
        }
    }
}

6.10.3 應用高度函數

在我們創建了網格之後,我們可以從MeshData網格中提取我們想要的頂點元素,將平坦網格變成代表山丘的曲面,並根據頂點高度(y座標)爲每個頂點生成顏色。

// Not to be confused with GeometryGenerator.Vertex.
struct Vertex
{
XMFLOAT3 Pos;
XMFLOAT4 Color;
};
void HillsApp::BuildGeometryBuffers()
{
GeometryGenerator::MeshData grid;
GeometryGenerator geoGen;
geoGen.CreateGrid(160.0f, 160.0f, 50, 50, grid);
mGridIndexCount = grid.Indices.size();
//
// Extract the vertex elements we are interested and apply the
// height function to each vertex. In addition, color the vertices
// based on their height so we have sandy looking beaches, grassy low
// hills, and snow mountain peaks.
//
std::vector<Vertex> vertices(grid.Vertices.size());
for(size_t i = 0; i < grid.Vertices.size(); ++i)
{
XMFLOAT3 p = grid.Vertices[i].Position;
p.y = GetHeight(p.x, p.z);
vertices[i].Pos = p;
// Color the vertex based on its height.
if( p.y < -10.0f )
{
// Sandy beach color.
vertices[i].Color = XMFLOAT4(1.0f, 0.96f, 0.62f, 1.0f);
}
else if(p.y < 5.0f)
{
// Light yellow-green.
vertices[i].Color = XMFLOAT4(0.48f, 0.77f, 0.46f, 1.0f);
}
else if(p.y < 12.0f)
{
// Dark yellow-green.
vertices[i].Color = XMFLOAT4(0.1f, 0.48f, 0.19f, 1.0f);
}
else if(p.y < 20.0f)
{
// Dark brown.
vertices[i].Color = XMFLOAT4(0.45f, 0.39f, 0.34f, 1.0f);
}
else
{
// White snow.
vertices[i].Color = XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f);
}
}
D3D11_BUFFER_DESC vbd;
vbd.Usage = D3D11_USAGE_IMMUTABLE;
vbd.ByteWidth = sizeof(Vertex) * grid.Vertices.size();
vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER;
vbd.CPUAccessFlags = 0;
vbd.MiscFlags = 0;
D3D11_SUBRESOURCE_DATA vinitData;
vinitData.pSysMem = &vertices[0];
HR(md3dDevice->CreateBuffer(&vbd, &vinitData, &mVB));
//
// Pack the indices of all the meshes into one index buffer.
//
D3D11_BUFFER_DESC ibd;
ibd.Usage = D3D11_USAGE_IMMUTABLE;
ibd.ByteWidth = sizeof(UINT) * mGridIndexCount;
ibd.BindFlags = D3D11_BIND_INDEX_BUFFER;
ibd.CPUAccessFlags = 0;
ibd.MiscFlags = 0;
D3D11_SUBRESOURCE_DATA iinitData;
iinitData.pSysMem = &grid.Indices[0];
HR(md3dDevice->CreateBuffer(&ibd, &iinitData, &mIB));
}

6-11
圖6.11 “Hills Demo”的屏幕截圖

我們在這個演示中使用的函數f(x,z)由下式給出:

float HillsApp::GetHeight(float x, float z)const
{
return 0.3f*(z*sinf(0.1f*x) + x*cosf(0.1f*z));
}

它的圖形看起來有點像山丘和山谷的地形(見圖6.11)。 演示程序的其餘部分與盒式演示非常相似。

6.11 形狀Demo

在本節中,我們將介紹如何構建GeometryGenerator類支持的其他兩種幾何形狀:球體和圓柱體。這些形狀對繪製天空圓頂,調試,可視化碰撞檢測和延遲渲染非常有用。例如,您可能想要將所有遊戲角色渲染爲球體以進行調試測試。

圖6.12顯示了本節演示的屏幕截圖。除了學習如何繪製球體和圓柱體外,您還可以獲得在場景中定位和繪製多個對象的經驗(即創建多個世界變換矩陣)。此外,我們將所有場景幾何圖形放置在一個大頂點和索引緩衝區中。然後,我們將使用DrawIndexed方法一次繪製一個對象(因爲需要在對象之間更改世界矩陣); 所以你會看到一個使用DrawIndexedStartIndexLocationBaseVertexLocation參數的例子。

6-12
圖6.12 “形狀”演示的屏幕截圖。

6.11.1 生成圓柱網格

我們通過指定圓柱體的底部和頂部半徑,高度以及切片和堆疊數來定義圓柱體,如圖6.13所示。我們將圓柱體分成三部分:1)側面幾何體,2)頂蓋幾何體,以及3)底蓋幾何體。

6.11.1.1 圓柱側面幾何

我們生成以原點爲中心的圓柱體,平行於y軸。從圖6.13中可以看出,所有頂點都位於圓柱體的“環”上,其中有stackCount + 1個環,每個環都有sliceCount唯一的頂點。連續環之間的半徑差值爲Δr=(topRadius - bottomRadius)/stackCount。如果我們從索引爲0的底環開始,那麼第i個環的半徑爲ri=bottomRadius+iΔr ,第i個環的高度爲hi=h2+iΔh ,其中Δh是堆疊高度,h是圓柱體高度。 所以基本思想是迭代每個環並生成位於該環上的頂點。 這給出了以下實現(我們用粗體顯示了相關的代碼):

6-13
圖6.13 在此圖中,左側的圓柱體有8個切片和4個疊層,右側的圓柱體有16個切片和8個疊層。 切片和堆疊控制三角形密度。 請注意,頂部和底部的半徑可以不同,以便我們可以創建錐形物體,而不僅僅是“純”柱面。

void GeometryGenerator::CreateCylinder(float bottomRadius, float topRadius, float height, UINT sliceCount, UINT
stackCount, MeshData& meshData)
{
meshData.Vertices.clear();
meshData.Indices.clear();
//
// Build Stacks.
//
float stackHeight = height / stackCount;
// Amount to increment radius as we move up each stack
// level from bottom to top.
float radiusStep = (topRadius - bottomRadius) / stackCount;
UINT ringCount = stackCount+1;
// Compute vertices for each stack ring starting at
// the bottom and moving up.
for(UINT i = 0; i < ringCount; ++i)
{
float y = -0.5f*height + i*stackHeight;
float r = bottomRadius + i*radiusStep;
// vertices of ring
float dTheta = 2.0f*XM_PI/sliceCount;
for(UINT j = 0; j <= sliceCount; ++j)
{
Vertex vertex;
float c = cosf(j*dTheta);
float s = sinf(j*dTheta);
vertex.Position = XMFLOAT3(r*c, y, r*s);
vertex.TexC.x = (float)j/sliceCount;
vertex.TexC.y = 1.0f - (float)i/stackCount;
// Cylinder can be parameterized as follows, where we
// introduce v parameter that goes in the same direction
// as the v tex-coord so that the bitangent goes in the
// same direction as the v tex-coord.
// Let r0 be the bottom radius and let r1 be the
// top radius.
// y(v) = h - hv for v in [0,1].
// r(v) = r1 + (r0-r1)v
//
// x(t, v) = r(v)*cos(t)
// y(t, v) = h - hv
// z(t, v) = r(v)*sin(t)
//
// dx/dt = -r(v)*sin(t)
// dy/dt = 0
// dz/dt = +r(v)*cos(t)
//
// dx/dv = (r0-r1)*cos(t)
// dy/dv = -h
// dz/dv = (r0-r1)*sin(t)
// TangentU us unit length.
vertex.TangentU = XMFLOAT3(-s, 0.0f, c);
float dr = bottomRadius-topRadius;
XMFLOAT3 bitangent(dr*c, -height, dr*s);
XMVECTOR T = XMLoadFloat3(&vertex.TangentU);
XMVECTOR B = XMLoadFloat3(&bitangent);
XMVECTOR N = XMVector3Normalize(XMVector3Cross(T, B));
XMStoreFloat3(&vertex.Normal, N);
meshData.Vertices.push_back(vertex);
}
}

Note:觀察每個環的第一個和最後一個頂點在位置上是重複的,但紋理座標不重複。 我們必須這樣做,以便我們可以正確地將紋理應用於圓柱體。
實際的方法GeometryGenerator::CreateCylinder會創建額外的頂點數據,例如法線向量和紋理座標,這些將在未來的演示中很有用。 現在不要擔心這些數量。

從圖6.14可以看出,每個堆棧中的每個切片都有一個四邊形(兩個三角形)。 圖6.14顯示了第i個棧和第j個分區的索引由下式給出:

6-14
圖6.14 包含在第i和第i + 1個環中的頂點A,B,C,D和第j個切片。

ΔABC = (i · n + j, (i + 1) · n + j), (i + 1) · n + j + 1)
ΔACD = (i · n + j, (i + 1) · n + j + 1, i · n + j + 1
其中n是每個環的頂點數。所以關鍵的想法是循環遍歷每個堆棧中的每個切片,並應用上述公式。

// Add one because we duplicate the first and last vertex per ring
// since the texture coordinates are different.
UINT ringVertexCount = sliceCount+1;
// Compute indices for each stack.
for(UINT i = 0; i < stackCount; ++i)
{
for(UINT j = 0; j < sliceCount; ++j)
{
meshData.Indices.push_back(i*ringVertexCount + j);
meshData.Indices.push_back((i+1)*ringVertexCount + j);
meshData.Indices.push_back((i+1)*ringVertexCount + j+1);
meshData.Indices.push_back(i*ringVertexCount + j);
meshData.Indices.push_back((i+1)*ringVertexCount + j+1);
meshData.Indices.push_back(i*ringVertexCount + j+1);
}
}
BuildCylinderTopCap(bottomRadius, topRadius,
height, sliceCount, stackCount, meshData);
BuildCylinderBottomCap(bottomRadius, topRadius,
height, sliceCount, stackCount, meshData);
}

6.11.1.2 帽幾何

生成帽幾何形狀相當於生成頂部和底部環的切片三角形以近似一個圓:

void GeometryGenerator::BuildCylinderTopCap(float bottomRadius,
float topRadius, float height, UINT sliceCount,
UINT stackCount, MeshData& meshData)
{
UINT baseIndex = (UINT)meshData.Vertices.size();
float y = 0.5f*height;
float dTheta = 2.0f*XM_PI/sliceCount;
// Duplicate cap ring vertices because the texture coordinates
// and normals differ.
for(UINT i = 0; i <= sliceCount; ++i)
{
float x = topRadius*cosf(i*dTheta);
float z = topRadius*sinf(i*dTheta);
// Scale down by the height to try and make top cap
// texture coord area proportional to base.
float u = x/height + 0.5f;
float v = z/height + 0.5f;
meshData.Vertices.push_back(
Vertex(x, y, z,
0.0f, 1.0f, 0.0f,
1.0f, 0.0f, 0.0f,
u, v));
}
// Cap center vertex.
meshData.Vertices.push_back(
Vertex(0.0f, y, 0.0f,
0.0f, 1.0f, 0.0f,
1.0f, 0.0f, 0.0f,
0.5f, 0.5f));
// Index of center vertex.
UINT centerIndex = (UINT)meshData.Vertices.size()-1;
for(UINT i = 0; i < sliceCount; ++i)
{
meshData.Indices.push_back(centerIndex);
meshData.Indices.push_back(baseIndex + i+1);
meshData.Indices.push_back(baseIndex + i);
}
}

底部的上限代碼是類似的。

6.11.2生成球形網格

我們通過指定半徑以及切片和堆棧數來定義一個球體,如圖6.15所示。用於生成球體的算法與圓柱體的算法非常相似,除了每個圓環的半徑改變是基於三角函數的非線性方式。我們將把它留給讀者來研究GeometryGenerator::CreateSphere代碼。

6-15
圖6.15 切片和堆棧的概念也適用於控制鑲嵌級別的球體。

6-16
圖6.16 通過重複細分並重投影到球體上來近似地球

6.11.3 生成地圈網格

從圖6.15中可以看出,球體的三角形沒有相同的面積。這在某些情況下可能不合需要。一個地圈用三角形近似一個球體,幾何面積相等,邊長相等(見圖6.16)。

爲了生成地圈,我們從二十面體開始,細分三角形,然後將新的頂點投影到具有給定半徑的球體上。 我們可以重複這個過程來改善曲面細分。

圖6.17顯示了一個三角形可以細分爲四個相等大小的三角形。只需沿原始三角形邊緣的中點找到新的頂點。然後通過將頂點投影到單位球上然後標量乘以r:V=rv||v|| 來將新頂點投影到半徑爲r的球體上。

6-17
圖6.17 將三角形細分爲四個面積相等的三角形。

代碼如下:

void GeometryGenerator::CreateGeosphere(float radius, UINT numSubdivisions, MeshData& meshData)
{
// Put a cap on the number of subdivisions.
numSubdivisions = MathHelper::Min(numSubdivisions, 5u);
// Approximate a sphere by tessellating an icosahedron.
const float X = 0.525731f;
const float Z = 0.850651f;
XMFLOAT3 pos[12] =
{
XMFLOAT3(-X, 0.0f, Z), XMFLOAT3(X, 0.0f, Z),
XMFLOAT3(-X, 0.0f, -Z), XMFLOAT3(X, 0.0f, -Z),
XMFLOAT3(0.0f, Z, X), XMFLOAT3(0.0f, Z, -X),
XMFLOAT3(0.0f, -Z, X), XMFLOAT3(0.0f, -Z, -X),
XMFLOAT3(Z, X, 0.0f), XMFLOAT3(-Z, X, 0.0f),
XMFLOAT3(Z, -X, 0.0f), XMFLOAT3(-Z, -X, 0.0f)
};
DWORD k[60] =
{
1,4,0, 4,9,0, 4,5,9, 8,5,4, 1,8,4,
1,10,8, 10,3,8, 8,3,5, 3,2,5, 3,7,2,
3,10,7, 10,6,7, 6,11,7, 6,0,11, 6,1,0,
10,1,6, 11,0,9, 2,11,9, 5,2,9, 11,2,7
};
meshData.Vertices.resize(12);
meshData.Indices.resize(60);
for(size_t i = 0; i < 12; ++i)
meshData.Vertices[i].Position = pos[i];
for(size_t i = 0; i < 60; ++i)
meshData.Indices[i] = k[i];
for(size_t i = 0; i < numSubdivisions; ++i)
Subdivide(meshData);
// Project vertices onto sphere and scale.
for(size_t i = 0; i < meshData.Vertices.size(); ++i)
{
// Project onto unit sphere.
XMVECTOR n = XMVector3Normalize(XMLoadFloat3(
&meshData.Vertices[i].Position));
// Project onto sphere.
XMVECTOR p = radius*n;
XMStoreFloat3(&meshData.Vertices[i].Position, p);
XMStoreFloat3(&meshData.Vertices[i].Normal, n);
// Derive texture coordinates from spherical coordinates.
float theta = MathHelper::AngleFromXY(
meshData.Vertices[i].Position.x,
meshData.Vertices[i].Position.z);
float phi = acosf(meshData.Vertices[i].Position.y / radius);
meshData.Vertices[i].TexC.x = theta/XM_2PI;
meshData.Vertices[i].TexC.y = phi/XM_PI;
// Partial derivative of P with respect to theta
meshData.Vertices[i].TangentU.x = -radius*sinf(phi)*sinf(theta);
meshData.Vertices[i].TangentU.y = 0.0f;
meshData.Vertices[i].TangentU.z = +radius*sinf(phi)*cosf(theta);
XMVECTOR T = XMLoadFloat3(&meshData.Vertices[i].TangentU);
XMStoreFloat3(&meshData.Vertices[i].TangentU,
XMVector3Normalize(T));
}
}

6.11.4 Demo代碼

在本節中,我們將回顧“形狀”演示和前兩個演示之間的主要區別。 忽略我們繪製不同的幾何圖形,主要區別是我們繪製多個對象。 每個物體都有一個世界矩陣,用於描述物體相對於世界空間的局部空間,它指定了物體在世界中的位置。 請注意,即使我們在本演示中繪製了多個球體和圓柱體,我們只需要一個球體和圓柱體幾何體的副本。 我們簡單地重繪同一個球體和圓柱體網格多次,但使用不同的世界矩陣; 回想一下這就是所謂的實例。 這些矩陣是在初始化時創建的,如下所示:

// Define transformations from local spaces to world space.
XMFLOAT4X4 mSphereWorld[10];
XMFLOAT4X4 mCylWorld[10];
XMFLOAT4X4 mBoxWorld;
XMFLOAT4X4 mGridWorld;
XMFLOAT4X4 mCenterSphere;
XMMATRIX I = XMMatrixIdentity();
XMStoreFloat4x4(&mGridWorld, I);
XMMATRIX boxScale = XMMatrixScaling(2.0f, 1.0f, 2.0f);
XMMATRIX boxOffset = XMMatrixTranslation(0.0f, 0.5f, 0.0f);
XMStoreFloat4x4(&mBoxWorld, XMMatrixMultiply(boxScale, boxOffset));
XMMATRIX centerSphereScale = XMMatrixScaling(2.0f, 2.0f, 2.0f);
XMMATRIX centerSphereOffset = XMMatrixTranslation(0.0f, 2.0f, 0.0f);
XMStoreFloat4x4(&mCenterSphere, XMMatrixMultiply(centerSphereScale, centerSphereOffset));
// We create 5 rows of 2 cylinders and spheres per row.
for(int i = 0; i < 5; ++i)
{
XMStoreFloat4x4(&mCylWorld[i*2+0],
XMMatrixTranslation(-5.0f, 1.5f, -10.0f + i*5.0f));
XMStoreFloat4x4(&mCylWorld[i*2+1],
XMMatrixTranslation(+5.0f, 1.5f, -10.0f + i*5.0f));
XMStoreFloat4x4(&mSphereWorld[i*2+0],
XMMatrixTranslation(-5.0f, 3.5f, -10.0f + i*5.0f));
XMStoreFloat4x4(&mSphereWorld[i*2+1],
XMMatrixTranslation(+5.0f, 3.5f, -10.0f + i*5.0f));
}

我們將所有網格頂點和索引打包到一個頂點和索引緩衝區中。 這是通過連接頂點和索引數組完成的。 這意味着當我們繪製一個對象時,我們只繪製了頂點和索引緩衝區的一個子集。 我們需要知道三個量才能繪製幾何圖形的一個子集(回憶圖6.3)。 我們需要知道連接索引緩衝區中每個對象的起始索引,並且我們需要知道每個對象的索引計數。 我們需要的第三件事是連接頂點緩衝區中每個對象的第一個頂點的索引偏移量。 這是因爲當我們連接頂點數組時,我們沒有抵消索引來補償(回想第5章練習2); 然而,如果我們將索引偏移到每個對象的第一個頂點,我們可以將它傳遞給ID3D11DeviceContext :: DrawIndexed的第三個參數,並且該偏移量將被添加到該繪製調用中的所有索引以爲我們執行偏移。

下面的代碼顯示瞭如何創建幾何緩衝區,如何緩存必要的繪圖數量以及如何繪製對象。

void ShapesApp::BuildGeometryBuffers()
{
GeometryGenerator::MeshData box;
GeometryGenerator::MeshData grid;
GeometryGenerator::MeshData sphere;
GeometryGenerator::MeshData cylinder;
GeometryGenerator geoGen;
geoGen.CreateBox(1.0f, 1.0f, 1.0f, box);
geoGen.CreateGrid(20.0f, 30.0f, 60, 40, grid);
geoGen.CreateSphere(0.5f, 20, 20, sphere);
geoGen.CreateCylinder(0.5f, 0.3f, 3.0f, 20, 20, cylinder);
// Cache the vertex offsets to each object in the concatenated
// vertex buffer.
mBoxVertexOffset = 0;
mGridVertexOffset = box.Vertices.size();
mSphereVertexOffset = mGridVertexOffset + grid.Vertices.size();
mCylinderVertexOffset = mSphereVertexOffset + sphere.Vertices.size();
// Cache the index count of each object.
mBoxIndexCount = box.Indices.size();
mGridIndexCount = grid.Indices.size();
mSphereIndexCount = sphere.Indices.size();
mCylinderIndexCount = cylinder.Indices.size();
// Cache the starting index for each object in the concatenated
// index buffer.
mBoxIndexOffset = 0;
mGridIndexOffset = mBoxIndexCount;
mSphereIndexOffset = mGridIndexOffset + mGridIndexCount;
mCylinderIndexOffset = mSphereIndexOffset + mSphereIndexCount;
UINT totalVertexCount =
box.Vertices.size() +
grid.Vertices.size() +
sphere.Vertices.size() +
cylinder.Vertices.size();
UINT totalIndexCount =
mBoxIndexCount +
mGridIndexCount +
mSphereIndexCount +
mCylinderIndexCount;
//
// Extract the vertex elements we are interested in and pack the
// vertices of all the meshes into one vertex buffer.
//
std::vector<Vertex> vertices(totalVertexCount);
XMFLOAT4 black(0.0f, 0.0f, 0.0f, 1.0f);
UINT k = 0;
for(size_t i = 0; i < box.Vertices.size(); ++i, ++k)
{
vertices[k].Pos = box.Vertices[i].Position;
vertices[k].Color = black;
}
for(size_t i = 0; i < grid.Vertices.size(); ++i, ++k)
{
vertices[k].Pos = grid.Vertices[i].Position;
vertices[k].Color = black;
}
for(size_t i = 0; i < sphere.Vertices.size(); ++i, ++k)
{
vertices[k].Pos = sphere.Vertices[i].Position;
vertices[k].Color = black;
}
for(size_t i = 0; i < cylinder.Vertices.size(); ++i, ++k)
{
vertices[k].Pos = cylinder.Vertices[i].Position;
vertices[k].Color = black;
}
D3D11_BUFFER_DESC vbd;
vbd.Usage = D3D11_USAGE_IMMUTABLE;
vbd.ByteWidth = sizeof(Vertex) * totalVertexCount;
vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER;
vbd.CPUAccessFlags = 0;
vbd.MiscFlags = 0;
D3D11_SUBRESOURCE_DATA vinitData;
vinitData.pSysMem = &vertices[0];
HR(md3dDevice->CreateBuffer(&vbd, &vinitData, &mVB));
//
// Pack the indices of all the meshes into one index buffer.
//
std::vector<UINT> indices;
indices.insert(indices.end(), box.Indices.begin(), box.Indices.end());
indices.insert(indices.end(), grid.Indices.begin(), grid.Indices.end());
indices.insert(indices.end(), sphere.Indices.begin(),
sphere.Indices.end());
indices.insert(indices.end(), cylinder.Indices.begin(),
cylinder.Indices.end());
D3D11_BUFFER_DESC ibd;
ibd.Usage = D3D11_USAGE_IMMUTABLE;
ibd.ByteWidth = sizeof(UINT) * totalIndexCount;
ibd.BindFlags = D3D11_BIND_INDEX_BUFFER;
ibd.CPUAccessFlags = 0;
ibd.MiscFlags = 0;
D3D11_SUBRESOURCE_DATA iinitData;
iinitData.pSysMem = &indices[0];
HR(md3dDevice->CreateBuffer(&ibd, &iinitData, &mIB));
}
void ShapesApp::DrawScene()
{
md3dImmediateContext->ClearRenderTargetView(mRenderTargetView,
reinterpret_cast<const float*>(&Colors::Blue));
md3dImmediateContext->ClearDepthStencilView(mDepthStencilView,
D3D11_CLEAR_DEPTH|D3D11_CLEAR_STENCIL, 1.0f, 0);
md3dImmediateContext->IASetInputLayout(mInputLayout);
md3dImmediateContext->IASetPrimitiveTopology(
D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
md3dImmediateContext->RSSetState(mWireframeRS);
UINT stride = sizeof(Vertex);
UINT offset = 0;
md3dImmediateContext->IASetVertexBuffers(0, 1, &mVB, &stride, &offset);
md3dImmediateContext->IASetIndexBuffer(mIB, DXGI_FORMAT_R32_UINT, 0);
// Set constants
XMMATRIX view = XMLoadFloat4x4(&mView);
XMMATRIX proj = XMLoadFloat4x4(&mProj);
XMMATRIX viewProj = view*proj;
D3DX11_TECHNIQUE_DESC techDesc;
mTech->GetDesc(&techDesc);
for(UINT p = 0; p < techDesc.Passes; ++p)
{
// Draw the grid.
XMMATRIX world = XMLoadFloat4x4(&mGridWorld);
mfxWorldViewProj->SetMatrix(
reinterpret_cast<float*>(&(world*viewProj)));
mTech->GetPassByIndex(p)->Apply(0, md3dImmediateContext);
md3dImmediateContext->DrawIndexed(
mGridIndexCount, mGridIndexOffset, mGridVertexOffset);
// Draw the box.
world = XMLoadFloat4x4(&mBoxWorld);
mfxWorldViewProj->SetMatrix(
reinterpret_cast<float*>(&(world*viewProj)));
mTech->GetPassByIndex(p)->Apply(0, md3dImmediateContext);
md3dImmediateContext->DrawIndexed(
mBoxIndexCount, mBoxIndexOffset, mBoxVertexOffset);
// Draw center sphere.
world = XMLoadFloat4x4(&mCenterSphere);
mfxWorldViewProj->SetMatrix(
reinterpret_cast<float*>(&(world*viewProj)));
mTech->GetPassByIndex(p)->Apply(0, md3dImmediateContext);
md3dImmediateContext->DrawIndexed(
mSphereIndexCount, mSphereIndexOffset, mSphereVertexOffset);
// Draw the cylinders.
for(int i = 0; i < 10; ++i)
{
world = XMLoadFloat4x4(&mCylWorld[i]);
mfxWorldViewProj->SetMatrix(
reinterpret_cast<float*>(&(world*viewProj)));
mTech->GetPassByIndex(p)->Apply(0, md3dImmediateContext);
md3dImmediateContext->DrawIndexed(mCylinderIndexCount,
mCylinderIndexOffset, mCylinderVertexOffset);
}
// Draw the spheres.
for(int i = 0; i < 10; ++i)
{
world = XMLoadFloat4x4(&mSphereWorld[i]);
mfxWorldViewProj->SetMatrix(
reinterpret_cast<float*>(&(world*viewProj)));
mTech->GetPassByIndex(p)->Apply(0, md3dImmediateContext);
md3dImmediateContext->DrawIndexed(mSphereIndexCount,
mSphereIndexOffset, mSphereVertexOffset);
}
}
HR(mSwapChain->Present(0, 0));
}

6.12 從文件加載幾何

雖然箱子,網格,球體和圓柱體可以滿足本書中的一些演示,但是一些演示將通過渲染更復雜的幾何體而受益。 稍後我們將介紹如何從流行的3D建模格式加載3D網格。 與此同時,我們已經將頭骨網格的幾何圖形(圖6.18)導出爲一個簡單的頂點列表(僅限位置和法向量)和索引。 我們可以簡單地使用標準C ++文件I / O從文件中讀取頂點和索引,並將它們複製到我們的頂點和索引緩衝區中。 文件格式是一個非常簡單的文本文件:

VertexCount: 31076
TriangleCount: 60339
VertexList (pos, normal)
{
0.592978 1.92413 -2.62486 0.572276 0.816877 0.0721907
0.571224 1.94331 -2.66948 0.572276 0.816877 0.0721907
0.609047 1.90942 -2.58578 0.572276 0.816877 0.0721907
…
}
TriangleList
{
0 1 2
3 4 5
6 7 8
…
}

6-18
圖6.18 “頭骨”演示的屏幕截圖

6.13 動態頂點緩衝器

到目前爲止,我們已經使用了在初始化時固定的靜態緩衝區。相反,動態緩衝區的內容通常是每幀更改。當我們需要動畫時,通常會使用動態緩衝區。例如,假設我們正在進行波浪模擬,並且我們求解解函數f(x,z,t)的波動方程。該函數表示時間t時xz平面中每個點處的波高。如果我們要使用這個函數來繪製波浪,我們可以像使用峯和谷一樣使用三角形網格網格,並將f(x,z,t)應用到每個網格點以獲得波峯高度網格點。因爲這個函數也依賴於時間t(即波面隨時間變化),所以我們需要在很短的時間內(例如每1/30秒)將這個函數重新應用到網格點上以獲得平滑的動畫。因此,我們需要一個動態頂點緩衝區,以便隨着時間的推移更新三角形網格頂點的高度。導致動態頂點緩衝區的另一種情況是具有複雜物理和碰撞檢測的粒子系統。在每一幀中,我們將在CPU上進行物理和碰撞檢測以找到粒子的新位置。由於粒子位置正在改變每一幀,我們需要一個動態頂點緩衝區來更新用於繪製每幀的粒子位置。

回想一下,爲了使緩衝區動態化,我們必須指定使用D3D11_USAGE_DYNAMIC; 另外,因爲我們將寫入緩衝區,所以我們需要CPU訪問標誌D3D11_CPU_ACCESS_WRITE:

D3D11_BUFFER_DESC vbd;
vbd.Usage = D3D11_USAGE_DYNAMIC;
vbd.ByteWidth = sizeof(Vertex) * mWaves.VertexCount();
vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER;
vbd.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
vbd.MiscFlags = 0;
HR(md3dDevice->CreateBuffer(&vbd, 0, &mWavesVB));

然後我們可以使用ID3D11DeviceContext::Map函數來獲取指向緩衝區內存塊開始的指針並寫入它:

HRESULT ID3D11DeviceContext::Map(
ID3D11Resource *pResource,
UINT Subresource,
D3D11_MAP MapType,
UINT MapFlags,
D3D11_MAPPED_SUBRESOURCE *pMappedResource);

1.pResource:指向我們想要讀取/寫入的資源的指針。 緩衝區是Direct3D 11資源的一種類型,但其他資源也可以使用此方法訪問,例如紋理資源。
2.Subresource:資源中包含的子資源的索引。 我們將會看到後面如何使用它; 我們的緩衝區沒有子資源,所以指定0。
3.MapType:通用標誌是以下之一:
D3D11_MAP_WRITE_DISCARD:指示硬件放棄緩衝區並返回一個指向新分配的緩衝區的指針; 當我們寫入新分配的緩衝區時,通過允許硬件從丟棄的緩衝區繼續渲染,可以防止硬件停頓。
D3D11_MAP_WRITE_NO_OVERWRITE:告訴我們只打算寫入緩衝區未初始化部分的硬件; 這也可以防止硬件停滯,因爲它允許它在我們寫入緩衝區的未初始化部分的同時繼續渲染先前寫入的幾何圖形。
D3D11_MAP_READ:與分段緩衝區一起使用,您需要將GPU緩衝區的副本讀入系統內存。
4.MapFlags:我們不使用的可選標誌,因此指定爲0; 有關詳細信息,請參閱SDK文檔。
5.pMappedResource:返回一個指向D3D11_MAPPED_SUBRESOURCE的指針,從中我們可以訪問資源數據進行讀寫。
D3D11_MAPPED_SUBRESOURCE結構定義如下:

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

1.pData:指向讀/寫資源的原始內存的指針。 您必須將其轉換爲資源存儲的適當數據格式。
2.RowPitch:資源中一行數據的字節大小。 例如,對於2D紋理,這是一行的字節大小。
3.DepthPitch:資源中一頁數據的字節大小。 例如,對於3D紋理,這是3D紋理的一個2D圖像子集的字節大小。

RowPitch和DepthPitch之間的區別在於2D和3D資源(可以考慮2D或3D陣列)。 對於基本上是一維數據數組的頂點/索引緩衝區,RowPitch和DepthPitch被賦予相同的值,並且它等於頂點/索引緩衝區的字節大小。

以下代碼顯示了我們如何更新“Waves”演示中的頂點緩衝區:

D3D11_MAPPED_SUBRESOURCE mappedData;
HR(md3dImmediateContext->Map(mWavesVB, 0,
D3D11_MAP_WRITE_DISCARD, 0, &mappedData));
Vertex* v = reinterpret_cast<Vertex*>(mappedData.pData);
for(UINT i = 0; i < mWaves.VertexCount(); ++i)
{
v[i].Pos = mWaves[i];
v[i].Color = XMFLOAT4(0.0f, 0.0f, 0.0f, 1.0f);
}
md3dImmediateContext->Unmap(mWavesVB, 0);

完成更新緩衝區後,必須調用ID3D11DeviceContext :: Unmap函數。

使用動態緩衝區時會有一些開銷,因爲新數據必須從CPU內存傳輸回GPU內存。 因此,靜態緩衝區應優先於動態緩衝區,只要提供靜態緩衝區即可。 最新版本的Direct3D引入了新功能,以減少對動態緩衝區的需求。 例如:
1.簡單的動畫可以在頂點着色器中完成。
2.通過渲染紋理或計算着色器,以及頂點紋理獲取功能,可以實現波浪模擬
就像之前描述的完全在GPU上運行的那樣。
3.幾何着色器提供了GPU創建或破壞原語的能力,這是通常需要在沒有幾何着色器的情況下在CPU上完成的任務。

索引緩衝區也可以是動態的。但是,在“Waves”演示中,三角形拓撲保持不變,只有頂點高度發生變化; 因此,只有頂點緩衝區需要是動態的。

本章的“Waves”演示使用一個動態頂點緩衝區來實現一個簡單的波浪模擬,就像本節開頭所描述的那樣。對於本書,我們並不關心波浪仿真的實際算法細節(參見[Lengyel02]),但更多的是用於說明動態緩衝區的過程:更新CPU上的仿真,然後相應地更新頂點緩衝區 使用Map / Unmap。

Note:在“Waves”演示中,我們以線框模式渲染波形; 這是因爲沒有照明,很難在固體填充模式下看到波浪運動。

我們再次提到,可以使用更高級的方法在GPU上實現此演示,例如渲染紋理功能或計算着色器以及頂點紋理獲取。由於我們尚未涉及這些主題,因此我們在CPU上執行波浪模擬並使用動態頂點緩衝區更新新頂點。

6.14 總結

  1. Direct3D中的頂點可以包含除空間位置以外的其他數據。爲了創建自定義頂點格式,我們首先定義一個結構來保存我們選擇的頂點數據。一旦我們定義了一個頂點結構,我們就通過定義一個輸入佈局描述來描述它到Direct3D–一個D3D11_INPUT_ELEMENT_DESC元素的數組,每個頂點組件一個。通過這個數組描述,我們使用ID3D11Device :: CreateInputLayout創建一個ID3D11InputLayout對象,並且我們可以通過ID3D11DeviceContext :: IASetInputLayout方法將輸入佈局綁定到IA階段。
    2.爲了使GPU訪問一個頂點/索引數組,它們需要放置在一個稱爲緩衝區的特殊資源結構中,該結構由ID3D11Buffer接口表示。存儲頂點的緩衝區稱爲頂點緩衝區,存儲索引的緩衝區稱爲索引緩衝區。 Direct3D緩衝區不僅存儲數據,還描述數據如何被訪問以及將如何綁定到渲染管道。通過填寫D3D11_BUFFER_DESC和D3D11_SUBRESOURCE_DATA實例並調用ID3D11Device :: CreateBuffer來創建緩衝區。使用ID3D11DeviceContext :: IASetVertexBuffers方法將頂點緩衝區綁定到IA階段,並使用ID3D11DeviceContext :: IASetIndexBuffer方法將索引緩衝區綁定到IA階段。非索引
    可以使用ID3D11DeviceContext :: Draw繪製幾何圖形,並且可以使用ID3D11DeviceContext :: DrawIndexed繪製索引幾何圖形。
    3.頂點着色器是一個用HLSL編寫的程序,在GPU上執行,它輸入一個頂點並輸出一個頂點。每個繪製的頂點都通過頂點着色器。這使程序員能夠在每個頂點基礎上進行專門的工作,以實現各種渲染效果。從頂點着色器輸出的值被傳遞到管道中的下一個階段。
    4.常量緩衝區只是數據塊,可以存儲C ++應用程序代碼和着色器程序可以訪問的不同變量。通過這種方式,C ++應用程序可以與着色器通信並更新着色器使用的常量緩衝區中的值;例如,C ++應用程序可以更改着色器使用的世界視圖投影矩陣。一般建議是根據您需要更新其內容的頻率創建常量緩衝區。分開常量緩衝區的動機是效率。當一個常量緩衝區被更新時,它的所有變量都必須被更新;因此,根據更新頻率對它們進行分組以最大限度地減少冗餘更新是高效的。
    5.像素着色器是用HLSL編寫的程序,在GPU上執行,它輸入插入的頂點數據並輸出一個顏色值。作爲硬件優化,像素片段可能會在流入像素着色器之前被流水線拒絕(例如,early-z拒絕)。像素着色器使程序員能夠以像素爲基礎進行專門的工作,以實現各種渲染效果。從像素着色器輸出的值將傳遞到管道中的下一個階段。
    6.渲染狀態是設備維護的狀態,它會影響幾何體的渲染方式。渲染狀態保持有效,直到更改爲止,並​​且當前值將應用於任何後續繪製操作的幾何圖形。所有渲染狀態都有初始默認狀態。 Direct3D將渲染狀態分爲三個狀態塊:光柵化器狀態(ID3D11RasterizerState),混合狀態(ID3D11BlendState)和深度/模板狀態(ID3D11DepthStencilState)。渲染狀態可以在C ++應用程序級或有效文件中創建和設置。
  2. Direct3D效果(ID3DX11Effect)封裝了至少一種渲染技術。渲染技術包含指定如何以特定方式渲染3D幾何體的代碼。渲染技術至少包含一個渲染過程。對於每個渲染過程,渲染幾何體。多遍技術需要多次渲染幾何才能達到所需的結果。每一遍包含頂點着色器,可選幾何着色器,可選鑲嵌着色器,像素着色器以及用於繪製該過程的幾何圖形的渲染狀態。頂點,幾何,鑲嵌和像素着色器可以訪問效果文件中定義的常量緩衝區中的變量以及紋理資源。我們可以使用編譯時間參數來使用效果框架根據給定的編譯時參數爲我們生成着色器變體。
    8.當緩衝區的內容需要在運行時間(例如,每一幀或每1/30秒)頻繁更新時,使用動態緩衝區。動態緩衝區必須使用D3D11_USAGE_DYNAMIC用法和
    D3D11_CPU_ACCESS_WRITE CPU訪問標誌。使用ID3D11DeviceContext :: Map和ID3D11DeviceContext :: Unmap方法更新緩衝區。

6.15 練習

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