DirectX12学习笔记(六)Drawing in Direct3D

目录

6.1 VERTICES AND INPUT LAYOUTS

6.2 VERTEX BUFFERS

6.3 INDICES AND INDEX BUFFERS

6.4 EXAMPLE VERTEX SHADER

6.4.1 Input Layout Description and Input Signature Linking

6.5 EXAMPLE PIXEL SHADER

6.6 CONSTANT BUFFERS

6.6.1 Creating Constant Buffers

6.6.2 Updating Constant Buffers

6.6.3 Upload Buffer Helper

6.6.4 Constant Buffer Descriptors

6.6.5 Root Signature and Descriptor Tables

6.7 Compiling Shaders

6.7.1 Offline Compilation

6.7.2 Generated Assembly

6.7.3 Using Visual Studio to Compile Shaders Offline

6.8 Rasterizer State

6.9 Pipeline State Object

6.10 Geometry Helper Structure

6.11 Box Demo

6.13 EXERCISES


本章回到关注Direct3D API接口和方法,来配置渲染管线、定义顶点和像素着色器以及向渲染管线提交几何图形以供绘制。

6.1 VERTICES AND INPUT LAYOUTS

要创建自定义顶点格式,我们首先创建一个结构来保存我们选择的顶点数据。例如下面举例说明了两种不同的顶点格式:一个由位置和颜色组成,另一个由位置、法向量和两组二维纹理坐标组成:

struct Vertex1
{
    XMFLOAT3 Pos;
    XMFLOAT4 Color;
};

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

一旦我们定义了一个顶点结构,我们需要为Direct3D提供顶点结构的描述,以便它知道如何处理每个组件。该描述以input layout description的形式提供给Direct3D,由D3D12_INPUT_LAYOUT_DESC结构表示:

typedef struct D3D12_INPUT_LAYOUT_DESC
{
    const D3D12_INPUT_ELEMENT_DESC *pInputElementDescs;
    UINT NumElements;
} D3D12_INPUT_LAYOUT_DESC;

input layout description就是D3D12_INPUT_ELEMENT_DESC元素的数组,以及数组中的元素数量。

D3D12_INPUT_ELEMENT_DESC数组中的每个元素描述并对应于顶点结构中的一个组件。因此,如果顶点结构有两个组件,那么相应的D3D12_INPUT_ELEMENT_DESC数组将有两个元素。D3D12_INPUT_ELEMENT_DESC结构定义为:

typedef struct D3D12_INPUT_ELEMENT_DESC
{
    LPCSTR SemanticName;
    UINT SemanticIndex;
    DXGI_FORMAT Format;
    UINT InputSlot;
    UINT AlignedByteOffset;
    D3D12_INPUT_CLASSIFICATION InputSlotClass;
    UINT InstanceDataStepRate;
} D3D12_INPUT_ELEMENT_DESC;
  1.  SemanticName:要与元素关联的字符串。这可以是任何有效的变量名。语义用于将顶点结构中的元素映射到顶点着色器输入签名中的元素。见下图。DirectX12学习笔记(六)Drawing in Direct3D_第1张图片
  2. SemanticIndex:附加到语义上的索引。例如上图,一个顶点结构可能有不止一组纹理坐标。因此,与其引入一个新的语义名称,不如只需在末尾附加一个索引来区分两个纹理坐标集。在着色器代码中没有指定索引的语义默认为索引0。例如上图中POSITION等价于POSITION0。
  3. Format:DXGI_FORMAT枚举类型的成员,为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      // 4 8-bit unsigned integer vector
  4. InputSlot:指定元素将来自的input slot索引。Direct3D支持16个input slot(0-15),通过它们来提供顶点数据。目前暂时我们将只使用输入槽0(所有顶点元素均来自同一输入槽)。

  5. AlignedByteOffset:以字节为单位,从指定的input slot的c++顶点结构开始到顶点组件开始的偏移量。例如,在下面的顶点结构中,元素Pos是0字节的偏移量,因为它的开始与顶点结构的开始相一致。元素Normal有12字节的偏移量,因为我们必须跳过Pos的字节才能到达Normal的开始。

    struct Vertex2
    {
        XMFLOAT3 Pos;    // 0-byte offset
        XMFLOAT3 Normal; // 12-byte offset
        XMFLOAT2 Tex0;   // 24-byte offset
        XMFLOAT2 Tex1;   // 32-byte offset
    };
  6. InputSlotClass:暂时指定D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA(龙书写的是D3D12_INPUT_PER_VERTEX_DATA)。另一个选项用于instancing。

  7. InstanceDataStepRate:现在指定0;其他值仅用于instancing。

对于前两个顶点结构示例Vertex1和Vertex2,相应的input layout description如下:

D3D12_INPUT_ELEMENT_DESC desc1[] =
{
    {"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0},
    {"COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 12, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0}
};

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

6.2 VERTEX BUFFERS

为了让GPU访问一个顶点数组,需要将它们放在一个称为缓冲区的GPU资源(ID3D12Resource)中。我们称存储顶点的缓冲区为vertex buffer。缓冲区是比纹理更简单的资源:它们不是多维的,也没有mipmaps、过滤器或多采样支持。在我们需要为GPU提供诸如顶点的数据元素数组时将使用缓冲区。

正如我们在4.3.8中所做的,我们通过填写描述缓冲区资源的D3D12_RESOURCE_DESC结构来创建ID3D12Resource对象,然后调用ID3D12Device::CreateCommittedResource方法创建资源。Direct3D 12提供了一个c++封装类CD3DX12_RESOURCE_DESC(继承自D3D12_RESOURCE_DESC),并提供了方便的构造函数和方法。它提供了以下方法来简化描述缓冲区的D3D12_RESOURCE_DESC的构造:

static inline CD3DX12_RESOURCE_DESC Buffer(
    UINT64 width,
    D3D12_RESOURCE_FLAGS flags = D3D12_RESOURCE_FLAG_NONE,
    UINT64 alignment = 0 )
{
    return CD3DX12_RESOURCE_DESC(
        D3D12_RESOURCE_DIMENSION_BUFFER,
        alignment, width, 1, 1, 1,
        DXGI_FORMAT_UNKNOWN, 1, 0,
        D3D12_TEXTURE_LAYOUT_ROW_MAJOR, flags );
}

缓冲区的宽度指缓冲区中的字节数。例如,如果缓冲区存储64个浮点数,那么宽度就是64*sizeof(float)。

CD3DX12_RESOURCE_DESC类还提供了更为方便的用于构造描述纹理资源和查询资源信息的D3D12_RESOURCE_DESC的方法:

  1. CD3DX12_RESOURCE_DESC::Tex1D
  2. CD3DX12_RESOURCE_DESC::Tex2D
  3. CD3DX12_RESOURCE_DESC::Tex3D

Direct3D 12中的所有资源都由ID3D12Resource接口表示。这与Direct3D 11形成了对比,Direct3D 11为各种资源提供了不同的接口,如ID3D11Buffer和ID3D11Texture2D。资源的类型由D3D12_RESOURCE_DESC::D3D12_RESOURCE_DIMENSION字段指定。例如,缓冲区的尺寸为D3D12_RESOURCE_DIMENSION_BUFFER,二维纹理的尺寸为D3D12_RESOURCE_DIMENSION_TEXTURE2D。  

对于静态的几何图形(每帧不改变的几何图形),我们将vertex buffer放在默认堆中(D3D12_HEAP_TYPE_DEFAULT)以获得最佳性能。一般来说游戏中的大多数几何体都是这样(如树,建筑,地形,角色)。vertex buffer初始化后,只有GPU需要从vertex buffer中读取来绘制几何图形,所以默认的堆是有意义的。既然CPU无法写入默认堆中的vertex buffer,那如何初始化我们的vertex buffer?

除了创建实际的vertex buffer资源外,我们还需要创建一个具有堆类型D3D12_HEAP_TYPE_UPLOAD的中间upload buffer资源。回顾4.3.8,当需要将数据从CPU复制到GPU内存时,我们将资源提交到upload heap。创建upload buffer之后,我们将顶点数据从系统内存复制到upload buffer,然后将顶点数据从upload buffer复制到实际的vertex buffer。

因为需要一个中间upload buffer来初始化默认缓冲区(堆类型为D3D12_HEAP_TYPE_DEFAULT的缓冲区)的数据,所以我们在d3dUtil.h/.cpp中构建了以下实用函数,以避免每次需要默认缓冲区时重复这项工作:

Microsoft::WRL::ComPtr d3dUtil::CreateDefaultBuffer(
    ID3D12Device* device,
    ID3D12GraphicsCommandList* cmdList,
    const void* initData,
    UINT64 byteSize,
    Microsoft::WRL::ComPtr& uploadBuffer)
{
    ComPtr defaultBuffer;

    // Create the actual default buffer resource.
    ThrowIfFailed(device->CreateCommittedResource(
        &CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT),
        D3D12_HEAP_FLAG_NONE,
        &CD3DX12_RESOURCE_DESC::Buffer(byteSize),
        D3D12_RESOURCE_STATE_COMMON,
        nullptr,
        IID_PPV_ARGS(defaultBuffer.GetAddressOf())));

    // In order to copy CPU memory data into our default buffer, we need
    // to create an intermediate upload heap.
    ThrowIfFailed(device->CreateCommittedResource(
        &CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
        D3D12_HEAP_FLAG_NONE,
        &CD3DX12_RESOURCE_DESC::Buffer(byteSize),
        D3D12_RESOURCE_STATE_GENERIC_READ,
        nullptr,
        IID_PPV_ARGS(uploadBuffer.GetAddressOf())));

    // Describe the data we want to copy into the default buffer.
    D3D12_SUBRESOURCE_DATA subResourceData = {};
    subResourceData.pData = initData;
    subResourceData.RowPitch = byteSize;
    subResourceData.SlicePitch = subResourceData.RowPitch;
    
    // Schedule to copy the data to the default buffer resource.
    // At a high level, the helper function UpdateSubresources
    // will copy the CPU memory into the intermediate upload heap.
    // Then, using ID3D12CommandList::CopySubresourceRegion,
    // the intermediate upload heap data will be copied to mBuffer.
    cmdList->ResourceBarrier(1,
        &CD3DX12_RESOURCE_BARRIER::Transition(defaultBuffer.Get(),
        D3D12_RESOURCE_STATE_COMMON,
        D3D12_RESOURCE_STATE_COPY_DEST));
    UpdateSubresources<1>(cmdList,
        defaultBuffer.Get(), uploadBuffer.Get(),
        0, 0, 1, &subResourceData);
    cmdList->ResourceBarrier(1,
        &CD3DX12_RESOURCE_BARRIER::Transition(defaultBuffer.Get(),
        D3D12_RESOURCE_STATE_COPY_DEST,
        D3D12_RESOURCE_STATE_GENERIC_READ));

    // Note: uploadBuffer has to be kept alive after the above function
    // calls because the command list has not been executed yet that
    // performs the actual copy.
    // The caller can Release the uploadBuffer after it knows the copy
    // has been executed.
    return defaultBuffer;
}

D3D12_SUBRESOURCE_DATA结构体定义如下:

typedef struct D3D12_SUBRESOURCE_DATA
{
    const void *pData;
    LONG_PTR RowPitch;
    LONG_PTR SlicePitch;
} D3D12_SUBRESOURCE_DATA;
  1. pData:指向系统内存数组的指针,其中包含要初始化缓冲区的数据。如果缓冲区可以存储n个顶点,那么系统数组必须包含至少n个顶点,才能使整个缓冲区可以初始化。
  2. RowPitch:对于缓冲区,我们以字节为单位复制的数据的大小。
  3. SlicePitch:对于缓冲区,我们以字节为单位复制的数据的大小。

下面的代码展示了如何使用这个类来创建一个默认的缓冲区,该缓冲区存储一个立方体的8个顶点,每个顶点都有不同的颜色与之关联:

Vertex vertices[] =
{
    { XMFLOAT3(-1.0f, -1.0f, -1.0f), XMFLOAT4(Colors::White) },
    { XMFLOAT3(-1.0f, +1.0f, -1.0f), XMFLOAT4(Colors::Black) },
    { XMFLOAT3(+1.0f, +1.0f, -1.0f), XMFLOAT4(Colors::Red) },
    { XMFLOAT3(+1.0f, -1.0f, -1.0f), XMFLOAT4(Colors::Green) },
    { XMFLOAT3(-1.0f, -1.0f, +1.0f), XMFLOAT4(Colors::Blue) },
    { XMFLOAT3(-1.0f, +1.0f, +1.0f), XMFLOAT4(Colors::Yellow) },
    { XMFLOAT3(+1.0f, +1.0f, +1.0f), XMFLOAT4(Colors::Cyan) },
    { XMFLOAT3(+1.0f, -1.0f, +1.0f), XMFLOAT4(Colors::Magenta) }
};

const UINT64 vbByteSize = 8 * sizeof(Vertex);

ComPtr VertexBufferGPU = nullptr;
ComPtr VertexBufferUploader = nullptr;
VertexBufferGPU = d3dUtil::CreateDefaultBuffer(md3dDevice.Get(),
    mCommandList.Get(), vertices, vbByteSize, VertexBufferUploader);

Vertex类型和颜色被定义如下 :

struct Vertex
{
    XMFLOAT3 Pos;
    XMFLOAT4 Color;
};

为了将vertex buffer绑定到管线,我们需要创建vertex buffer view到vertex buffer资源。与RTV(render target view)不同,我们不需要vertex buffer view的descriptor heap。vertex buffer view由D3D12_VERTEX_BUFFER_VIEW_DESC结构表示:

typedef struct D3D12_VERTEX_BUFFER_VIEW
{
    D3D12_GPU_VIRTUAL_ADDRESS BufferLocation;
    UINT SizeInBytes;
    UINT StrideInBytes;
} D3D12_VERTEX_BUFFER_VIEW;
  1. BufferLocation:要创建view的vertex buffer资源的虚拟地址。我们可以使用ID3D12Resource::GetGPUVirtualAddress方法来获取这个。
  2. SizeInBytes:从BufferLocation开始的vertex buffer中要查看的字节数。
  3. StrideInBytes:以字节为单位的每个顶点元素的大小。

创建vertex buffer并创建view之后,可以将其绑定到管线的input slot,将顶点提供给管道的input assembler阶段。这可以通过以下方法实现:

void ID3D12GraphicsCommandList::IASetVertexBuffers(
    UINT StartSlot,
    UINT NumBuffers,
    const D3D12_VERTEX_BUFFER_VIEW *pViews);
  1. StartSlot:开始绑定vertex buffer的input slot。从0-15索引有16个input slot。
  2. NumBuffers:我们绑定到input slot的vertex buffer的数量。
  3. pViews:指向vertex buffer view数组的第一个元素的指针。

例子如下:

D3D12_VERTEX_BUFFER_VIEW vbv;
vbv.BufferLocation = VertexBufferGPU->GetGPUVirtualAddress();
vbv.StrideInBytes = sizeof(Vertex);
vbv.SizeInBytes = 8 * sizeof(Vertex);

D3D12_VERTEX_BUFFER_VIEW vertexBuffers[1] = { vbv };
mCommandList->IASetVertexBuffers(0, 1, vertexBuffers);

IASetVertexBuffers方法可能看起来有点复杂,因为它支持将vertex buffer数组设置为各种input slot。但是,目前我们将只使用一个input slot。

vertex buffer将一直绑定到input slot直到更改它。所以如果你使用一个以上的顶点缓冲区,你可以这样组织你的代码:

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

D3D12_VERTEX_BUFFER_VIEW_DESC mVBView1; // view to mVB1
D3D12_VERTEX_BUFFER_VIEW_DESC mVBView2; // view to mVB2

/*…Create the vertex buffers and views…*/

mCommandList->IASetVertexBuffers(0, 1, &VBView1);
/* …draw objects using vertex buffer 1… */

mCommandList->IASetVertexBuffers(0, 1, &mVBView2);
/* …draw objects using vertex buffer 2… */

将vertex buffer设置为input slot不会绘制它们,它只让顶点准备好被送入管道。实际绘制顶点的最后一步是使用ID3D12GraphicsCommandList:: DrawInstanced方法:

void ID3D12CommandList::DrawInstanced(
    UINT VertexCountPerInstance,
    UINT InstanceCount,
    UINT StartVertexLocation,
    UINT StartInstanceLocation);
  1. VertexCountPerInstance:绘制的顶点数(每个实例)。
  2. InstanceCount:用于instancing。现在将这个设置为1,因为我们只绘制一个实例。
  3. StartVertexLocation:指定vertex buffer中开始绘图的第一个顶点的索引(从零开始)。
  4. StartInstanceLocation:用于instancing。目前设为0。

其中两个参数VertexCountPerInstance和StartVertexLocation在顶点缓冲区中定义要绘制的顶点的连续子集:

DirectX12学习笔记(六)Drawing in Direct3D_第2张图片

DrawInstanced方法没有指定顶点定义的图元类型。参考5.5.2,图元拓扑状态是用ID3D12GraphicsCommandList::IASetPrimitiveTopology方法设置的。下面是一个例子:

cmdList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST

6.3 INDICES AND INDEX BUFFERS

与顶点类似,为了让GPU访问一个索引数组,它们需要被放置在一个GPU资源(ID3D12Resource)buffer中。我们称存储索引的缓冲区为index buffer。因为我们的d3dUtil::CreateDefaultBuffer函数通过void*处理通用数据,所以我们可以使用这个相同的函数来创建一个index buffer(或任何默认缓冲区)。

为了将index buffer绑定到管线,我们需要创建index buffer view到index buffer资源。与vertex buffer view一样,我们不需要index buffer view的descriptor heap。index buffer view由D3D12_INDEX_BUFFER_VIEW结构表示:

typedef struct D3D12_INDEX_BUFFER_VIEW
{
    D3D12_GPU_VIRTUAL_ADDRESS BufferLocation;
    UINT SizeInBytes;
    DXGI_FORMAT Format;
} D3D12_INDEX_BUFFER_VIEW;
  1. BufferLocation:要创建view的index buffer资源的虚拟地址。我们可以使用ID3D12Resource::GetGPUVirtualAddress方法来获得它。
  2. SizeInBytes:从BufferLocation开始的index buffer查看的字节数。
  3. Format:索引的格式必须是16位索引的DXGI_FORMAT_R16_UINT或32位索引的DXGI_FORMAT_R32_UINT。应该使用16位索引来减少内存和带宽,只有当索引值需要额外的32位范围时才使用32位索引。

与vertex buffer和其他Direct3D资源一样,在使用它之前我们需要将它绑定到管线。使用ID3D12CommandList::SetIndexBuffer方法将index buffer绑定到输入汇编程序阶段。下面的代码演示了如何创建一个定义立方体三角形的index buffer,创建一个view,并将其绑定到管线:

std::uint16_t 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
};

const UINT ibByteSize = 36 * sizeof(std::uint16_t);

ComPtr IndexBufferGPU = nullptr;
ComPtr IndexBufferUploader = nullptr;
IndexBufferGPU = d3dUtil::CreateDefaultBuffer(md3dDevice.Get(),
    mCommandList.Get(), indices, ibByteSize, IndexBufferUploader);

D3D12_INDEX_BUFFER_VIEW ibv;
ibv.BufferLocation = IndexBufferGPU->GetGPUVirtualAddress();
ibv.Format = DXGI_FORMAT_R16_UINT;
ibv.SizeInBytes = ibByteSize;

mCommandList->IASetIndexBuffer(&ibv);

在使用索引时,我们必须使用ID3D12GraphicsCommandList:: DrawIndexedInstanced 方法,而不是DrawInstanced:

void ID3D12GraphicsCommandList::DrawIndexedInstanced(
    UINT IndexCountPerInstance,
    UINT InstanceCount,
    UINT StartIndexLocation,
    INT BaseVertexLocation,
    UINT StartInstanceLocation);
  1. IndexCountPerInstance:绘制的索引数量(每个实例)。
  2. InstanceCount:instancing,当前设置为1。
  3. StartIndexLocation:指向index buffer中的一个元素的索引,标记开始读取索引的起点。
  4. BaseVertexLocation:vertex buffer中开始绘图的第一个顶点的索引。
  5. StartInstanceLocation:instancing,当前设置为0。

假设我们有三个对象:球、盒子和圆柱体,每个对象都有自己的vertex buffer和index buffer,每个局部index buffer.中的索引都有对应的局部顶点缓冲区。假设将球、盒子和圆柱体的顶点和索引连接到一个全局vertex buffer和index buffer中,如下图所示。(因为在更改顶点和索引缓冲区时需要一些API开销,可以连接他们的vertex buffer和index buffer。一般这不会成为瓶颈,但如果有可以很容易合并的许多小的vertex buffer和index buffer,那么出于性能考虑这样做是值得的。)在连接之后,索引不再是正确的了,因为它们存储的索引位置相对于它们相应的局部vertex buffer,而不是全局的buffer,因此索引需要重新计算。

DirectX12学习笔记(六)Drawing in Direct3D_第3张图片

通常,通过将对象的基本顶点位置添加到每个索引中来计算对象的新索引。不必自己计算新的索引,我们可以让Direct3D通过将基本顶点位置传递给DrawIndexedInstanced的第四个参数来完成。

我们可以通过以下三个调用逐一绘制球、盒子和圆柱体

mCmdList->DrawIndexedInstanced(numSphereIndices, 1, 0, 0, 0);
mCmdList->DrawIndexedInstanced(numBoxIndices, 1, firstBoxIndex, firstBoxVertexPos,0);
mCmdList->DrawIndexedInstanced(numCylIndices, 1, firstCylIndex, firstCylVertexPos,0);

 总结一下vertex buffer和index buffer的步骤:

  • 声明描述
  1. 声明Verter类型的变量,Beffur函数获取CD3DX12_RESOURCE_DESC实例。
  2. 声明uint16数组indices,Beffur函数获取CD3DX12_RESOURCE_DESC实例。
  • 使用上述的两个信息利用CreateCommittedResource创建vertex/index buffer资源和upload buffer资源并且提交到对应的堆,CPU利用upload buffer来更新,然后拷贝到实际的vertex/index buffer上。
  • IASetVertexBuffers创建了view,绑定到管线的input slot,并且将顶点和索引提供给管道的input assembler阶段。
  • 用DrawInstanced或DrawIndexedInstanced开始绘制。

GPU不直接绑定资源,而是用过descriptor(view)对象来引用,而descriptor会放在我们创建的descriptor heap中,所以创建render target view,depth/stencil buffer view的函数都需要从descriptor heap中获取descriptor句柄参数。IASetVertexBuffers创建了view并且直接把资源绑定到管线,所以不需要有descriptor heap。

6.4 EXAMPLE VERTEX SHADER

下面是一个简单顶点着色器的实现(参见5.6):

cbuffer cbPerObject : register(b0)
{
    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++,因此很容易学习。随着本书的深入,我们将引入任何新的HLSL概念,以便实现手边的演示。着色器通常编写在基于文本的文件中,扩展名为.hlsl。

顶点着色器是被调用的函数VS,你可以给顶点着色器任何有效的函数名。这个顶点着色器有四个参数:前两个是输入参数,后两个是输出参数(由out关键字表示)。HLSL没有引用或指针,因此要从一个函数返回多个值,您需要使用结构或out参数。在HLSL中,函数总是内联的。

前两个输入参数形成了顶点着色器的输入签名,并对应于我们绘制时使用的自定义顶点结构中的数据成员。使用参数语义“:POSITION”和“:COLOR”将顶点结构中的元素映射到顶点着色器输入参数,如下图。

DirectX12学习笔记(六)Drawing in Direct3D_第4张图片

暂时不写HLSL。

6.4.1 Input Layout Description and Input Signature Linking

6.5 EXAMPLE PIXEL SHADER

6.6 CONSTANT BUFFERS

6.6.1 Creating Constant Buffers

constant buffer是GPU资源(ID3D12Resource)的一个例子,它的数据内容可以在着色程序中引用,纹理和其他类型的缓冲资源也可以在着色程序中引用。6.4中的顶点着色器示例代码有如下一段:

cbuffer cbPerObject : register(b0)
{
    float4x4 gWorldViewProj;
};

这段代码引用了一个名为cbPerObject的cbuffer对象(constant buffer),在这个示例中,constant buffer存储一个4×4的矩阵gWorldViewProj。

与vertex buffer和index buffer不同,constant buffer通常被CPU每帧更新一次。比如如果摄像机正在移动每一帧,那么constant buffer将需要在每帧更新新的视图矩阵。因此,我们在upload堆中创建constant buffer而不是默认堆中,以便我们可以从CPU更新内容。

constant buffer还有一个特殊的硬件要求,它们的大小必须是硬件最小分配大小(256字节)的倍数。

我们经常需要多个相同类型的constant buffer。例如,上面的constant buffer cbPerObject存储每个对象都不同的常量,所以如果我们有n个对象,那么我们将需要n个这种类型的constant buffer。下面的代码显示了我们如何创建一个缓冲区,存储NumElements个constant buffer:

struct ObjectConstants
{
    DirectX::XMFLOAT4X4 WorldViewProj = MathHelper::Identity4x4();
};

UINT elementByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof(ObjectConstants));

ComPtr mUploadCBuffer;
device->CreateCommittedResource(
    &CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
    D3D12_HEAP_FLAG_NONE,
    &CD3DX12_RESOURCE_DESC::Buffer(mElementByteSize * NumElements),
    D3D12_RESOURCE_STATE_GENERIC_READ,
    nullptr,
    IID_PPV_ARGS(&mUploadCBuffer));

我们可以将mUploadCBuffer看作是存储ObjectConstants类型的constant buffer数组(使用填充使其成为256字节的倍数)。当需要绘制对象时,我们只需将constant buffer view(CBV)绑定到存储该对象常量的buffer的子区域。注意,我们通常将缓冲区mUploadCBuffer称为constant buffer,因为它存储了一个constant buffer数组。

功能函数d3dUtil::CalcConstantBufferByteSize做的算法是将缓冲区的字节大小四舍五入为最小硬件分配大小(256字节)的倍数:

UINT d3dUtil::CalcConstantBufferByteSize(UINT
byteSize)
{
    // Constant buffers must be a multiple of the minimum hardware
    // allocation size (usually 256 bytes). So round up to nearest
    // multiple of 256. We do this by adding 255 and then masking off
    // the lower 2 bytes which store all bits < 256.
    // Example: Suppose byteSize = 300.
    // (300 + 255) & ˜255
    // 555 & ˜255
    // 0x022B & ˜0x00ff
    // 0x022B & 0xff00
    // 0x0200
    // 512
    return (byteSize + 255) & ˜255;
}

 即使我们以256的倍数分配常数数据,也不需要显式地填充HLSL结构中相应的常数数据,因为它是隐式完成的:

// Implicitly padded to 256 bytes.
cbuffer cbPerObject : register(b0)
{
    float4x4 gWorldViewProj;
};

// Explicitly padded to 256 bytes.
cbuffer cbPerObject : register(b0)
{
    float4x4 gWorldViewProj;
    float4x4 Pad0;
    float4x4 Pad1;
    float4x4 Pad1;
};

为了避免将constant buffer元素舍入为256字节的倍数,可以显式地将所有constant buffer结构填充为256字节的倍数。 

 Direct3D 12引入了着色器model 5.1。着色器model 5.1引入了另一种HLSL语法来定义一个常量缓冲区,如下图所示:

struct ObjectConstants
{
    float4x4 gWorldViewProj;
    uint matIndex;
};
ConstantBuffer gObjConstants : register(b0);

 然后使用数据成员的语法在着色器中访问constant buffer的字段

uint index = gObjConstants.matIndex;

6.6.2 Updating Constant Buffers

因为我们使用了D3D12_HEAP_TYPE_UPLOAD创建了一个constant buffer,所以我们可以将数据从CPU上传到constant buffer资源。要做到这一点,我们首先必须获得一个指向资源数据的指针,这可以通过Map方法实现:

ComPtr mUploadBuffer;
BYTE* mMappedData = nullptr;
mUploadBuffer->Map(0, nullptr, reinterpret_cast(&mMappedData));
  1.  第一个参数是一个子资源索引,它标识要映射的子资源。对于一个缓冲区,唯一的子资源就是缓冲区本身,所以我们把它设为0。
  2. 第二个参数是一个可选指针,指向描述要映射的内存范围的D3D12_RANGE结构。指定null映射整个资源。
  3. 第三个参数返回一个指向映射数据的指针。

为了将数据从系统内存复制到constant buffer,我们可以做一个memcpy:

memcpy(mMappedData, &data, dataSizeInBytes);

 当我们完成了一个constant buffer,我们应该在释放内存前Unmap:

if(mUploadBuffer != nullptr)
    mUploadBuffer->Unmap(0, nullptr);

mMappedData = nullptr;
  1. 第一个参数是一个子资源索引,它标识要映射的子资源,对于一个缓冲区来说,它是0。
  2. 第二个参数是一个可选的指针,指向描述要Unmap的内存范围的D3D12_RANGE结构。指定null将取消对整个资源的映射。

6.6.3 Upload Buffer Helper

在upload buffer周围构建一个轻量级包装类非常方便。我们在UploadBuffer.h中定义了以下类,以便更容易地使用upload缓冲区。它为我们处理upload buffer资源的构造和销毁,处理资源的mapping和unmapping,并提供CopyData方法来更新缓冲区中的特定元素。我们使用CopyData方法当需要改变CPU上传缓冲区的内容时(如当view矩阵改变时)。注意这个类可以用于任何upload buffer,不一定是constant buffer。但是如果我们确实将它用于constant buffer,则需要通过isConstantBuffer构造函数参数说明这一点。如果它存储一个constant buffer,那么它将自动填充内存,使每个constant buffer为256字节的倍数。

template class UploadBuffer
{
public:
    UploadBuffer(ID3D12Device* device, UINT elementCount, bool isConstantBuffer) :
    mIsConstantBuffer(isConstantBuffer)
    {
        mElementByteSize = sizeof(T);

        // Constant buffer elements need to be multiples of 256 bytes.
        // This is because the hardware can only view constant data
        // at m*256 byte offsets and of n*256 byte lengths.
        // typedef struct D3D12_CONSTANT_BUFFER_VIEW_DESC {
        // UINT64 OffsetInBytes; // multiple of 256
        // UINT SizeInBytes; // multiple of 256
        // } D3D12_CONSTANT_BUFFER_VIEW_DESC;
        if(isConstantBuffer)
            mElementByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof(T));

        ThrowIfFailed(device->CreateCommittedResource(
            &CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
            D3D12_HEAP_FLAG_NONE,
            &CD3DX12_RESOURCE_DESC::Buffer(mElementByteSize*elementCount),
            D3D12_RESOURCE_STATE_GENERIC_READ,
            nullptr,
            IID_PPV_ARGS(&mUploadBuffer)));

        ThrowIfFailed(mUploadBuffer->Map(0, nullptr, 
            reinterpret_cast(&mMappedData)));

        // We do not need to unmap until we are done with the resource.
        // However, we must not write to the resource while it is in use by
        // the GPU (so we must use synchronization techniques).
    }

    UploadBuffer(const UploadBuffer& rhs) = delete;
    UploadBuffer& operator=(const UploadBuffer& rhs) = delete;
    ˜UploadBuffer()
    {
        if(mUploadBuffer != nullptr)
            mUploadBuffer->Unmap(0, nullptr);

        mMappedData = nullptr;
        }

    ID3D12Resource* Resource()const
    {
        return mUploadBuffer.Get();
    }

    void CopyData(int elementIndex, const T& data)
    {
        memcpy(&mMappedData[elementIndex*mElementByteSize], &data, sizeof(T));
    }

private:
    Microsoft::WRL::ComPtr mUploadBuffer;
    BYTE* mMappedData = nullptr;
    UINT mElementByteSize = 0;
    bool mIsConstantBuffer = false;
};

 在本章的演示中,我们允许用户用鼠标旋转和移动相机,并在Update函数的每一帧中使用新的视图矩阵更新合并的world-view-projection矩阵:

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 (x - mLastMousePos.x));
        float dy = XMConvertToRadians(0.25f*static_cast (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(x - mLastMousePos.x);
        float dy = 0.005f*static_cast(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::Update(const GameTimer& gt)
{
    // 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 view = XMMatrixLookAtLH(pos, target, up);
    XMStoreFloat4x4(&mView, view);

    XMMATRIX world = XMLoadFloat4x4(&mWorld);
    XMMATRIX proj = XMLoadFloat4x4(&mProj);
    XMMATRIX worldViewProj = world*view*proj;

    // Update the constant buffer with the latest
    worldViewProj matrix.
    ObjectConstants objConstants;
    XMStoreFloat4x4(&objConstants.WorldViewProj, 
        XMMatrixTranspose(mObjectCB->CopyData(0, objConstants));
}

6.6.4 Constant Buffer Descriptors

在4.1.6中,我们通过descriptor对象将资源绑定到渲染管线。到目前为止我们已经使用descriptors/views来实现render targets,depth/stencil buffers,vertex buffers和index buffers。我们还需要descriptor来将constant buffer绑定到管线。constant buffer descriptor位于D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV类型的descriptor heap中。这样的堆可以混合存储constant buffer、着色器资源和无序访问资源三者的descriptor。为了存储这些新的descriptor,我们需要创建一个新的descriptor heap

D3D12_DESCRIPTOR_HEAP_DESC cbvHeapDesc;
cbvHeapDesc.NumDescriptors = 1;
cbvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
cbvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
cbvHeapDesc.NodeMask = 0;

ComPtr mCbvHeap;
md3dDevice->CreateDescriptorHeap(&cbvHeapDesc, IID_PPV_ARGS(&mCbvHeap));

和之前的descriptor heap区别是我们指定了D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE标志来指示着色程序将访问这些descriptor。

通过填写D3D12_CONSTANT_BUFFER_VIEW_DESC实例并调用ID3D12Device::CreateConstantBufferView创建constant buffer view:

// Constant data per-object.
struct ObjectConstants
{
    XMFLOAT4X4 WorldViewProj = MathHelper::Identity4x4();
};

// Constant buffer to store the constants of n object.
std::unique_ptr> mObjectCB = nullptr;
mObjectCB = std::make_unique>(md3dDevice.Get(), n, true);

UINT objCBByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof(ObjectConstants));

// Address to start of the buffer (0th constant buffer).
D3D12_GPU_VIRTUAL_ADDRESS cbAddress = mObjectCB->Resource()->GetGPUVirtualAddress();

// Offset to the ith object constant buffer in the buffer.
int boxCBufIndex = i;
cbAddress += boxCBufIndex*objCBByteSize;

D3D12_CONSTANT_BUFFER_VIEW_DESC cbvDesc;
cbvDesc.BufferLocation = cbAddress;
cbvDesc.SizeInBytes = d3dUtil::CalcConstantBufferByteSize(sizeof(ObjectConstants));

md3dDevice->CreateConstantBufferView(
    &cbvDesc,
    mCbvHeap->GetCPUDescriptorHandleForHeapStart());

D3D12_CONSTANT_BUFFER_VIEW_DESC结构描述了要绑定到HLSL constant buffer结构的constant buffer资源的子集。如前所述,通常一个constant buffer为n个对象存储每个对象常量的数组,但是我们可以通过使用BufferLocation和SizeInBytes获得第i个对象常量数据的view。由于硬件需求,D3D12_CONSTANT_BUFFER_VIEW_DESC::SizeInBytes和D3D12_CONSTANT_BUFFER_VIEW_DESC::OffsetInBytes成员必须是256字节的倍数。例如,如果您指定64,那么您将得到以下调试错误:

D3D12 ERROR: ID3D12Device::CreateConstantBufferView: SizeInBytes of 64 is invalid. Device requires SizeInBytes be a multiple of 256.

D3D12 ERROR: ID3D12Device:: CreateConstantBufferView: OffsetInBytes of 64 is invalid. Device requires OffsetInBytes be a multiple of 256.

6.6.5 Root Signature and Descriptor Tables

通常,在执行draw调用之前,不同的着色器程序会期望将不同的资源绑定到渲染管线。资源被绑定到特定的寄存器槽,在那里它们可以被着色程序访问。例如前面的顶点和像素着色器只期望一个constant buffer被绑定到寄存器b0。我们在本书后面使用一些更高级的顶点和像素着色器,几个常量缓冲区,纹理和采样器绑定到不同的寄存器槽:

// Texture resource bound to texture register slot 0.
Texture2D gDiffuseMap : register(t0);

// Sampler resources bound to sampler register slots 0-5.
SamplerState gsamPointWrap : register(s0);
SamplerState gsamPointClamp : register(s1);
SamplerState gsamLinearWrap : register(s2);
SamplerState gsamLinearClamp : register(s3);
SamplerState gsamAnisotropicWrap : register(s4);
SamplerState gsamAnisotropicClamp : register(s5);

// cbuffer resource bound to cbuffer register slots 0-2
cbuffer cbPerObject : register(b0)
{
    float4x4 gWorld;
    float4x4 gTexTransform;
};

// Constant data that varies per material.
cbuffer cbPass : register(b1)
{
    float4x4 gView;
    float4x4 gProj;
    […] // Other fields omitted for brevity.
};

cbuffer cbMaterial : register(b2)
{
    float4 gDiffuseAlbedo;
    float3 gFresnelR0;
    float gRoughness;
    float4x4 gMatTransform;
};

root signature定义了在执行draw调用之前,应用程序将绑定到渲染管线的资源,以及将这些资源映射到着色器输入寄存器的位置。root signature必须与它将与之一起使用的着色器兼容(在执行draw调用之前,root signature必须提供着色器期望绑定到渲染管线的所有资源)。这将在pipeline state object(PSO)创建时进行验证(6.9)。不同的draw call可能使用不同的着色程序集,这将需要不同的root signature。

如果我们认为着色器程序是一个函数,着色器期望的输入资源是函数参数,那么root signature可以被认为是定义了一个函数签名(因此命名为root signature)。通过绑定不同的资源作为参数,着色器输出将会不同。例如一个顶点着色器将取决于实际的顶点被输入到着色器,以及绑定的资源。

root signature在Direct3D中由ID3D12RootSignature接口表示。它是由一组根参数定义的,这些根参数描述了一次draw call着色器期望的资源。根参数可以是根常量、根descriptor或descriptors table。我们将在下一章讨论根常量和根descriptor;在本章中,我们将只使用descriptors table。descriptors table指定descriptors heap中连续的descriptors范围。

下面的代码创建一个root signature,它有个根参数是一个descriptors table,大到足以存储一个CBV(constant buffer view):

// Root parameter can be a table, root descriptor or root constants.
CD3DX12_ROOT_PARAMETER slotRootParameter[1];

// Create a single descriptor table of CBVs.
CD3DX12_DESCRIPTOR_RANGE cbvTable;
cbvTable.Init(
    D3D12_DESCRIPTOR_RANGE_TYPE_CBV,
    1, // Number of descriptors in table
    0);// base shader register arguments are bound to for this root parameter

slotRootParameter[0].InitAsDescriptorTable(
    1, // Number of ranges
    &cbvTable); // Pointer to array of ranges

// A root signature is an array of root parameters.
CD3DX12_ROOT_SIGNATURE_DESC rootSigDesc(1,
    slotRootParameter, 0, nullptr,
    D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);

// create a root signature with a single slot which points to a
// descriptor range consisting of a single constant buffer.
ComPtr serializedRootSig = nullptr;
ComPtr errorBlob = nullptr;
HRESULT hr = D3D12SerializeRootSignature(&rootSigDesc,
    D3D_ROOT_SIGNATURE_VERSION_1,
    serializedRootSig.GetAddressOf(),
    errorBlob.GetAddressOf());

ThrowIfFailed(md3dDevice->CreateRootSignature(
    0,
    serializedRootSig->GetBufferPointer(),
    serializedRootSig->GetBufferSize(),
    IID_PPV_ARGS(&mRootSignature)));

我们将在下一章详细描述CD3DX12_ROOT_PARAMETER和CD3DX12_DESCRIPTOR_RANGE,但是现在只需要理解这些代码:

CD3DX12_ROOT_PARAMETER slotRootParameter[1];

CD3DX12_DESCRIPTOR_RANGE cbvTable;
cbvTable.Init(
    D3D12_DESCRIPTOR_RANGE_TYPE_CBV, // table type
    1, // Number of descriptors in table
    0);// base shader register arguments are bound to for this root parameter

slotRootParameter[0].InitAsDescriptorTable(
    1, // Number of ranges
    &cbvTable); // Pointer to array of ranges

 创建一个根参数,该参数期望将一个CBV的descriptor table绑定到constant buffer寄存器0(在HLSL代码中的register(b0))。

root signature仅定义应用程序将绑定到渲染管线的资源,它实际上不做任何资源绑定。一旦使用命令列表设置了根签名,我们使用ID3D12GraphicsCommandList::SetGraphicsRootDescriptorTable将描述符表绑定到管道:

void ID3D12GraphicsCommandList::SetGraphicsRootDescriptorTable(
    UINT RootParameterIndex,
    D3D12_GPU_DESCRIPTOR_HANDLE BaseDescriptor);
  1. RootParameterIndex:正在设置的根参数索引。
  2. BaseDescriptor:堆中指定表中第一个descriptor的句柄。例如如果root signature指定该表有5个descriptor,那么堆中的BaseDescriptor和后面的4个descriptor将被设置为这个根表。

下面的代码将root signature和CBV堆设置到命令列表,并设置descriptor table标识要绑定到管道的资源:

mCommandList->SetGraphicsRootSignature(mRootSignature.Get());
ID3D12DescriptorHeap* descriptorHeaps[] = { mCbvHeap.Get() };
mCommandList->SetDescriptorHeaps(_countof(descriptorHeaps), descriptorHeaps);

// Offset the CBV we want to use for this draw call.
CD3DX12_GPU_DESCRIPTOR_HANDLE cbv(mCbvHeap->GetGPUDescriptorHandleForHeapStart());
cbv.Offset(cbvIndex, mCbvSrvUavDescriptorSize);

mCommandList->SetGraphicsRootDescriptorTable(0, cbv);

为了提高性能,尽可能使root signature小一些,并尽量减少在每帧中更改root signature的次数。

应用程序绑定的root signature(descriptor tables、根常量和根descriptor)的内容在draw/dispatch调用之间更改任何内容时,D3D12驱动程序会自动进行版本控制。因此,每个draw/dispatch都获得一组惟一的root signature状态。

 更改根签名会丢失所有现有绑定。也就是说,需要将所有资源重新绑定到新根签名期望的管线中。

6.7 Compiling Shaders

在Direct3D中,着色程序必须首先编译成可移植的字节码。然后图形驱动程序将这个字节码再次编译成GPU最理想的本地指令 [ATI1]。在运行时我们可以用以下函数编译一个着色器:

HRESULT D3DCompileFromFile(
    LPCWSTR pFileName,
    const D3D_SHADER_MACRO *pDefines,
    ID3DInclude *pInclude,
    LPCSTR pEntrypoint,
    LPCSTR pTarget,
    UINT Flags1,
    UINT Flags2,
    ID3DBlob **ppCode,
    ID3DBlob **ppErrorMsgs);
  1. pFileName:要编译的HLSL源代码,. HLSL文件的名称。
  2. pDefines:高级选项,参见SDK文档。在这本书中我们总是指定null。
  3. pInclude:高级选项,参见SDK文档。在这本书中我们总是指定null。
  4. pEntrypoint: 着色器入口点的函数名。一个.hlsl可以包含多个着色器程序(例如一个顶点着色器和一个像素着色器),所以我们需要指定要编译的特定着色器入口点。
  5. pTarget:指定着色器程序类型和版本的字符串。在本书中,我们的目标版本是5.0和5.1。
    a) vs_5_0 and vs_5_1: Vertex shader 5.0 and 5.1。
    b) hs_5_0 and hs_5_1: Hull shader 5.0 and 5.1。
    c) ds_5_0 and ds_5_1: Domain shader 5.0 and 5.1。
    d) gs_5_0 and gs_5_1: Geometry shader 5.0 and 5.1。
    e) ps_5_0 and ps_5_1: Pixel shader 5.0 and 5.1。
    f) cs_5_0 and cs_5_1: Compute shader 5.0 and 5.1。
  6. Flags1:标记指定应该如何编译着色器代码。SDK文档中列出了很多这样的标记,但是在本书中我们只使用了两个:
    a) D3DCOMPILE_DEBUG: 在调试模式下编译着色器。
    b) D3DCOMPILE_SKIP_OPTIMIZATION:指示编译器跳过
  7. Flags2:高级选项,参见SDK文档。
  8. ppCode:返回指向ID3DBlob数据结构的指针,该结构存储编译后着色器对象的字节码。
  9. ppErrorMsgs:返回指向ID3DBlob数据结构的指针,该结构存储一个包含编译错误的字符串,如果有的话。

ID3DBlob类型只是一个普通的内存块,它有两个方法:

  1. LPVOID GetBufferPointer:返回一个void*数据,在使用之前必须将其转换为适当的类型(参见下面的示例)。
  2. SIZE_T GetBufferSize:返回缓冲区的字节大小。

为了支持错误输出,我们在d3dutier .h/.cpp中实现以下函数来在运行时编译着色器:

ComPtr d3dUtil::CompileShader(
    const std::wstring& filename,
    const D3D_SHADER_MACRO* defines,
    const std::string& entrypoint,
    const std::string& target)
{
    // Use debug flags in debug mode.
    UINT compileFlags = 0;
#if defined(DEBUG) || defined(_DEBUG)
    compileFlags = D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION;
#endif

    HRESULT hr = S_OK;
    ComPtr byteCode = nullptr;
    ComPtr errors;
    hr = D3DCompileFromFile(filename.c_str(), defines,
        D3D_COMPILE_STANDARD_FILE_INCLUDE,
        entrypoint.c_str(), target.c_str(), compileFlags,
        0, &byteCode, &errors);

    // Output errors to debug window.
    if(errors != nullptr)
        OutputDebugStringA((char*)errors->GetBufferPointer());

    ThrowIfFailed(hr);

    return byteCode;
}

下面是调用它的一个例子:

ComPtr mvsByteCode = nullptr;
ComPtr mpsByteCode = nullptr;
mvsByteCode = d3dUtil::CompileShader(L”Shaders\color.hlsl”, nullptr, “VS”, “vs_5_0”);
mpsByteCode = d3dUtil::CompileShader(L”Shaders\color.hlsl”, nullptr, “PS”, “ps_5_0”);

HLSL的错误和警告会通过ppErrorMsgs参数返回。比如如果我们拼错了mul函数,那么我们会得到下面的错误输出:

Shaders\color.hlsl(29,14-55): error X3004: undeclared identifier ‘mu’

并非一定要将shader资源绑定到渲染管线才能编译一个着色器,我们将在6.9中看到如何做到这一点。

6.7.1 Offline Compilation

与在运行时编译着色器不同,我们可以在单独的步骤(例如一个构建步骤,或作为资产内容管线过程的一部分)中离线编译它们。这样做的理由如下:

  1. 对于复杂的着色器,编译可能需要很长时间。因此,离线编译将使加载速度更快。
  2. 在构建过程比运行时看到着色器编译错误更早更方便。
  3. Windows 8商店应用程序必须使用离线编译。

编译的着色器通常使用.cso(compiled shader object)扩展。

为了离线编译着色器,我们使用了DirectX自带的FXC工具。这是一个命令行工具。分别编译在color.hlsl中以VS和PS为入口点的顶点和像素着色器,我们可以这样写调试版本:

fxc "color.hlsl" /Od /Zi /T vs_5_0 /E "VS" /Fo "color_vs.cso" /Fc "color_vs.asm"
fxc "color.hlsl" /Od /Zi /T ps_5_0 /E "PS" /Fo "color_ps.cso" /Fc "color_ps.asm"

 发布版本:

fxc "color.hlsl" /T vs_5_0 /E "VS" /Fo "color_vs.cso" /Fc "color_vs.asm"
fxc "color.hlsl" /T ps_5_0 /E "PS" /Fo "color_ps.cso" /Fc "color_ps.asm"

 DirectX12学习笔记(六)Drawing in Direct3D_第5张图片

如果你试图编译一个带有语法错误的着色器,FXC将输出错误/警告到命令窗口。例如如果我们在color.hlsl中错误地命名了一个变量:

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

 我们得到了从这一个错误而来的相当多的错误(顶部的错误是要修复的关键):

color.hlsl(29,42-54): error X3004: undeclared identifier ‘worldViewProj’
color.hlsl(29,14-55): error X3013: ‘mul’: no matching 2 parameter intrinsic function
color.hlsl(29,14-55): error X3013: Possible intrinsic functions are: 
color.hlsl(29,14-55): error X3013: mul(float|half…

在编译时获取错误消息要比在运行时方便得多。

我们已经展示了如何离线编译顶点和像素着色器到.cso文件。因此我们不再需要在运行时做这件事(我们不需要调用D3DCompileFromFile)。但是我们仍然需要从.cso文件加载编译后的shader对象字节码到我们的应用程序中。

ComPtr d3dUtil::LoadBinary(const std::wstring& filename)
{
    std::ifstream fin(filename, std::ios::binary);

    fin.seekg(0, std::ios_base::end);
    std::ifstream::pos_type size = (int)fin.tellg();
    fin.seekg(0, std::ios_base::beg);

    ComPtr blob;
    ThrowIfFailed(D3DCreateBlob(size,
        blob.GetAddressOf()));

    fin.read((char*)blob->GetBufferPointer(), size);
    fin.close();

    return blob;
}
…
ComPtr mvsByteCode = d3dUtil::LoadBinary(L”Shaders\color_vs.cso”);
ComPtr mpsByteCode = d3dUtil::LoadBinary(L”Shaders\color_ps.cso”);

6.7.2 Generated Assembly

FXC的/Fc可选参数生成可移植的汇编代码。不时查看着色器的程序集对于检查着色器指令数量和查看生成的代码类型很有用——有时可能与您期望的很不同。例如如果您的HLSL代码中有一个条件语句,那么您可能希望在汇编代码中有一个分支指令。在可编程gpu的早期,着色器中的分支过去是很昂贵的,因此有时编译器会通过计算两个分支来简化一个条件语句,然后在两个分支之间进行内插来选择正确的答案。下面的代码会给出相同结果:

DirectX12学习笔记(六)Drawing in Direct3D_第6张图片

因此这被展开的方法在没有任何分支的情况下给出了相同的结果,但是如果不查看汇编代码我们就不知道是否展开了还是生成了真正的分支指令。重点是有时你想看看程序集它到底发生了什么。下面是为顶点着色器生成的程序集的一个例子:

//
// Generated by Microsoft (R) HLSL Shader Compiler 6.4.9844.0
//
//
// 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 | skipOptimization
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 2
//
// Initial variable locations:
// v0.x <- vin.PosL.x; v0.y <- vin.PosL.y; v0.z <- vin.PosL.z;
// v1.x <- vin.Color.x; v1.y <- vin.Color.y; v1.z <- vin.Color.z; v1.w <- vin.Color.w;
// o1.x <- .Color.x;
// o1.y <- .Color.y;
// o1.z <- .Color.z;
// o1.w <- .Color.w;
// o0.x <- .PosH.x;
// o0.y <- .PosH.y;
// o0.z <- .PosH.z;
// o0.w <- .PosH.w
//
#line 29 “color.hlsl”
mov r0.xyz, v0.xyzx
mov r0.w, l(1.000000)
dp4 r1.x, r0.xyzw, cb0[0].xyzw // r1.x <- vout.PosH.x
dp4 r1.y, r0.xyzw, cb0[1].xyzw // r1.y <- vout.PosH.y
dp4 r1.z, r0.xyzw, cb0[2].xyzw // r1.z <- vout.PosH.z
dp4 r1.w, r0.xyzw, cb0[3].xyzw // r1.w <- vout.PosH.w

#line 32
mov r0.xyzw, v1.xyzw // r0.x <- vout.Color.x; r0.y <- vout.Color.y;
                     // r0.z <- vout.Color.z; r0.w <- vout.Color.w
mov o0.xyzw, r1.xyzw
mov o1.xyzw, r0.xyzw
ret
// Approximately 10 instruction slots used

6.7.3 Using Visual Studio to Compile Shaders Offline

Visual Studio 2013为编译着色器程序提供了一些集成支持。您可以将.hlsl文件添加到项目中,Visual Studio (VS)将识别它们并提供编译选项(见下图)。这些选项为FXC参数提供了一个UI。当您将HLSL文件添加到VS项目时,它将成为构建过程的一部分,并且着色器将使用FXC编译。

DirectX12学习笔记(六)Drawing in Direct3D_第7张图片

使用VS集成HLSL支持的一个缺点是每个文件只支持一个着色器程序,你不能在一个文件中同时存储顶点和像素着色器。此外,有时我们希望用不同的预处理器指令编译相同的着色器程序,以获得不同的着色器变体,同样集成VS也不支持,因为它是每个.hlsl输出一个.cso。

6.8 Rasterizer State

虽然渲染管道的许多部分是可编程的,但有些部分是只能配置的。光栅化状态组由D3D12_RASTERIZER_DESC结构表示,用于配置渲染管道的光栅化阶段:

typedef struct D3D12_RASTERIZER_DESC {
    D3D12_FILL_MODE FillMode; // Default: D3D12_FILL_SOLID
    D3D12_CULL_MODE CullMode; // Default: D3D12_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
    UINT ForcedSampleCount; // Default: 0

    // Default: D3D12_CONSERVATIVE_RASTERIZATION_MODE_OFF
    D3D12_CONSERVATIVE_RASTERIZATION_MODE ConservativeRaster;
} D3D12_RASTERIZER_DESC;

这些成员大多数是高级特性或不经常使用。建议参考SDK文档了解每个成员的描述。这里只描述四个:

  1. FillMode:指定D3D12_FILL_WIREFRAME为线框渲染,指定D3D12_FILL_SOLID为实体渲染。默认是实体渲染。
  2. CullMode:指定D3D12_CULL_NONE来禁用剔除,D3D12_CULL_BACK来剔除背面的三角形,或D3D12_CULL_FRONT来剔除正面的三角形。默认情况下背面三角形会被剔除。
  3. FrontCounterClockwise:如果您想要将顺时针方向(相对于相机)的三角形视为正面,指定为false。如果您想要将逆时针方向(相对于相机)的三角形视为正面,指定为true。默认情况下,此状态为false。
  4. ScissorEnable:指定true来启用剪刀测试(4.3.10),指定false禁用它。默认为false。

下面的代码展示了如何创建一个光栅化状态,打开线框图模式,并禁用背面剔除:

CD3DX12_RASTERIZER_DESC rsDesc(D3D12_DEFAULT);
rsDesc.FillMode = D3D12_FILL_WIREFRAME;
rsDesc.CullMode = D3D12_CULL_NONE;

CD3DX12_RASTERIZER_DESC是一个方便的类,它扩展了D3D12_RASTERIZER_DESC并添加了一些辅助构造函数。特别是有一个接受类型为CD3D12_DEFAULT的对象的构造函数,这只是一个用于重载的虚拟类型,用于指示光栅化状态成员应该初始化为默认值。CD3D12_DEFAULT和D3D12_DEFAULT的定义如下:

struct CD3D12_DEFAULT {};
extern const DECLSPEC_SELECTANY CD3D12_DEFAULT D3D12_DEFAULT;

 D3D12_DEFAULT用于Direct3D的几个便利类中。

6.9 Pipeline State Object

例如我们已经展示了如何描述input layout description,如何创建顶点和像素着色器,以及如何配置光栅化状态组。但是,我们还没有展示如何将这些对象绑定到图形管线以供实际使用。大多数控制图形管线状态的对象被指定为pipeline state object(PSO)的聚合,它由ID3D12PipelineState接口表示。为了创建一个PSO,我们首先通过填写一个D3D12_GRAPHICS_PIPELINE_STATE_DESC实例来描述它:

typedef struct D3D12_GRAPHICS_PIPELINE_STATE_DESC
{ 
    ID3D12RootSignature *pRootSignature;
    D3D12_SHADER_BYTECODE VS;
    D3D12_SHADER_BYTECODE PS;
    D3D12_SHADER_BYTECODE DS;
    D3D12_SHADER_BYTECODE HS;
    D3D12_SHADER_BYTECODE GS;
    D3D12_STREAM_OUTPUT_DESC StreamOutput;
    D3D12_BLEND_DESC BlendState;
    UINT SampleMask;
    D3D12_RASTERIZER_DESC RasterizerState;
    D3D12_DEPTH_STENCIL_DESC DepthStencilState;
    D3D12_INPUT_LAYOUT_DESC InputLayout;
    D3D12_PRIMITIVE_TOPOLOGY_TYPE PrimitiveTopologyType;
    UINT NumRenderTargets;
    DXGI_FORMAT RTVFormats[8];
    DXGI_FORMAT DSVFormat;
    DXGI_SAMPLE_DESC SampleDesc;
} D3D12_GRAPHICS_PIPELINE_STATE_DESC;
  1. pRootSignature: 指向与此PSO绑定的root signature的指针。root signature必须与PSO指定的着色器兼容。
  2. VS:要绑定的顶点着色器。这是由D3D12_SHADER_BYTECODE结构指定的,该结构是指向编译后的字节数据的指针,字节数据的大小以字节为单位。
    typedef struct D3D12_SHADER_BYTECODE {
        const BYTE *pShaderBytecode;
        SIZE_T BytecodeLength;
    } D3D12_SHADER_BYTECODE;
  3. PS:要绑定的pixel着色器。
  4. DS:要绑定的domain着色器。
  5. HS:要绑定的hull着色器。
  6. GS:要绑定的geometry着色器。
  7. StreamOutput:stream-out的高级技术。我们现在只是把这个域归零。
  8. BlendState:指定配置混合的混合状态。当前指定默认的CD3DX12_BLEND_DESC(D3D12_DEFAULT)。
  9. SampleMask:多采样最多可采集32个采样。这个32位整数值用于启用/禁用采样。例如,如果您关闭了第5位,那么第5个样本将不会被取走。当然,禁用第5个采样只有在实际使用至少5个采样的多采样时才会有效果。如果应用程序使用单采样,那么只有该参数的第一个部分起作用。通常使用默认的0xffffffff,它不会禁用任何采样。
  10. RasterizerState:指定配置光栅化程序的光栅化状态。
  11. DepthStencilState:指定配置depth/stencil test的depth/stencil状态。我们将在后面的章节中讨论这个状态组;现在指定默认的CD3DX12_DEPTH_STENCIL_DESC(D3D12_DEFAULT)。
  12. InputLayout:指定input layout description,它是D3D12_INPUT_ELEMENT_DESC元素的数组,包含数组中元素的数量信息。
    typedef struct D3D12_INPUT_LAYOUT_DESC
    {
        const D3D12_INPUT_ELEMENT_DESC *pInputElementDescs;
        UINT NumElements;
    } D3D12_INPUT_LAYOUT_DESC;
  13. PrimitiveTopologyType:指定图元拓扑类型。
    typedef enum D3D12_PRIMITIVE_TOPOLOGY_TYPE {
        D3D12_PRIMITIVE_TOPOLOGY_TYPE_UNDEFINED = 0,
        D3D12_PRIMITIVE_TOPOLOGY_TYPE_POINT = 1,
        D3D12_PRIMITIVE_TOPOLOGY_TYPE_LINE = 2,
        D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE = 3,
        D3D12_PRIMITIVE_TOPOLOGY_TYPE_PATCH = 4
    } D3D12_PRIMITIVE_TOPOLOGY_TYPE;
  14. NumRenderTargets:同时使用的target render数量。
  15. RTVFormats:render target的格式。支持同时写入多个render target的数组。应该与我们使用PSO的渲染目标的设置相匹配。
  16. DSVFormat:depth/stencil buffer的格式。应该与我们使用PSO的depth/stencil buffer的设置相匹配。
  17. SampleDesc:描述多重采样的数量和质量等级。应该与我们正在使用的render target的设置相匹配。

171/5000  
在填写了D3D12_GRAPHICS_PIPELINE_STATE_DESC实例之后,我们使用ID3D12Device::CreateGraphicsPipelineState方法创建一个ID3D12PipelineState对象:

ComPtr mRootSignature;
std::vector mInputLayout;
ComPtr mvsByteCode;
ComPtr mpsByteCode;
… 
D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc;
ZeroMemory(&psoDesc, sizeof(D3D12_GRAPHICS_PIPELINE_STATE_DESC));
psoDesc.InputLayout = { mInputLayout.data(), (UINT)mInputLayout.size() };
psoDesc.pRootSignature = mRootSignature.Get();
psoDesc.VS =
{
    reinterpret_cast(mvsByteCode->GetBufferPointer()),
    mvsByteCode->GetBufferSize()
};
psoDesc.PS =
{
    reinterpret_cast(mpsByteCode->GetBufferPointer()),
    mpsByteCode->GetBufferSize()
};
psoDesc.RasterizerState = CD3D12_RASTERIZER_DESC(D3D12_DEFAULT);
psoDesc.BlendState = CD3D12_BLEND_DESC(D3D12_DEFAULT);
psoDesc.DepthStencilState = CD3D12_DEPTH_STENCIL_DESC(D3D12_DEFAULT);
psoDesc.SampleMask = UINT_MAX;
psoDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
psoDesc.NumRenderTargets = 1;
psoDesc.RTVFormats[0] = mBackBufferFormat;
psoDesc.SampleDesc.Count = m4xMsaaState ? 4 : 1;
psoDesc.SampleDesc.Quality = m4xMsaaState ? (m4xMsaaQuality - 1) : 0;
psoDesc.DSVFormat = mDepthStencilFormat;

ComPtr mPSO;
md3dDevice->CreateGraphicsPipelineState(&psoDesc, IID_PPV_ARGS(&mPSO)));

相当多的状态汇聚在一个ID3D12PipelineState对象。为了提高性能我们将所有这些对象指定为图形管线的集合。通过将它们指定为集合,Direct3D可以验证所有状态是否兼容,并且驱动程序可以预先生成所有代码来编写硬件状态。在Direct3D 11状态模型中,这些渲染状态块是分别设置的。然而这些状态是相关的,如果一个状态改变了,可能需要驱动程序为另一个依赖它的状态重新编写硬件程序。由于要更改许多状态来配置管线,所以硬件状态会进行冗余重编程。为了避免这种冗余,驱动程序通常会延迟对硬件状态的编程,直到发出一个draw call,此时整个管道状态都是已知的。但是这种延迟需要驱动程序在运行时进行额外的记帐工作,它需要跟踪哪些状态已经更改,然后生成在运行时对硬件状态进行编程的代码。在新的Direct3D 12模型中,驱动程序可以在初始化时生成编写管道状态所需的所有代码,因为我们将大多数管道状态指定为一个集合。

由于PSO的验证和创建非常耗时,所以应该在初始化时生成PSO。例外情况可能是在运行时根据需要在第一次引用PSO时创建PSO,然后将其存储在hash表之类的集合中,以便能够快速获取以供将来使用。

并不是所有的渲染状态都封装在PSO中。像viewport和scissor矩形这样的状态是独立于PSO指定的。这些状态可以有效地独立于其他管道状态进行设置,所以将它们包含在PSO中没有任何优势。

// Reset specifies initial PSO.
mCommandList->Reset(mDirectCmdListAlloc.Get(), mPSO1.Get())
/* …draw objects using PSO 1… */

// Change PSO
mCommandList->SetPipelineState(mPSO2.Get());
/* …draw objects using PSO 2… */

// Change PSO
mCommandList->SetPipelineState(mPSO3.Get());
/* …draw objects using PSO 3… */

换句话说,当一个PSO被绑定到命令列表后,直到覆盖它(或者重置命令列表)前它都不会改变。

为了提高性能,PSO应该保持最小状态的变化。将所有可以使用相同PSO的对象聚集在一起。不要每一次draw call就改变PSO!

6.10 Geometry Helper Structure

创建一个将vertex buffer和index buffer分组在一起以定义一组几何图形的结构是很有帮助的。这个结构可以保持顶点和索引数据的系统内存支持,以便CPU可以读取这些数据。CPU会需要访问几何数据来做诸如拾取和碰撞检测之类的事情。此外,该结构缓存vertex buffer和index buffer的重要属性(如格式和步长),并提供将views返回给缓冲区的方法。我们在整本书中每当我们定义一个几何块,将使用MeshGeometry(在d3dUtil.h中定义)结构。

// Defines a subrange of geometry in a MeshGeometry. This is for when
// multiple geometries are stored in one vertex and index buffer. It
// provides the offsets and data needed to draw a subset of geometry
// stores in the vertex and index buffers so that we can implement the
// technique described by Figure 6.3.
struct SubmeshGeometry
{
    UINT IndexCount = 0;
    UINT StartIndexLocation = 0;
    INT BaseVertexLocation = 0;

    // Bounding box of the geometry defined by this submesh.
    // This is used in later chapters of the book.
    DirectX::BoundingBox Bounds;
};

struct MeshGeometry
{
    // Give it a name so we can look it up by name.
    std::string Name;

    // System memory copies. Use Blobs because the vertex/index format can
    // be generic.
    // It is up to the client to cast appropriately.
    Microsoft::WRL::ComPtr VertexBufferCPU = nullptr;
    Microsoft::WRL::ComPtr IndexBufferCPU = nullptr;

    Microsoft::WRL::ComPtr VertexBufferGPU = nullptr;
    Microsoft::WRL::ComPtr IndexBufferGPU = nullptr;

    Microsoft::WRL::ComPtr VertexBufferUploader = nullptr;
    Microsoft::WRL::ComPtr IndexBufferUploader = nullptr;

    // Data about the buffers.
    UINT VertexByteStride = 0;
    UINT VertexBufferByteSize = 0;
    DXGI_FORMAT IndexFormat = DXGI_FORMAT_R16_UINT;
    UINT IndexBufferByteSize = 0;

    // A MeshGeometry may store multiple geometries in one vertex/index
    // buffer.
    // Use this container to define the Submesh geometries so we can draw
    // the Submeshes individually.
    std::unordered_map DrawArgs;

    D3D12_VERTEX_BUFFER_VIEW VertexBufferView()const
    {
        D3D12_VERTEX_BUFFER_VIEW vbv;
        vbv.BufferLocation = VertexBufferGPU->GetGPUVirtualAddress();
        vbv.StrideInBytes = VertexByteStride;
        vbv.SizeInBytes = VertexBufferByteSize;

        return vbv;
    }

    D3D12_INDEX_BUFFER_VIEW IndexBufferView()const
    {
        D3D12_INDEX_BUFFER_VIEW ibv;
        ibv.BufferLocation = IndexBufferGPU->GetGPUVirtualAddress();
        ibv.Format = IndexFormat;
        ibv.SizeInBytes = IndexBufferByteSize;

        return ibv;
    }

    // We can free this memory after we finish upload to the GPU.
    void DisposeUploaders()
    {
        VertexBufferUploader = nullptr;
        IndexBufferUploader = nullptr;
    }
};

6.11 Box Demo

详见官方案例。 

6.13 EXERCISES

第2题:使用两个vertex buffers(和两个输入槽)来为管道提供顶点,一个用于存储位置元素,另一个用于存储颜色元素。为此将使用两个顶点结构来存储分割数据:

struct VPosData
{
    XMFLOAT3 Pos;
};

struct VColorData
{
    XMFLOAT4 Color;
};

D3D12_INPUT_ELEMENT_DESC数组变为这样:

D3D12_INPUT_ELEMENT_DESC vertexDesc[] =
{
    {“POSITION”, 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_PER_VERTEX_DATA, 0},
    {“COLOR”, 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 0, D3D12_INPUT_PER_VERTEX_DATA, 0}
};

position元素连接到输入槽0,color元素连接到输入槽1,并因此D3D12_INPUT_ELEMENT_DESC::AlignedByteOffset两个元素都为0。然后使用ID3D12CommandList::IASetVertexBuffers将两个顶点缓冲区绑定到槽0和槽1上。Direct3D使用来自不同输入槽的元素来组装顶点来优化,需要较少的vertex buffers使用少的输入槽,从而节省数据带宽。为了提高性能,建议将输入槽的数量最小化到小于或等于3的小数量。

对于题目一种办法是可以建多个MeshGeometry分别存不同的vertex buffer,绘制时IASetVertexBuffers把不同的vertex buffer绑定到不同的输入槽。

 

你可能感兴趣的:(DirectX12)