Skip to content

绘制

Note

写在前面:

对于后续提到的数据,尽管c++支持在类体中直接初始化,但直接在类体初始化会导致阅读和管理困难,且DX12项目中的初始化往往依赖D3D12提供的接口,在极端情况下可能存在相关接口还没完成初始化的时序问题

因此,在通常实践中,应该将初始化语法写在具体的方法内,并统一调用。譬如,下文提到的顶点与输入布局描述就可以在BuildPSO方法中初始化。

Note

对于任何代码中创建的任何数据和对象,最初都只存在于CPU中,为了使GPU能够访问和处理这些数据,需要手动将数据送到GPU中,即在初始化渲染器时大段的代码都是在执行”创建数据-创建上传缓冲区和资源缓冲区-创建资源缓冲区视图-拷贝数据到上传缓冲区-从上传缓冲区拷贝到资源缓冲区->销毁上传缓冲区“这一过程。

约定以下命名:

上传缓冲区/上传堆(upload Buffer):CPU无法将资源直接送进GPU用于计算的区域,只能将它暂存在RAM或GPU划分出来用于和CPU通讯的区域内。称这些区域为上传缓冲区/上传堆,是CPU可读写GPU可读但速度较慢的区域。

资源缓冲区/默认堆(Default Buffer):GPU内部存储资源和计算的区域,按照对应的资源进行进一步的命名划分,如顶点缓冲区、视图缓冲区,其本质是连续内存的二进制数据,需要根据描述符来确定具体的读取方式。

其中,常量缓冲区是例外,由于每帧都有大量的变化,通常直接写在上传缓冲区内。

顶点与输入布局描述

1. 定义顶点结构体,声明顶点中存储的具体数据。譬如,一个包含位置和颜色信息的顶点以如下方式进行定义:

顶点结构体和顶点类型绑定,有多少顶点类型就需要创建多少顶点结构体。

1
2
3
4
5
    struct Vertex
    {
        XMFLOAT3 Pos; 
        XMFLOAT4 Color; 
    }; 

2. 为每个顶点结构体创建D3D12_INPUT_ELEMENT_DESC数组,用于描述顶点结构体中每个元素。

\(1.\)中的Vertex为例,创建D3D12_INPUT_ELEMENT_DESC的语句如下:

D3D12_INPUT_ELEMENT_DESC elementDescs[] = {
    {
        "POSITION",                     // 语义名:必须与 HLSL 中的冒号后名称一致
        0,                              // 语义索引:如果有两个坐标,这里写 0 和 1
        DXGI_FORMAT_R32G32B32_FLOAT,    // 格式:对应 XMFLOAT3 (3个32位浮点数)
        0,                              // 输入槽:通常设为 0
        0,                              // 偏移量:Pos 是结构体第一个成员,偏移量为 0
        D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, // 类别:按顶点数据处理
        0                               // 实例步进:非实例化渲染设为 0
    },
    {
        "COLOR",                        // 语义名:对应 HLSL 中的 COLOR
        0,                              // 语义索引
        DXGI_FORMAT_R32G32B32A32_FLOAT, // 格式:对应 XMFLOAT4 (4个32位浮点数)
        0,                              // 输入槽:同一个 Buffer,槽位相同
        12,                             // 偏移量:跳过前面的 Pos (3*4=12字节)
        D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA,
        0
    }
};

D3D12_INPUT_ELEMENT_DESC包含\(7\)个元素,每个元素有固定含义,描述如下:

D3D12_INPUT_ELEMENT_DESC结构体定义及含义
1
2
3
4
5
6
7
8
9
           typedef struct D3D12_INPUT_ELEMENT_DESC {
               LPCSTR                     SemanticName;         // 1. 语义名
               UINT                       SemanticIndex;        // 2. 语义索引
               DXGI_FORMAT                Format;               // 3. 数据格式
               UINT                       InputSlot;            // 4. 输入槽
               UINT                       AlignedByteOffset;    // 5. 对齐字节偏移
               D3D12_INPUT_CLASSIFICATION InputSlotClass;       // 6. 输入分类
               UINT                       InstanceDataStepRate; // 7. 实例步进速率
           } D3D12_INPUT_ELEMENT_DESC;
  • SemanticName:语义名,一个和元素相关联的特定字符串,表明该元素的用途。

    语义名需要和顶点着色器输入签名(参数列表)保持一致,因此通常会使用规范化命名,常用语义名包括"POSITION""COLOR""NORMAL""TEXCOORD"等。

  • SemanticIndex:语义索引,如果一个顶点结构体中存在多个相同语义的的元素,则需要通过语义索引区分它们。

    这样做能在区分相同语义的元素的同时不引进新的语义名,默认从0开始。

  • Format:数据格式,即顶点结构体中对应的元素的数据格式,但这里不写在顶点结构体中声明元素时的类型名,应该写DXGI_FORMAT枚举
  • InputSlot:输入槽索引,指定传递元素所以所用的输入槽(顶点缓冲区),值范围为0-15,只有一个时默认设为0。
  • AlignedByteOffset:对齐字节偏移,值为顶点结构体中这个元素前所有元素占用的字节总和。

    如果是首个元素的话,值为0。

  • InputSlotClass:输入分类,表明数据按顶点还是实例读取,有以下两个选项:

    D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA:每个顶点读取一次数据。最常用的选项,画普通物体时使用。

    D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA:用于实例化渲染。

  • InstanceDataStepRate:实例步进速率,默认设为0,开启实例化渲染时设为1。

3. 声明D3D12_INPUT_LAYOUT_DESC对象,作为D3D12_INPUT_ELEMENT_DESC的清单,并将D3D12_INPUT_ELEMENT_DESC数组绑定到该对象上。

\(1.\)\(2.\)声明的顶点结构体为例,该步骤的语句如下:

1
2
3
D3D12_INPUT_LAYOUT_DESC inputLayoutDesc;     //声明对象
inputLayoutDesc.pInputElementDescs = elementDescs.data();      // 指向描述顶点结构体的D3D12_INPUT_ELEMENT_DESC数组
inputLayoutDesc.NumElements = _countof(elementDescs);   // 告诉 GPU 顶点结构体中有两个元素

其中,data()方法是标准STL容器提供的方法,返回指向容器首个元素的原始指针

Note

D3D12_INPUT_LAYOUT_DESC对象是一个过程对象,仅用于初始化渲染管线,因此不必将其声明为类的成员。

顶点缓冲区和顶点缓冲区视图

顶点缓冲区视图本质是描述符,但不需要放进描述符堆中

1. 使用上文中的顶点类型创建顶点数组。

vertices
Vertex vertieces[]={
        {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)}
    };

2. 根据顶点数组的大小计算缓冲区大小,创建上传缓冲区和顶点缓冲区,创建顶点缓冲区视图,将顶点数组通过上传缓冲区拷贝到顶点缓冲区。

Note

这一过程仅在录制命令,还没有执行。

实现代码
    //根据顶点数量计算box对应的顶点缓冲区字节大小
    const UINT64 vbByteSize=sizeof(vertieces);

    //创建顶点缓冲区和上传缓冲区的指针
    ComPtr<ID3D12Resource>VertexBufferGPU=nullptr;
    ComPtr<ID3D12Resource>VertexBufferUpload=nullptr;

    //创建上传缓冲区和顶点缓冲区,创建顶点缓冲区视图,将顶点数组通过上传缓冲区拷贝到顶点缓冲区。
    //该方法由龙书作者进行封装
   VertexBufferGPU=d3dUtil::CreateDefaultBuffer(md3dDevice.Get(),mCommandList.Get(),vertieces,vbByteSize,VertexBufferUpload);

其中,d3dUtil::CreateDefaultBuffer是由龙书作者封装的工具类,其实现如下:

d3dUtil::CreateDefaultBuffer()
//方法签名,该方法接收多个参数,返回一个实现了ID3DResource接口的对象
Microsoft::WRL::ComPtr<ID3D12Resource> d3dUtil::CreateDefaultBuffer(
    ID3D12Device* device,       //Device 原始指针
    ID3D12GraphicsCommandList* cmdList,    //命令列表 原始指针
    const void* initData,   //初始化数据的指针,即之后要拷贝到缓冲区的资源。这里需要传指针,如果传的是C风格数组可以不用指针,数组名会自动退化为首元素指针
    UINT64 byteSize,    //缓冲区的字节大小
    Microsoft::WRL::ComPtr<ID3D12Resource>& uploadBuffer)    //输出参数,用于存放上传缓冲区的com指针的引用,在调用时传指针的名字即可
{
    ComPtr<ID3D12Resource> 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),       //资源类型:大小为byteSize的缓冲区
        D3D12_RESOURCE_STATE_COMMON,      //初始化状态为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),   //物理位置:CPU上传堆(内存
        D3D12_HEAP_FLAG_NONE,
        &CD3DX12_RESOURCE_DESC::Buffer(byteSize),    
        D3D12_RESOURCE_STATE_GENERIC_READ,    //初始化状态为GENERIC_READ,意为准备好让CPU拷贝数据到此/GPU读取并拷贝
        nullptr,
        IID_PPV_ARGS(uploadBuffer.GetAddressOf())));


    // Describe the data we want to copy into the default buffer.
    //原始数据拷贝到GPU需要被包装成统一的格式,因此需要描述怎么读取被包装后的数据
    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));
    //拷贝,1是模板类,表示这次拷贝的资源数量
    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.
    //现在只是完成了录制,而没有实际执行
    //不可以销毁上传缓冲区


    //返回默认堆的com指针
    return defaultBuffer;
}

索引和索引缓冲区

和顶点类似,如果需要GPU可以访问索引,就需要索引缓冲区和对应的视图(描述)。索引缓冲区视图同样不需要放进描述符堆。

1. 创建几何体的索引数组,此处以立方体为例

indices
//定义box的索引数据
    std::uint16_t indices[]={
        //前表面
        0,1,2,
        0,2,3,
        //后表面
        4,6,5,
        4,7,6,
        //左表面
        4,5,1,
        4,1,0,
        //右表面
        3,2,6,
        3,6,7,
        //上表面
        1,5,6,
        1,6,2,
        //下表面
        4,0,3,
        4,3,7
    };

2. 根据顶点数组的大小计算缓冲区大小,创建上传缓冲区和顶点缓冲区,创建顶点缓冲区视图,将顶点数组通过上传缓冲区拷贝到顶点缓冲区。

顶点着色器

DX12使用高级着色器语言(HLSL)编写着色器。

常量缓冲区

常量缓冲区由CPU每帧更新,位于上传堆而非默认堆,大小必须为硬件最小分配空间(256B)的整数倍,多数时候需要多个常量缓冲区。

根签名和描述符表

将根签名称为来源标记会更好理解。

流水线状态对象

Pipeline State Object

帧资源

在通常情况下,CPU必须等待GPU执行完当前帧的命令后才会开始下一帧的计算,以避免提前销毁GPU执行命令队列中的命令所需用到的资源。这使得在每帧内GPU和CPU各自存在一些闲置的时间,造成相当程度的性能浪费。

帧资源是一种有效的解决方案:通过创建数个包含每帧需要使用的管线对象的结构体数组,使CPU在GPU绘制期间可以进行下一帧或数帧的计算。

基本的帧资源结构体至少应该包含以下成员:

  • 命令分配器
  • 常量缓冲区(全局、对象、材质)
  • 围栏值
  • 指向各个常量缓冲区的指针

以下为一个基础的帧资源结构体的结构:

FrameResource.h
#pragma once

#include "stdafx.h"
#include "DXSampleHelper.h"
#include "VertexData.h"
#include "d3dUtil.h"

using Microsoft::WRL::ComPtr;

class FrameResource {
public:
    UINT64 m_fenceValue=0;

    ComPtr<ID3D12CommandAllocator> m_commandAllocator;

    //Constant buffer resource for each frame.
    ComPtr<ID3D12Resource> m_passCB;  //pass constant buffer
    ComPtr<ID3D12Resource> m_objectCB;  //object constant buffer
    ComPtr<ID3D12Resource> m_materialCB;  //material constant buffer

    //ptr to mapped data for each frame.
    UINT8* mp_passCBMapped = nullptr;
    UINT8* mp_objectCBMapped = nullptr;
    UINT8* mp_materialCBMapped = nullptr;

public:
    FrameResource(ID3D12Device* device,UINT passCBElementNums,UINT objectCBElementNums,UINT materialCBElementNums);
    ~FrameResource();

private:
    FrameResource() = delete;
};
FrameResource.cpp
#include "FrameResource.h"

FrameResource::FrameResource(ID3D12Device* device, UINT passCBElementNums, UINT objectCBElementNums, UINT materialCBElementNums)
{
    //Create command allocator for each frame.
    ThrowIfFailed(device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&m_commandAllocator)));

    /* todo: constant buffer */
    //当前假定所有常量缓冲区中存储的元素都是ObjectConstants
    UINT passCBByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof(ObjectConstants)) * passCBElementNums;
    UINT objectCBByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof(ObjectConstants)) * objectCBElementNums;
    UINT materialCBByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof(ObjectConstants)) * materialCBElementNums;

    //Create constant buffer resource for each frame.
    //Constant Buffer real is upload heap;
    {
        auto heapProperties = CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD);
        auto resourceDesc = CD3DX12_RESOURCE_DESC::Buffer(passCBByteSize);
        ThrowIfFailed(device->CreateCommittedResource(
            &heapProperties,
            D3D12_HEAP_FLAG_NONE,
            &resourceDesc,
            D3D12_RESOURCE_STATE_GENERIC_READ,
            nullptr,
            IID_PPV_ARGS(&m_passCB)));
    }

    {
        auto heapProperties = CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD);
        auto resourceDesc = CD3DX12_RESOURCE_DESC::Buffer(objectCBByteSize);
        ThrowIfFailed(device->CreateCommittedResource(
            &heapProperties,
            D3D12_HEAP_FLAG_NONE,
            &resourceDesc,
            D3D12_RESOURCE_STATE_GENERIC_READ,
            nullptr,
            IID_PPV_ARGS(&m_objectCB)));
    }

    {
        auto heapProperties = CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD);
        auto resourceDesc = CD3DX12_RESOURCE_DESC::Buffer(materialCBByteSize);
        ThrowIfFailed(device->CreateCommittedResource(
            &heapProperties,
            D3D12_HEAP_FLAG_NONE,
            &resourceDesc,
            D3D12_RESOURCE_STATE_GENERIC_READ,
            nullptr,
            IID_PPV_ARGS(&m_materialCB)));
    }

    ThrowIfFailed(m_passCB->Map(0, nullptr, reinterpret_cast<void**>(&mp_passCBMapped)));
    ThrowIfFailed(m_objectCB->Map(0, nullptr, reinterpret_cast<void**>(&mp_objectCBMapped)));
    ThrowIfFailed(m_materialCB->Map(0, nullptr, reinterpret_cast<void**>(&mp_materialCBMapped)));
}

FrameResource::~FrameResource() {
    if (m_passCB) {
        m_passCB->Unmap(0, nullptr);
    }
    if (m_objectCB) {
        m_objectCB->Unmap(0, nullptr);
    }
    if (m_materialCB) {
        m_materialCB->Unmap(0, nullptr);
    }
}

在渲染器中使用帧资源时,应注意在LoadPipeline()方法后、LoadAsset()方法前创建帧资源,这是因为创建命令队列等资源需要使用到命令分配器。

初始化帧资源数组:
void SampleTriangle::BuildFrameResources() {
    //create frame resource and add to vector.
    for (int i = 0; i < gNumFrameResources; i++) {
        m_frameResources.push_back(std::make_unique<FrameResource>(m_device.Get(), 1, 1, 1));
    }

    //mark curr frame resource ptr.
    m_currFrameResourceIndex = 0;
    m_currFrameResource = m_frameResources[m_currFrameResourceIndex].get();
}

其中,gNumFrameResources为帧资源数组的元素数量,m_currFrameResourceIndex为当前使用的帧资源在帧资源中的索引,m_currFrameResource为指向当前正在录制的帧资源的指针。

由于需要每帧更换帧资源,因此要在每帧更新当前使用的帧资源及其索引,这些逻辑应该在onUpdate方法中执行而非OnRender,因为它们并不涉及任何GPU相关的渲染。

onUpdate()
void SampleTriangle::OnUpdate(){
    //update curr index and curr ptr.
    m_currFrameResourceIndex = (m_currFrameResourceIndex + 1) % gNumFrameResources;
    m_currFrameResource = m_frameResources[m_currFrameResourceIndex].get();

    //check curr frame resource is still in use by gpu or not.
    //if fenceValue==0 ,it is program's first frame and not need wait.
    if(m_currFrameResource->m_fenceValue != 0 && m_fence->GetCompletedValue() < m_currFrameResource->m_fenceValue){
        ThrowIfFailed(m_fence->SetEventOnCompletion(m_currFrameResource->m_fenceValue, m_fenceEvent));
        WaitForSingleObject(m_fenceEvent, INFINITE);
    }

    //...other logic...
}

关于引入帧资源到渲染器中的详细变动见Frame Resource.imgui base · SchlingEvens/UmbraForward@9028c9c,此处略去细节说明。

不同形状的几何体