013-利用 Direct3D 绘制几何体

利用 Direct3D 绘制几何体

计算机图形学的核心任务之一是绘制和显示三维几何形体。在 DirectX 12 中,这个过程涉及多个步骤:定义几何数据、设置渲染状态、编写着色器程序,以及发送绘制命令。本章将详细讲解如何利用 Direct3D 绘制各种几何体,从基本概念到实际实现。

6.1 顶点与输入布局

顶点是构成 3D 几何体的基本元素,每个顶点通常包含位置信息及其他属性(如颜色、法线、纹理坐标等)。输入布局定义了顶点数据的内存结构,使 Direct3D 能够正确解释顶点数据。

6.1.1 顶点结构定义

顶点结构是一个自定义的 C++ 结构体,用于存储每个顶点的属性数据:

cpp

// 简单顶点结构(位置和颜色)
struct SimpleVertex {
    XMFLOAT3 Position;  // 顶点在3D空间中的位置
    XMFLOAT4 Color;     // 顶点颜色
};

// 包含纹理坐标的顶点结构
struct TexturedVertex {
    XMFLOAT3 Position;  // 位置
    XMFLOAT2 TexCoord;  // 纹理坐标
};

// 完整的顶点结构(适用于光照计算)
struct CompleteVertex {
    XMFLOAT3 Position;  // 位置
    XMFLOAT3 Normal;    // 法线
    XMFLOAT2 TexCoord;  // 纹理坐标
    XMFLOAT4 Color;     // 颜色
    XMFLOAT3 Tangent;   // 切线(用于法线映射)
    XMFLOAT3 Binormal;  // 副法线
};

在设计顶点结构时,需要考虑以下因素:

内存对齐:为了最佳性能,顶点结构应遵循 DirectX 的内存对齐要求
数据大小:只包含必要的数据以减少内存和带宽使用
着色器兼容性:确保顶点结构与顶点着色器的输入参数匹配

6.1.2 输入布局描述

输入布局描述告诉 Direct3D 如何解释顶点缓冲区中的数据:

cpp

// 为 SimpleVertex 创建输入布局描述
D3D12_INPUT_ELEMENT_DESC simpleInputLayout[] = {
    { "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 }
};

// 为 TexturedVertex 创建输入布局描述
D3D12_INPUT_ELEMENT_DESC texturedInputLayout[] = {
    { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, 
      D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
    { "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 12, 
      D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }
};

// 为 CompleteVertex 创建输入布局描述
D3D12_INPUT_ELEMENT_DESC completeInputLayout[] = {
    { "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 },
    { "COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 32, 
      D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
    { "TANGENT", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 48, 
      D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
    { "BINORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 60, 
      D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }
};

每个输入元素描述包含以下信息:

语义名(如 “POSITION”, “COLOR”):与着色器输入参数匹配
语义索引:处理同名语义的索引
数据格式:指定数据类型和组件数量
输入槽:指定数据来源的顶点缓冲区
字节偏移:从顶点开始到该元素的字节偏移
分类:指定数据是按顶点还是按实例
实例数据步进率:实例化绘制中的步进率

6.1.3 顶点数据的内存布局

理解顶点数据的内存布局对于正确设置输入布局至关重要:

asciidoc

SimpleVertex 内存布局 (24字节):
+-------------+-------------+
| Position (12字节) | Color (16字节)  |
+-------------+-------------+
0            12             28

TexturedVertex 内存布局 (20字节):
+-------------+-------------+
| Position (12字节) | TexCoord (8字节) |
+-------------+-------------+
0            12             20

CompleteVertex 内存布局 (72字节):
+-------------+-------------+-------------+-------------+-------------+-------------+
| Position (12) | Normal (12)  | TexCoord (8) | Color (16)  | Tangent (12) | Binormal (12)|
+-------------+-------------+-------------+-------------+-------------+-------------+
0            12            24            32            48            60            72

6.1.4 创建和设置顶点缓冲区

定义好顶点结构和输入布局后,需要创建顶点缓冲区并填充数据:

cpp

// 创建三角形的顶点数据
SimpleVertex triangleVertices[] = {
    { XMFLOAT3(0.0f, 0.5f, 0.0f), XMFLOAT4(1.0f, 0.0f, 0.0f, 1.0f) },   // 顶部(红色)
    { XMFLOAT3(0.5f, -0.5f, 0.0f), XMFLOAT4(0.0f, 1.0f, 0.0f, 1.0f) },  // 右下(绿色)
    { XMFLOAT3(-0.5f, -0.5f, 0.0f), XMFLOAT4(0.0f, 0.0f, 1.0f, 1.0f) }  // 左下(蓝色)
};

// 计算缓冲区大小并创建顶点缓冲区资源
const UINT vertexBufferSize = sizeof(triangleVertices);

// 创建顶点缓冲区
ComPtr<ID3D12Resource> vertexBuffer;

// 创建上传堆属性
CD3DX12_HEAP_PROPERTIES heapProps(D3D12_HEAP_TYPE_UPLOAD);

// 创建缓冲区资源描述
CD3DX12_RESOURCE_DESC bufferDesc = CD3DX12_RESOURCE_DESC::Buffer(vertexBufferSize);

// 创建顶点缓冲区
device->CreateCommittedResource(
    &heapProps,
    D3D12_HEAP_FLAG_NONE,
    &bufferDesc,
    D3D12_RESOURCE_STATE_GENERIC_READ,
    nullptr,
    IID_PPV_ARGS(&vertexBuffer)
);

// 将顶点数据复制到顶点缓冲区
UINT8* pVertexDataBegin;
CD3DX12_RANGE readRange(0, 0);  // CPU 不需要读取此资源
vertexBuffer->Map(0, &readRange, reinterpret_cast<void**>(&pVertexDataBegin));
memcpy(pVertexDataBegin, triangleVertices, vertexBufferSize);
vertexBuffer->Unmap(0, nullptr);

// 创建顶点缓冲区视图
D3D12_VERTEX_BUFFER_VIEW vertexBufferView;
vertexBufferView.BufferLocation = vertexBuffer->GetGPUVirtualAddress();
vertexBufferView.SizeInBytes = vertexBufferSize;
vertexBufferView.StrideInBytes = sizeof(SimpleVertex);

在绘制时,需要将顶点缓冲区设置到输入装配器阶段:

cpp

// 设置顶点缓冲区到命令列表
commandList->IASetVertexBuffers(0, 1, &vertexBufferView);

// 设置图元拓扑
commandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);

// 绘制顶点
commandList->DrawInstanced(3, 1, 0, 0);

6.2 顶点缓冲区

顶点缓冲区是存储顶点数据的 GPU 内存资源。在 Direct3D 12 中,创建和管理顶点缓冲区需要开发者显式控制资源状态和内存分配。

6.2.1 创建顶点缓冲区

顶点缓冲区的创建涉及多个步骤,包括内存分配、数据上传和视图创建:

cpp

// 创建顶点缓冲区的工具类
class VertexBuffer {
public:
    VertexBuffer(ID3D12Device* device, size_t numVertices, size_t stride, const void* data) 
        : m_numVertices(numVertices), m_stride(stride) {
        
        const UINT bufferSize = static_cast<UINT>(numVertices * stride);
        
        // 1. 创建顶点缓冲区资源(DEFAULT 堆 - GPU 访问最优)
        CD3DX12_HEAP_PROPERTIES defaultHeapProps(D3D12_HEAP_TYPE_DEFAULT);
        CD3DX12_RESOURCE_DESC bufferDesc = CD3DX12_RESOURCE_DESC::Buffer(bufferSize);
        
        device->CreateCommittedResource(
            &defaultHeapProps,
            D3D12_HEAP_FLAG_NONE,
            &bufferDesc,
            D3D12_RESOURCE_STATE_COPY_DEST,  // 初始状态为复制目标
            nullptr,
            IID_PPV_ARGS(&m_vertexBuffer)
        );
        
        // 2. 创建上传堆资源用于传输数据
        CD3DX12_HEAP_PROPERTIES uploadHeapProps(D3D12_HEAP_TYPE_UPLOAD);
        
        device->CreateCommittedResource(
            &uploadHeapProps,
            D3D12_HEAP_FLAG_NONE,
            &bufferDesc,
            D3D12_RESOURCE_STATE_GENERIC_READ,
            nullptr,
            IID_PPV_ARGS(&m_uploadBuffer)
        );
        
        // 3. 将数据复制到上传堆
        void* mappedData = nullptr;
        CD3DX12_RANGE readRange(0, 0);  // CPU不需要从上传堆读取数据
        m_uploadBuffer->Map(0, &readRange, &mappedData);
        memcpy(mappedData, data, bufferSize);
        m_uploadBuffer->Unmap(0, nullptr);
        
        // 4. 创建顶点缓冲区视图
        m_vertexBufferView.BufferLocation = m_vertexBuffer->GetGPUVirtualAddress();
        m_vertexBufferView.SizeInBytes = bufferSize;
        m_vertexBufferView.StrideInBytes = static_cast<UINT>(stride);
    }
    
    // 将顶点数据从上传堆复制到默认堆
    void CopyVertexData(ID3D12GraphicsCommandList* cmdList) {
        cmdList->CopyBufferRegion(
            m_vertexBuffer.Get(), 0,
            m_uploadBuffer.Get(), 0,
            m_numVertices * m_stride
        );
        
        // 转换资源状态为顶点缓冲区
        CD3DX12_RESOURCE_BARRIER barrier = CD3DX12_RESOURCE_BARRIER::Transition(
            m_vertexBuffer.Get(),
            D3D12_RESOURCE_STATE_COPY_DEST,
            D3D12_RESOURCE_STATE_VERTEX_AND_CONSTANT_BUFFER
        );
        
        cmdList->ResourceBarrier(1, &barrier);
    }
    
    // 获取顶点缓冲区视图
    const D3D12_VERTEX_BUFFER_VIEW& GetView() const {
        return m_vertexBufferView;
    }
    
private:
    ComPtr<ID3D12Resource> m_vertexBuffer;    // GPU 内存中的顶点缓冲区
    ComPtr<ID3D12Resource> m_uploadBuffer;    // 上传堆缓冲区(用于数据传输)
    D3D12_VERTEX_BUFFER_VIEW m_vertexBufferView; // 顶点缓冲区视图
    size_t m_numVertices;   // 顶点数量
    size_t m_stride;        // 每个顶点的字节大小
};

上面的类封装了创建顶点缓冲区的常见操作,包括:

在 GPU 默认堆中创建顶点缓冲区资源
创建上传堆资源用于数据传输
将顶点数据从 CPU 复制到上传堆
创建顶点缓冲区视图
提供将数据从上传堆复制到 GPU 内存的方法

6.2.2 动态更新顶点缓冲区

在一些场景中,需要动态更新顶点数据。这可以通过以下方式实现:

cpp

// 动态顶点缓冲区类
class DynamicVertexBuffer {
public:
    DynamicVertexBuffer(ID3D12Device* device, size_t numVertices, size_t stride)
        : m_numVertices(numVertices), m_stride(stride) {
        
        const UINT bufferSize = static_cast<UINT>(numVertices * stride);
        
        // 创建顶点缓冲区在上传堆中(允许 CPU 写入、GPU 读取)
        CD3DX12_HEAP_PROPERTIES uploadHeapProps(D3D12_HEAP_TYPE_UPLOAD);
        CD3DX12_RESOURCE_DESC bufferDesc = CD3DX12_RESOURCE_DESC::Buffer(bufferSize);
        
        device->CreateCommittedResource(
            &uploadHeapProps,
            D3D12_HEAP_FLAG_NONE,
            &bufferDesc,
            D3D12_RESOURCE_STATE_GENERIC_READ,
            nullptr,
            IID_PPV_ARGS(&m_vertexBuffer)
        );
        
        // 创建顶点缓冲区视图
        m_vertexBufferView.BufferLocation = m_vertexBuffer->GetGPUVirtualAddress();
        m_vertexBufferView.SizeInBytes = bufferSize;
        m_vertexBufferView.StrideInBytes = static_cast<UINT>(stride);
    }
    
    // 更新顶点数据
    void Update(const void* data, size_t size) {
        UINT8* mappedData = nullptr;
        CD3DX12_RANGE readRange(0, 0);
        m_vertexBuffer->Map(0, &readRange, reinterpret_cast<void**>(&mappedData));
        memcpy(mappedData, data, size);
        m_vertexBuffer->Unmap(0, nullptr);
    }
    
    // 获取顶点缓冲区视图
    const D3D12_VERTEX_BUFFER_VIEW& GetView() const {
        return m_vertexBufferView;
    }
    
private:
    ComPtr<ID3D12Resource> m_vertexBuffer;
    D3D12_VERTEX_BUFFER_VIEW m_vertexBufferView;
    size_t m_numVertices;
    size_t m_stride;
};

动态顶点缓冲区将资源直接创建在上传堆中,这样 CPU 可以直接修改其内容。不过,这样做的缺点是 GPU 访问上传堆的性能低于默认堆。因此,动态顶点缓冲区主要适用于频繁更新的小型几何体。

6.2.3 顶点缓冲区的高级应用

多流顶点缓冲区

Direct3D 12 支持使用多个顶点缓冲区作为输入,这在某些场景下很有用:

cpp

// 定义两个分离的顶点缓冲区
struct PositionVertex { XMFLOAT3 Position; };
struct ColorVertex { XMFLOAT4 Color; };

// 创建位置顶点缓冲区
PositionVertex positionVertices[] = {
    { XMFLOAT3(0.0f, 0.5f, 0.0f) },
    { XMFLOAT3(0.5f, -0.5f, 0.0f) },
    { XMFLOAT3(-0.5f, -0.5f, 0.0f) }
};
VertexBuffer positionBuffer(device, 3, sizeof(PositionVertex), positionVertices);

// 创建颜色顶点缓冲区
ColorVertex colorVertices[] = {
    { XMFLOAT4(1.0f, 0.0f, 0.0f, 1.0f) },
    { XMFLOAT4(0.0f, 1.0f, 0.0f, 1.0f) },
    { XMFLOAT4(0.0f, 0.0f, 1.0f, 1.0f) }
};
VertexBuffer colorBuffer(device, 3, sizeof(ColorVertex), colorVertices);

// 定义输入布局
D3D12_INPUT_ELEMENT_DESC multiStreamLayout[] = {
    {"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0},
    {"COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0}
};

// 绘制时设置多个顶点缓冲区
D3D12_VERTEX_BUFFER_VIEW vertexBufferViews[] = {
    positionBuffer.GetView(),
    colorBuffer.GetView()
};
commandList->IASetVertexBuffers(0, 2, vertexBufferViews);

多流顶点缓冲区的主要优点:

灵活性:不同属性可以独立更新
内存效率:某些属性可以在多个网格间共享
缓存效率:相关属性保持在一起,提高缓存命中率

实例化绘制

实例化允许使用一次绘制调用绘制同一个几何体的多个副本,每个实例可以有自己的变换、颜色等属性:

cpp

// 实例数据结构
struct InstanceData {
    XMFLOAT4X4 Transform;
    XMFLOAT4 Color;
};

// 创建实例数据
std::vector<InstanceData> instanceData(100);  // 100个实例
for (int i = 0; i < 100; i++) {
    // 计算每个实例的位置和颜色
    float x = (i % 10) * 2.0f - 9.0f;
    float z = (i / 10) * 2.0f - 9.0f;
    
    // 创建变换矩阵
    XMMATRIX transform = XMMatrixTranslation(x, 0.0f, z);
    XMStoreFloat4x4(&instanceData[i].Transform, XMMatrixTranspose(transform));
    
    // 设置颜色
    float r = (i % 10) / 10.0f;
    float g = (i / 10) / 10.0f;
    float b = 0.5f;
    instanceData[i].Color = XMFLOAT4(r, g, b, 1.0f);
}

// 创建实例缓冲区
VertexBuffer instanceBuffer(device, 100, sizeof(InstanceData), instanceData.data());

// 定义添加实例数据的输入布局
D3D12_INPUT_ELEMENT_DESC instancedLayout[] = {
    // 顶点数据
    {"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},
    
    // 实例数据(注意:设置为"按实例")
    {"WORLD", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 0, D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA, 1},
    {"WORLD", 1, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 16, D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA, 1},
    {"WORLD", 2, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 32, D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA, 1},
    {"WORLD", 3, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 48, D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA, 1},
    {"COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 64, D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA, 1}
};

// 绘制时设置两个缓冲区
D3D12_VERTEX_BUFFER_VIEW views[] = {
    vertexBuffer.GetView(),  // 模型的顶点缓冲区
    instanceBuffer.GetView() // 实例数据缓冲区
};
commandList->IASetVertexBuffers(0, 2, views);

// 实例化绘制
commandList->DrawInstanced(
    vertexCount,    // 每个实例的顶点数
    100,            // 实例数量
    0,              // 起始顶点位置
    0               // 起始实例位置
);

实例化绘制大大减少了 CPU 开销,适用于绘制大量相似对象(如树木、草、粒子等)。

6.3 索引和索引缓冲区

索引缓冲区存储指向顶点缓冲区的索引值,允许顶点数据重用,从而减少内存占用并提高绘制效率。

6.3.1 索引缓冲区的创建和使用

索引缓冲区的创建与顶点缓冲区类似,但用途不同:

cpp

// 创建索引缓冲区类
class IndexBuffer {
public:
    IndexBuffer(ID3D12Device* device, size_t numIndices, bool use32BitIndices, const void* data) 
        : m_numIndices(numIndices), m_is32Bit(use32BitIndices) {
        
        const UINT indexSize = use32BitIndices ? 4 : 2;  // 4 字节或 2 字节
        const UINT bufferSize = static_cast<UINT>(numIndices * indexSize);
        
        // 创建默认堆索引缓冲区
        CD3DX12_HEAP_PROPERTIES defaultHeapProps(D3D12_HEAP_TYPE_DEFAULT);
        CD3DX12_RESOURCE_DESC bufferDesc = CD3DX12_RESOURCE_DESC::Buffer(bufferSize);
        
        device->CreateCommittedResource(
            &defaultHeapProps,
            D3D12_HEAP_FLAG_NONE,
            &bufferDesc,
            D3D12_RESOURCE_STATE_COPY_DEST,
            nullptr,
            IID_PPV_ARGS(&m_indexBuffer)
        );
        
        // 创建上传堆资源
        CD3DX12_HEAP_PROPERTIES uploadHeapProps(D3D12_HEAP_TYPE_UPLOAD);
        
        device->CreateCommittedResource(
            &uploadHeapProps,
            D3D12_HEAP_FLAG_NONE,
            &bufferDesc,
            D3D12_RESOURCE_STATE_GENERIC_READ,
            nullptr,
            IID_PPV_ARGS(&m_uploadBuffer)
        );
        
        // 将索引数据复制到上传堆
        void* mappedData = nullptr;
        CD3DX12_RANGE readRange(0, 0);
        m_uploadBuffer->Map(0, &readRange, &mappedData);
        memcpy(mappedData, data, bufferSize);
        m_uploadBuffer->Unmap(0, nullptr);
        
        // 创建索引缓冲区视图
        m_indexBufferView.BufferLocation = m_indexBuffer->GetGPUVirtualAddress();
        m_indexBufferView.SizeInBytes = bufferSize;
        m_indexBufferView.Format = use32BitIndices ? 
            DXGI_FORMAT_R32_UINT : DXGI_FORMAT_R16_UINT;
    }
    
    // 将索引数据从上传堆复制到默认堆
    void CopyIndexData(ID3D12GraphicsCommandList* cmdList) {
        const UINT indexSize = m_is32Bit ? 4 : 2;
        cmdList->CopyBufferRegion(
            m_indexBuffer.Get(), 0,
            m_uploadBuffer.Get(), 0,
            m_numIndices * indexSize
        );
        
        // 转换资源状态
        CD3DX12_RESOURCE_BARRIER barrier = CD3DX12_RESOURCE_BARRIER::Transition(
            m_indexBuffer.Get(),
            D3D12_RESOURCE_STATE_COPY_DEST,
            D3D12_RESOURCE_STATE_INDEX_BUFFER
        );
        
        cmdList->ResourceBarrier(1, &barrier);
    }
    
    // 获取索引缓冲区视图
    const D3D12_INDEX_BUFFER_VIEW& GetView() const {
        return m_indexBufferView;
    }
    
    // 获取索引数量
    size_t GetIndexCount() const {
        return m_numIndices;
    }
    
private:
    ComPtr<ID3D12Resource> m_indexBuffer;
    ComPtr<ID3D12Resource> m_uploadBuffer;
    D3D12_INDEX_BUFFER_VIEW m_indexBufferView;
    size_t m_numIndices;
    bool m_is32Bit;
};

6.3.2 使用索引绘制几何体

下面是如何使用索引缓冲区创建和绘制一个简单的矩形(由两个三角形组成):

cpp

// 定义四个顶点(矩形的四个角)
SimpleVertex rectangleVertices[] = {
    { XMFLOAT3(-0.5f, -0.5f, 0.0f), XMFLOAT4(1.0f, 0.0f, 0.0f, 1.0f) }, // 左下
    { XMFLOAT3(-0.5f,  0.5f, 0.0f), XMFLOAT4(0.0f, 1.0f, 0.0f, 1.0f) }, // 左上
    { XMFLOAT3( 0.5f,  0.5f, 0.0f), XMFLOAT4(0.0f, 0.0f, 1.0f, 1.0f) }, // 右上
    { XMFLOAT3( 0.5f, -0.5f, 0.0f), XMFLOAT4(1.0f, 1.0f, 0.0f, 1.0f) }  // 右下
};

// 定义索引数据(两个三角形)
uint16_t rectangleIndices[] = {
    0, 1, 2,  // 第一个三角形(左下、左上、右上)
    0, 2, 3   // 第二个三角形(左下、右上、右下)
};

// 创建顶点缓冲区和索引缓冲区
VertexBuffer vertexBuffer(device, 4, sizeof(SimpleVertex), rectangleVertices);
IndexBuffer indexBuffer(device, 6, false, rectangleIndices);  // 6个索引,使用16位索引

// 在命令列表中设置并绘制
vertexBuffer.CopyVertexData(commandList);
indexBuffer.CopyIndexData(commandList);

// 设置顶点和索引缓冲区
commandList->IASetVertexBuffers(0, 1, &vertexBuffer.GetView());
commandList->IASetIndexBuffer(&indexBuffer.GetView());
commandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);

// 使用索引绘制
commandList->DrawIndexedInstanced(
    static_cast<UINT>(indexBuffer.GetIndexCount()),  // 索引数量
    1,                                              // 实例数量
    0,                                              // 起始索引位置
    0,                                              // 起始顶点位置
    0                                               // 起始实例位置
);

6.3.3 索引缓冲区的优势

使用索引缓冲区有以下优点:

内存节省:顶点数据可以被重用,减少内存需求
带宽优化:索引通常比顶点数据小,减少 GPU 内存带宽使用
绘制优化:GPU 可以缓存和重用顶点处理结果
简化几何体构建:便于构建和操作复杂几何体

索引缓冲区特别适合表示网格模型,因为在网格中许多三角形共享顶点。

6.4 顶点着色器示例

顶点着色器是 GPU 渲染管线中的第一个可编程阶段,负责处理输入顶点数据并转换顶点位置。

6.4.1 基础顶点着色器

最简单的顶点着色器负责将顶点从模型空间转换到裁剪空间:

hlsl

// 常量缓冲区定义
cbuffer ModelViewProjectionConstantBuffer : register(b0)
{
    matrix model;
    matrix view;
    matrix projection;
};

// 顶点着色器输入结构
struct VertexInput
{
    float3 position : POSITION;
    float4 color : COLOR;
};

// 顶点着色器输出结构
struct VertexOutput
{
    float4 position : SV_POSITION;
    float4 color : COLOR;
};

// 基础顶点着色器
VertexOutput main(VertexInput input)
{
    VertexOutput output;
    
    // 转换顶点位置:从模型空间到世界空间,再到观察空间,最后到裁剪空间
    float4 pos = float4(input.position, 1.0f);
    pos = mul(pos, model);        // 模型空间 -> 世界空间
    pos = mul(pos, view);         // 世界空间 -> 观察空间
    pos = mul(pos, projection);   // 观察空间 -> 裁剪空间
    output.position = pos;
    
    // 传递顶点颜色
    output.color = input.color;
    
    return output;
}

6.4.2 包含纹理坐标的顶点着色器

如果模型使用纹理,顶点着色器需要传递纹理坐标:

hlsl

// 带有纹理坐标的顶点输入
struct VertexInputTex
{
    float3 position : POSITION;
    float2 texCoord : TEXCOORD0;
};

// 带有纹理坐标的顶点输出
struct VertexOutputTex
{
    float4 position : SV_POSITION;
    float2 texCoord : TEXCOORD0;
};

// 处理纹理坐标的顶点着色器
VertexOutputTex mainTex(VertexInputTex input)
{
    VertexOutputTex output;
    
    // 转换顶点位置
    float4 pos = float4(input.position, 1.0f);
    pos = mul(pos, model);
    pos = mul(pos, view);
    pos = mul(pos, projection);
    output.position = pos;
    
    // 传递纹理坐标
    output.texCoord = input.texCoord;
    
    return output;
}

6.4.3 包含光照计算的顶点着色器

对于需要光照的模型,顶点着色器需要处理法线和准备光照计算:

hlsl

// 带有法线的顶点输入
struct VertexInputLighting
{
    float3 position : POSITION;
    float3 normal : NORMAL;
    float2 texCoord : TEXCOORD0;
};

// 传递光照信息的顶点输出
struct VertexOutputLighting
{
    float4 position : SV_POSITION;
    float3 worldPosition : POSITION;
    float3 normal : NORMAL;
    float2 texCoord : TEXCOORD0;
};

// 光照顶点着色器
VertexOutputLighting mainLighting(VertexInputLighting input)
{
    VertexOutputLighting output;
    
    // 计算位置
    float4 pos = float4(input.position, 1.0f);
    
    // 保存世界空间位置(用于光照计算)
    float4 worldPos = mul(pos, model);
    output.worldPosition = worldPos.xyz;
    
    // 完成变换到裁剪空间
    pos = mul(worldPos, view);
    pos = mul(pos, projection);
    output.position = pos;
    
    // 将法线从模型空间转换到世界空间
    // 注意:正确做法应该使用模型矩阵的逆转置矩阵
    output.normal = normalize(mul(input.normal, (float3x3)model));
    
    // 传递纹理坐标
    output.texCoord = input.texCoord;
    
    return output;
}

6.4.4 顶点着色器中的蒙皮动画

对于带骨骼动画的模型,顶点着色器需要实现蒙皮计算:

hlsl

// 蒙皮动画常量缓冲区
cbuffer SkinningConstantBuffer : register(b1)
{
    matrix boneTransforms[100];   // 最多支持100个骨骼
};

// 带有蒙皮信息的顶点输入
struct VertexInputSkinned
{
    float3 position : POSITION;
    float3 normal : NORMAL;
    float2 texCoord : TEXCOORD0;
    uint4 boneIndices : BLENDINDICES0;
    float4 boneWeights : BLENDWEIGHT0;
};

// 支持蒙皮的顶点着色器
VertexOutputLighting mainSkinned(VertexInputSkinned input)
{
    VertexOutputLighting output;
    
    // 骨骼动画变换
    float4 pos = float4(input.position, 1.0f);
    float3 normal = input.normal;
    
    // 初始化为零
    float4 skinnedPos = float4(0, 0, 0, 0);
    float3 skinnedNormal = float3(0, 0, 0);
    
    // 计算每个骨骼的影响
    for (int i = 0; i < 4; i++)
    {
        uint boneIndex = input.boneIndices[i];
        float weight = input.boneWeights[i];
        
        if (weight > 0)
        {
            // 获取骨骼变换矩阵
            matrix boneTransform = boneTransforms[boneIndex];
            
            // 应用骨骼变换到位置
            float4 localPos = mul(pos, boneTransform);
            skinnedPos += localPos * weight;
            
            // 应用骨骼变换到法线
            float3 localNormal = mul(normal, (float3x3)boneTransform);
            skinnedNormal += localNormal * weight;
        }
    }
    
    // 计算世界空间位置
    float4 worldPos = mul(skinnedPos, model);
    output.worldPosition = worldPos.xyz;
    
    // 完成变换到裁剪空间
    float4 viewPos = mul(worldPos, view);
    output.position = mul(viewPos, projection);
    
    // 处理法线
    output.normal = normalize(mul(skinnedNormal, (float3x3)model));
    
    // 传递纹理坐标
    output.texCoord = input.texCoord;
    
    return output;
}

6.4.5 实例化顶点着色器

支持实例化绘制的顶点着色器需要处理每个实例的特定属性:

hlsl

// 实例数据结构(在每个顶点的基础上,每个实例有自己的这些属性)
struct InstanceData
{
    float4 row0 : WORLD0;
    float4 row1 : WORLD1;
    float4 row2 : WORLD2;
    float4 row3 : WORLD3;
    float4 color : COLOR1;
};

// 实例化顶点着色器输入
struct VertexInputInstanced
{
    float3 position : POSITION;
    float3 normal : NORMAL;
    float2 texCoord : TEXCOORD0;
    InstanceData instance : INSTANCE;  // 实例数据
};

// 实例化顶点着色器输出
struct VertexOutputInstanced
{
    float4 position : SV_POSITION;
    float3 normal : NORMAL;
    float2 texCoord : TEXCOORD0;
    float4 color : COLOR;
};

// 实例化顶点着色器
VertexOutputInstanced mainInstanced(VertexInputInstanced input)
{
    VertexOutputInstanced output;
    
    // 构建实例的世界矩阵
    matrix instanceWorld = {
        input.instance.row0,
        input.instance.row1,
        input.instance.row2,
        input.instance.row3
    };
    
    // 计算实例的最终变换矩阵
    matrix finalWorld = mul(model, instanceWorld);
    
    // 转换顶点位置
    float4 pos = float4(input.position, 1.0f);
    float4 worldPos = mul(pos, finalWorld);
    float4 viewPos = mul(worldPos, view);
    output.position = mul(viewPos, projection);
    
    // 转换法线
    output.normal = normalize(mul(input.normal, (float3x3)finalWorld));
    
    // 传递纹理坐标
    output.texCoord = input.texCoord;
    
    // 传递实例颜色
    output.color = input.instance.color;
    
    return output;
}

6.5 像素着色器示例

像素着色器(也称为片段着色器)在顶点着色器处理之后执行,负责计算每个像素的最终颜色。

6.5.1 基本像素着色器

最简单的像素着色器直接输出插值的顶点颜色:

hlsl

// 输入来自顶点着色器
struct PixelInput
{
    float4 position : SV_POSITION;
    float4 color : COLOR;
};

// 基本像素着色器
float4 main(PixelInput input) : SV_TARGET
{
    return input.color;
}

6.5.2 纹理采样像素着色器

使用纹理的像素着色器从纹理中采样颜色:

hlsl

// 纹理和采样器
Texture2D diffuseTexture : register(t0);
SamplerState textureSampler : register(s0);

// 纹理输入
struct PixelInputTex
{
    float4 position : SV_POSITION;
    float2 texCoord : TEXCOORD0;
};

// 纹理像素着色器
float4 mainTex(PixelInputTex input) : SV_TARGET
{
    // 从纹理中采样颜色
    return diffuseTexture.Sample(textureSampler, input.texCoord);
}

6.5.3 基本光照像素着色器

实现基础光照模型(环境光、漫反射和镜面反射)的像素着色器:

hlsl

// 光照常量缓冲区
cbuffer LightingConstantBuffer : register(b2)
{
    float3 ambientColor;
    float3 lightDirection;
    float3 lightColor;
    float3 cameraPosition;
}

// 材质常量缓冲区
cbuffer MaterialConstantBuffer : register(b3)
{
    float3 diffuseColor;
    float specularPower;
    float3 specularColor;
    float padding;
}

// 光照输入
struct PixelInputLighting
{
    float4 position : SV_POSITION;
    float3 worldPosition : POSITION;
    float3 normal : NORMAL;
    float2 texCoord : TEXCOORD0;
};

// 带光照的像素着色器
float4 mainLighting(PixelInputLighting input) : SV_TARGET
{
    // 标准化法线
    float3 normal = normalize(input.normal);
    
    // 环境光照
    float3 ambient = ambientColor * diffuseColor;
    
    // 漫反射光照(Lambert模型)
    float3 lightDir = normalize(-lightDirection);
    float NdotL = max(dot(normal, lightDir), 0.0);
    float3 diffuse = NdotL * lightColor * diffuseColor;
    
    // 镜面反射光照(Blinn-Phong模型)
    float3 viewDir = normalize(cameraPosition - input.worldPosition);
    float3 halfDir = normalize(lightDir + viewDir);
    float NdotH = max(dot(normal, halfDir), 0.0);
    float3 specular = pow(NdotH, specularPower) * specularColor * lightColor;
    
    // 组合所有光照成分
    float3 finalColor = ambient + diffuse + specular;
    
    return float4(finalColor, 1.0);
}

6.5.4 带纹理的光照像素着色器

结合纹理和光照的像素着色器:

hlsl

// 纹理
Texture2D diffuseTexture : register(t0);
Texture2D normalTexture : register(t1);
Texture2D specularTexture : register(t2);
SamplerState textureSampler : register(s0);

// 光照+纹理像素着色器
float4 mainTextureLighting(PixelInputLighting input) : SV_TARGET
{
    // 从纹理中采样
    float4 diffuseMap = diffuseTexture.Sample(textureSampler, input.texCoord);
    float4 specularMap = specularTexture.Sample(textureSampler, input.texCoord);
    
    // 从法线贴图中获取法线
    float3 normalMap = normalTexture.Sample(textureSampler, input.texCoord).rgb;
    normalMap = normalMap * 2.0 - 1.0;  // 从 [0,1] 转换到 [-1,1]
    
    // 计算 TBN 矩阵(简化版 - 实际应用中需要计算切线空间)
    float3 normal = normalize(input.normal);
    
    // 环境光照
    float3 ambient = ambientColor * diffuseMap.rgb;
    
    // 漫反射光照
    float3 lightDir = normalize(-lightDirection);
    float NdotL = max(dot(normal, lightDir), 0.0);
    float3 diffuse = NdotL * lightColor * diffuseMap.rgb;
    
    // 镜面反射光照
    float3 viewDir = normalize(cameraPosition - input.worldPosition);
    float3 halfDir = normalize(lightDir + viewDir);
    float NdotH = max(dot(normal, halfDir), 0.0);
    float3 specular = pow(NdotH, specularPower) * specularMap.rgb * lightColor;
    
    // 组合所有光照成分
    float3 finalColor = ambient + diffuse + specular;
    
    return float4(finalColor, diffuseMap.a);
}

6.5.5 PBR(基于物理的渲染)像素着色器

实现基于物理的渲染模型的像素着色器:

hlsl

// PBR纹理
Texture2D albedoTexture : register(t0);    // 反照率纹理
Texture2D normalTexture : register(t1);    // 法线纹理
Texture2D metallicTexture : register(t2);  // 金属度纹理
Texture2D roughnessTexture : register(t3); // 粗糙度纹理
Texture2D aoTexture : register(t4);        // 环境光遮蔽纹理
SamplerState textureSampler : register(s0);

// 环境纹理(简化的IBL)
TextureCube irradianceMap : register(t5);
TextureCube prefilterMap : register(t6);
Texture2D brdfLUT : register(t7);

// PI常量
static const float PI = 3.14159265359;

// PBR辅助函数
float DistributionGGX(float NdotH, float roughness)
{
    float a = roughness * roughness;
    float a2 = a * a;
    float NdotH2 = NdotH * NdotH;
    
    float nom = a2;
    float denom = (NdotH2 * (a2 - 1.0) + 1.0);
    denom = PI * denom * denom;
    
    return nom / max(denom, 0.001);
}

float GeometrySchlickGGX(float NdotV, float roughness)
{
    float r = roughness + 1.0;
    float k = (r * r) / 8.0;
    
    float nom = NdotV;
    float denom = NdotV * (1.0 - k) + k;
    
    return nom / max(denom, 0.001);
}

float GeometrySmith(float NdotV, float NdotL, float roughness)
{
    float ggx1 = GeometrySchlickGGX(NdotV, roughness);
    float ggx2 = GeometrySchlickGGX(NdotL, roughness);
    
    return ggx1 * ggx2;
}

float3 FresnelSchlick(float cosTheta, float3 F0)
{
    return F0 + (1.0 - F0) * pow(max(1.0 - cosTheta, 0.0), 5.0);
}

float3 FresnelSchlickRoughness(float cosTheta, float3 F0, float roughness)
{
    return F0 + (max(float3(1.0 - roughness, 1.0 - roughness, 1.0 - roughness), F0) - F0) 
           * pow(max(1.0 - cosTheta, 0.0), 5.0);
}

// PBR像素着色器
float4 mainPBR(PixelInputLighting input) : SV_TARGET
{
    // 采样纹理
    float3 albedo = albedoTexture.Sample(textureSampler, input.texCoord).rgb;
    float metallic = metallicTexture.Sample(textureSampler, input.texCoord).r;
    float roughness = roughnessTexture.Sample(textureSampler, input.texCoord).r;
    float ao = aoTexture.Sample(textureSampler, input.texCoord).r;
    
    // 法线映射(简化版)
    float3 N = normalize(input.normal);
    float3 V = normalize(cameraPosition - input.worldPosition);
    float3 L = normalize(-lightDirection);
    float3 H = normalize(V + L);
    float3 R = reflect(-V, N);
    
    // 计算输入数据
    float NdotV = max(dot(N, V), 0.0);
    float NdotL = max(dot(N, L), 0.0);
    float NdotH = max(dot(N, H), 0.0);
    float HdotV = max(dot(H, V), 0.0);
    
    // 菲涅尔反射率F0(非金属为0.04,金属使用反照率)
    float3 F0 = float3(0.04, 0.04, 0.04);
    F0 = lerp(F0, albedo, metallic);
    
    // Cook-Torrance BRDF
    float D = DistributionGGX(NdotH, roughness);
    float G = GeometrySmith(NdotV, NdotL, roughness);
    float3 F = FresnelSchlick(HdotV, F0);
    
    float3 numerator = D * G * F;
    float denominator = 4.0 * NdotV * NdotL + 0.001;
    float3 specular = numerator / denominator;
    
    // 能量守恒:金属表面不会有漫反射
    float3 kD = (1.0 - F) * (1.0 - metallic);
    
    // Lambert漫反射
    float3 diffuse = kD * albedo / PI;
    
    // 直接光照
    float3 directLighting = (diffuse + specular) * lightColor * NdotL;
    
    // 间接光照(环境光)- 简化的IBL
    float3 F_ambient = FresnelSchlickRoughness(NdotV, F0, roughness);
    float3 kD_ambient = (1.0 - F_ambient) * (1.0 - metallic);
    
    float3 irradiance = irradianceMap.Sample(textureSampler, N).rgb;
    float3 diffuseIBL = kD_ambient * albedo * irradiance;
    
    float mip = roughness * 5.0; // 假设预过滤贴图有6个mip级别
    float3 prefilteredColor = prefilterMap.SampleLevel(textureSampler, R, mip).rgb;
    float2 brdf = brdfLUT.Sample(textureSampler, float2(NdotV, roughness)).rg;
    float3 specularIBL = prefilteredColor * (F_ambient * brdf.x + brdf.y);
    
    float3 ambientLighting = (diffuseIBL + specularIBL) * ao;
    
    // 最终颜色
    float3 finalColor = directLighting + ambientLighting;
    
    // 伽马校正(假设输入为线性空间)
    finalColor = pow(finalColor, float3(1.0/2.2, 1.0/2.2, 1.0/2.2));
    
    return float4(finalColor, 1.0);
}

6.6 常量缓冲区

常量缓冲区用于将 CPU 侧的数据传递给着色器程序,例如变换矩阵、光照参数等。

6.6.1 创建常量缓冲区

在 DirectX 12 中,常量缓冲区需要按照 256 字节对齐规则创建:

cpp

// 常量缓冲区类
template<typename T>
class ConstantBuffer {
public:
    ConstantBuffer(ID3D12Device* device, UINT elementCount = 1) 
        : m_elementCount(elementCount) {
        
        // 常量缓冲区需要按 256 字节对齐
        m_alignedElementSize = (sizeof(T) + 255) & ~255;
        
        // 创建上传堆资源
        const UINT bufferSize = m_alignedElementSize * elementCount;
        
        CD3DX12_HEAP_PROPERTIES uploadHeapProps(D3D12_HEAP_TYPE_UPLOAD);
        CD3DX12_RESOURCE_DESC bufferDesc = CD3DX12_RESOURCE_DESC::Buffer(bufferSize);
        
        device->CreateCommittedResource(
            &uploadHeapProps,
            D3D12_HEAP_FLAG_NONE,
            &bufferDesc,
            D3D12_RESOURCE_STATE_GENERIC_READ,
            nullptr,
            IID_PPV_ARGS(&m_resource)
        );
        
        // 映射资源以便 CPU 可以写入数据
        CD3DX12_RANGE readRange(0, 0); // CPU 不需要读取
        m_resource->Map(0, &readRange, reinterpret_cast<void**>(&m_mappedData));
    }
    
    ~ConstantBuffer() {
        if (m_resource != nullptr) {
            m_resource->Unmap(0, nullptr);
        }
    }
    
    // 更新缓冲区数据
    void Update(const T& data, UINT elementIndex = 0) {
        if (elementIndex >= m_elementCount) {
            throw std::out_of_range("Element index out of range");
        }
        
        memcpy(m_mappedData + elementIndex * m_alignedElementSize, &data, sizeof(T));
    }
    
    // 获取 GPU 虚拟地址
    D3D12_GPU_VIRTUAL_ADDRESS GetGPUVirtualAddress(UINT elementIndex = 0) const {
        return m_resource->GetGPUVirtualAddress() + elementIndex * m_alignedElementSize;
    }
    
    // 获取资源
    ID3D12Resource* GetResource() const {
        return m_resource.Get();
    }
    
private:
    ComPtr<ID3D12Resource> m_resource;
    UINT8* m_mappedData = nullptr;
    UINT m_elementCount;
    UINT m_alignedElementSize;
};

6.6.2 更新常量缓冲区

下面是如何创建和更新常量缓冲区的示例:

cpp

// 变换矩阵常量缓冲区结构
struct TransformMatrices {
    XMMATRIX model;
    XMMATRIX view;
    XMMATRIX projection;
};

// 光照参数常量缓冲区结构
struct LightParameters {
    XMFLOAT4 ambientColor;
    XMFLOAT4 lightDirection;  // 使用 XMFLOAT4 以保持对齐
    XMFLOAT4 lightColor;
    XMFLOAT4 cameraPosition;
};

// 创建常量缓冲区
ConstantBuffer<TransformMatrices> transformCB(device);
ConstantBuffer<LightParameters> lightCB(device);

// 更新变换矩阵常量缓冲区
void UpdateTransformBuffer(const Camera& camera) {
    TransformMatrices matrices;
    
    // 计算矩阵(注意:DirectXMath 使用行主序,但 HLSL 使用列主序,所以需要转置)
    XMMATRIX model = XMMatrixIdentity();
    XMMATRIX view = camera.GetViewMatrix();
    XMMATRIX projection = camera.GetProjectionMatrix();
    
    // 转置矩阵以符合 HLSL 的期望
    matrices.model = XMMatrixTranspose(model);
    matrices.view = XMMatrixTranspose(view);
    matrices.projection = XMMatrixTranspose(projection);
    
    // 更新缓冲区
    transformCB.Update(matrices);
}

// 更新光照参数常量缓冲区
void UpdateLightBuffer(const LightSystem& lighting, const Camera& camera) {
    LightParameters params;
    
    // 设置环境光
    params.ambientColor = XMFLOAT4(0.1f, 0.1f, 0.1f, 1.0f);
    
    // 设置方向光
    DirectionalLight dirLight = lighting.GetMainLight();
    params.lightDirection = XMFLOAT4(dirLight.direction.x, dirLight.direction.y, dirLight.direction.z, 0.0f);
    params.lightColor = XMFLOAT4(dirLight.color.x, dirLight.color.y, dirLight.color.z, dirLight.intensity);
    
    // 设置相机位置
    XMFLOAT3 camPos = camera.GetPosition();
    params.cameraPosition = XMFLOAT4(camPos.x, camPos.y, camPos.z, 1.0f);
    
    // 更新缓冲区
    lightCB.Update(params);
}

6.6.3 上传缓冲区辅助函数

为了简化资源管理,可以创建一个通用的上传缓冲区类:

cpp

// 上传缓冲区类(用于各种数据上传)
class UploadBuffer {
public:
    UploadBuffer(ID3D12Device* device, UINT size, bool isConstantBuffer = false) : 
        m_isConstantBuffer(isConstantBuffer) {
        
        // 如果是常量缓冲区,需要按 256 字节对齐
        m_size = isConstantBuffer ? (size + 255) & ~255 : size;
        
        // 创建上传堆资源
        CD3DX12_HEAP_PROPERTIES uploadHeapProps(D3D12_HEAP_TYPE_UPLOAD);
        CD3DX12_RESOURCE_DESC bufferDesc = CD3DX12_RESOURCE_DESC::Buffer(m_size);
        
        device->CreateCommittedResource(
            &uploadHeapProps,
            D3D12_HEAP_FLAG_NONE,
            &bufferDesc,
            D3D12_RESOURCE_STATE_GENERIC_READ,
            nullptr,
            IID_PPV_ARGS(&m_resource)
        );
        
        // 映射资源
        CD3DX12_RANGE readRange(0, 0);
        m_resource->Map(0, &readRange, reinterpret_cast<void**>(&m_mappedData));
    }
    
    ~UploadBuffer() {
        if (m_resource != nullptr) {
            m_resource->Unmap(0, nullptr);
        }
    }
    
    // 复制数据到缓冲区
    void CopyData(UINT elementIndex, const void* data, UINT elementSize) {
        UINT destOffset = elementIndex * (m_isConstantBuffer ? ((elementSize + 255) & ~255) : elementSize);
        memcpy(m_mappedData + destOffset, data, elementSize);
    }
    
    // 获取 GPU 虚拟地址
    D3D12_GPU_VIRTUAL_ADDRESS GetGPUVirtualAddress(UINT elementIndex = 0, UINT elementSize = 0) const {
        UINT offset = elementIndex * (m_isConstantBuffer ? ((elementSize + 255) & ~255) : elementSize);
        return m_resource->GetGPUVirtualAddress() + offset;
    }
    
    // 获取资源
    ID3D12Resource* GetResource() const {
        return m_resource.Get();
    }
    
private:
    ComPtr<ID3D12Resource> m_resource;
    UINT8* m_mappedData = nullptr;
    UINT m_size;
    bool m_isConstantBuffer;
};

6.6.4 常量缓冲区描述符

Direct3D 12 使用描述符来绑定资源到渲染管线。对于常量缓冲区,可以通过两种方式设置:

描述符表:使用描述符堆和描述符表
根常量缓冲区视图:直接在根签名中设置

使用描述符表

cpp

// 创建 CBV 描述符堆
ComPtr<ID3D12DescriptorHeap> cbvHeap;
D3D12_DESCRIPTOR_HEAP_DESC cbvHeapDesc = {};
cbvHeapDesc.NumDescriptors = 2;  // 两个 CBV:变换矩阵和光照参数
cbvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
cbvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
device->CreateDescriptorHeap(&cbvHeapDesc, IID_PPV_ARGS(&cbvHeap));

// 创建常量缓冲区视图 (CBV)
D3D12_CONSTANT_BUFFER_VIEW_DESC cbvDesc = {};
cbvDesc.BufferLocation = transformCB.GetGPUVirtualAddress();
cbvDesc.SizeInBytes = (sizeof(TransformMatrices) + 255) & ~255;

// 获取 CBV 堆的 CPU 句柄
CD3DX12_CPU_DESCRIPTOR_HANDLE cbvHandle(cbvHeap->GetCPUDescriptorHandleForHeapStart());

// 创建变换矩阵 CBV
device->CreateConstantBufferView(&cbvDesc, cbvHandle);

// 移动到下一个描述符位置
cbvHandle.Offset(device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV));

// 创建光照参数 CBV
cbvDesc.BufferLocation = lightCB.GetGPUVirtualAddress();
cbvDesc.SizeInBytes = (sizeof(LightParameters) + 255) & ~255;
device->CreateConstantBufferView(&cbvDesc, cbvHandle);

// 在绘制命令中设置描述符堆和表
commandList->SetDescriptorHeaps(1, cbvHeap.GetAddressOf());
commandList->SetGraphicsRootDescriptorTable(0, cbvHeap->GetGPUDescriptorHandleForHeapStart());
使用根常量缓冲区视图

cpp

// 在绘制命令中直接设置常量缓冲区
commandList->SetGraphicsRootConstantBufferView(0, transformCB.GetGPUVirtualAddress());
commandList->SetGraphicsRootConstantBufferView(1, lightCB.GetGPUVirtualAddress());

6.6.5 根签名和描述符表

根签名定义了着色器程序期望的资源类型和布局:

cpp

// 创建根签名
void CreateRootSignature(ID3D12Device* device, ID3D12RootSignature** rootSignature) {
    // 创建根参数
    CD3DX12_ROOT_PARAMETER rootParameters[2];
    
    // 参数 0:变换矩阵常量缓冲区(寄存器 b0)
    rootParameters[0].InitAsConstantBufferView(0);
    
    // 参数 1:光照参数常量缓冲区(寄存器 b1)
    rootParameters[1].InitAsConstantBufferView(1);
    
    // 创建静态采样器
    CD3DX12_STATIC_SAMPLER_DESC samplerDesc(0);
    
    // 创建根签名描述
    CD3DX12_ROOT_SIGNATURE_DESC rootSignatureDesc;
    rootSignatureDesc.Init(
        _countof(rootParameters), rootParameters,
        1, &samplerDesc,
        D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT
    );
    
    // 序列化根签名
    ComPtr<ID3DBlob> signature;
    ComPtr<ID3DBlob> error;
    D3D12SerializeRootSignature(
        &rootSignatureDesc, 
        D3D_ROOT_SIGNATURE_VERSION_1,
        &signature,
        &error
    );
    
    if (error != nullptr) {
        OutputDebugStringA((char*)error->GetBufferPointer());
        throw std::runtime_error("Failed to serialize root signature");
    }
    
    // 创建根签名
    device->CreateRootSignature(
        0,
        signature->GetBufferPointer(),
        signature->GetBufferSize(),
        IID_PPV_ARGS(rootSignature)
    );
}

通过这种方式,可以将常量缓冲区有效地绑定到渲染管线,使着色器程序能够访问 CPU 提供的数据。

6.7 编译着色器

为了使用着色器,需要将 HLSL 代码编译成 GPU 可以执行的字节码。有多种方法可以编译着色器:离线编译、运行时编译,或使用 Visual Studio 集成的编译功能。

6.7.1 离线编译

离线编译着色器是指在应用程序构建过程中预先编译着色器,而不是在运行时编译。这通常使用 DirectX 附带的着色器编译器工具(fxc.exe 或更新的 dxc.exe)完成。

使用 FXC 编译着色器

reasonml

// 命令行使用 fxc.exe 编译顶点着色器
fxc.exe /T vs_5_0 /E VSMain /Fo VertexShader.cso VertexShader.hlsl

// 编译像素着色器
fxc.exe /T ps_5_0 /E PSMain /Fo PixelShader.cso PixelShader.hlsl

选项说明:

/T vs_5_0 – 指定着色器类型和版本(顶点着色器 5.0)
/E VSMain – 指定入口点函数名
/Fo VertexShader.cso – 指定输出文件名
VertexShader.hlsl – 输入源文件

使用 DXC 编译着色器(支持 Shader Model 6.x)

reasonml

// 使用新的 dxc.exe 编译器
dxc.exe -T vs_6_0 -E VSMain -Fo VertexShader.cso VertexShader.hlsl

// 启用优化
dxc.exe -T ps_6_0 -E PSMain -O3 -Fo PixelShader.cso PixelShader.hlsl

6.7.2 生成着色器汇编代码

查看编译后的着色器代码对于理解和优化着色器很有帮助:

reasonml

// 生成可读的着色器汇编代码
fxc.exe /T vs_5_0 /E VSMain /Fc VertexShader.asm VertexShader.hlsl

// 或使用 DXC
dxc.exe -T vs_6_0 -E VSMain -Fc VertexShader.asm VertexShader.hlsl

通过检查汇编代码,可以了解着色器的指令序列、寄存器使用情况和潜在的性能问题。

6.7.3 在 C++ 代码中加载编译好的着色器

预编译的着色器需要在运行时加载并创建着色器对象:

cpp

// 从文件加载编译好的着色器
std::vector<uint8_t> LoadShaderFromFile(const std::wstring& filename) {
    std::ifstream file(filename, std::ios::binary | std::ios::ate);
    if (!file.is_open()) {
        throw std::runtime_error("Failed to open shader file: " + std::string(filename.begin(), filename.end()));
    }
    
    size_t fileSize = static_cast<size_t>(file.tellg());
    std::vector<uint8_t> buffer(fileSize);
    
    file.seekg(0);
    file.read(reinterpret_cast<char*>(buffer.data()), fileSize);
    file.close();
    
    return buffer;
}

// 加载并使用着色器
void LoadShaders() {
    // 加载顶点着色器
    std::vector<uint8_t> vertexShaderData = LoadShaderFromFile(L"VertexShader.cso");
    
    // 创建顶点着色器
    D3D12_SHADER_BYTECODE vertexShaderBytecode = {};
    vertexShaderBytecode.pShaderBytecode = vertexShaderData.data();
    vertexShaderBytecode.BytecodeLength = vertexShaderData.size();
    
    // 加载像素着色器
    std::vector<uint8_t> pixelShaderData = LoadShaderFromFile(L"PixelShader.cso");
    
    // 创建像素着色器
    D3D12_SHADER_BYTECODE pixelShaderBytecode = {};
    pixelShaderBytecode.pShaderBytecode = pixelShaderData.data();
    pixelShaderBytecode.BytecodeLength = pixelShaderData.size();
    
    // 使用着色器创建流水线状态
    D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc = {};
    // 设置其他 PSO 参数...
    psoDesc.VS = vertexShaderBytecode;
    psoDesc.PS = pixelShaderBytecode;
    
    device->CreateGraphicsPipelineState(&psoDesc, IID_PPV_ARGS(&pipelineState));
}

6.7.4 运行时编译着色器

在某些情况下,需要在运行时编译着色器,例如为了支持动态生成的着色器代码或用户自定义的着色器:

cpp

// 运行时编译着色器
ComPtr<ID3DBlob> CompileShader(
    const std::wstring& filename,
    const std::string& entryPoint,
    const std::string& target,
    const D3D_SHADER_MACRO* defines = nullptr) {
    
    UINT compileFlags = 0;
#if defined(DEBUG) || defined(_DEBUG)
    // 在调试构建中启用调试标志
    compileFlags |= D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION;
#else
    // 在发布构建中启用优化
    compileFlags |= D3DCOMPILE_OPTIMIZATION_LEVEL3;
#endif
    
    // 加载和编译着色器
    ComPtr<ID3DBlob> shaderBlob;
    ComPtr<ID3DBlob> errorBlob;
    HRESULT hr = D3DCompileFromFile(
        filename.c_str(),
        defines,
        D3D_COMPILE_STANDARD_FILE_INCLUDE,
        entryPoint.c_str(),
        target.c_str(),
        compileFlags,
        0,
        &shaderBlob,
        &errorBlob
    );
    
    // 处理编译错误
    if (FAILED(hr)) {
        if (errorBlob) {
            OutputDebugStringA(static_cast<char*>(errorBlob->GetBufferPointer()));
            throw std::runtime_error(static_cast<char*>(errorBlob->GetBufferPointer()));
        }
        throw std::runtime_error("Failed to compile shader: " + std::to_string(hr));
    }
    
    return shaderBlob;
}

// 使用运行时编译着色器
void CompileAndCreateShaders() {
    // 编译顶点着色器
    ComPtr<ID3DBlob> vertexShaderBlob = CompileShader(
        L"Shaders.hlsl",
        "VSMain",
        "vs_5_0"
    );
    
    // 编译像素着色器
    ComPtr<ID3DBlob> pixelShaderBlob = CompileShader(
        L"Shaders.hlsl",
        "PSMain",
        "ps_5_0"
    );
    
    // 创建流水线状态
    D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc = {};
    // 设置其他参数...
    psoDesc.VS = CD3DX12_SHADER_BYTECODE(vertexShaderBlob.Get());
    psoDesc.PS = CD3DX12_SHADER_BYTECODE(pixelShaderBlob.Get());
    
    device->CreateGraphicsPipelineState(&psoDesc, IID_PPV_ARGS(&pipelineState));
}

6.7.5 利用 Visual Studio 离线编译着色器

Visual Studio 提供了内置的 HLSL 编译支持,可以通过调整项目设置为 HLSL 文件自动配置编译选项:

在 Visual Studio 项目中加入 .hlsl 文件
右键点击该文件,选择”属性”
将”Item Type”设置为”HLSL Compiler”
设置”Shader Type”为对应的着色器类型(如 Vertex Shader, Pixel Shader)
设置”Shader Model”为所需的版本(如 5.0, 6.0 等)
设置”Entry Point Name”为着色器入口函数名
设置”Output File”为所需的输出文件名

这样,当编译项目时,Visual Studio 会自动编译 HLSL 文件并将结果放在指定位置。

6.8 光栅器状态

光栅器阶段将几何图元转换为屏幕上的像素。光栅器状态定义了此阶段的各种行为,如填充模式、剔除模式和深度裁剪等。

6.8.1 配置光栅器状态

在 Direct3D 12 中,光栅器状态是流水线状态对象的一部分:

cpp

// 创建默认光栅器状态
D3D12_RASTERIZER_DESC rasterizerDesc = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT);

// 自定义光栅器状态
D3D12_RASTERIZER_DESC customRasterizerDesc = {};
customRasterizerDesc.FillMode = D3D12_FILL_MODE_SOLID;     // 实体填充模式
customRasterizerDesc.CullMode = D3D12_CULL_MODE_BACK;      // 背面剔除
customRasterizerDesc.FrontCounterClockwise = FALSE;        // 顺时针为正面
customRasterizerDesc.DepthBias = 0;                        // 无深度偏移
customRasterizerDesc.DepthBiasClamp = 0.0f;
customRasterizerDesc.SlopeScaledDepthBias = 0.0f;
customRasterizerDesc.DepthClipEnable = TRUE;               // 启用深度裁剪
customRasterizerDesc.MultisampleEnable = FALSE;            // 禁用多重采样
customRasterizerDesc.AntialiasedLineEnable = FALSE;        // 禁用线条抗锯齿
customRasterizerDesc.ForcedSampleCount = 0;
customRasterizerDesc.ConservativeRaster = D3D12_CONSERVATIVE_RASTERIZATION_MODE_OFF;

// 线框渲染模式
D3D12_RASTERIZER_DESC wireframeDesc = customRasterizerDesc;
wireframeDesc.FillMode = D3D12_FILL_MODE_WIREFRAME;

// 设置光栅器状态到 PSO 描述符
psoDesc.RasterizerState = customRasterizerDesc;

6.8.2 不同剔除模式的效果

光栅器状态的剔除模式决定了哪些三角形面会被绘制:

cpp

// 无剔除模式 - 绘制所有三角形
D3D12_RASTERIZER_DESC noCullDesc = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT);
noCullDesc.CullMode = D3D12_CULL_MODE_NONE;

// 背面剔除 - 只绘制面向观察者的三角形(默认)
D3D12_RASTERIZER_DESC backCullDesc = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT);
backCullDesc.CullMode = D3D12_CULL_MODE_BACK;

// 正面剔除 - 只绘制背向观察者的三角形(适用于特殊效果)
D3D12_RASTERIZER_DESC frontCullDesc = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT);
frontCullDesc.CullMode = D3D12_CULL_MODE_FRONT;

6.8.3 深度偏差应用

深度偏差可用于解决Z-fighting(深度冲突)问题,或实现阴影映射等效果:

cpp

// 阴影映射用的深度偏移
D3D12_RASTERIZER_DESC shadowMapDesc = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT);
shadowMapDesc.DepthBias = 1000;                    // 常量深度偏移
shadowMapDesc.DepthBiasClamp = 0.0f;               // 深度偏移不超过此值
shadowMapDesc.SlopeScaledDepthBias = 1.0f;         // 根据表面斜率调整的深度偏移

6.9 流水线状态对象

流水线状态对象(Pipeline State Object, PSO)封装了渲染流水线的所有状态,包括着色器、输入布局、光栅器状态、混合状态等。

6.9.1 创建基本的流水线状态对象

cpp

// 创建基本的 PSO
ComPtr<ID3D12PipelineState> CreateBasicPSO(
    ID3D12Device* device,
    ID3D12RootSignature* rootSignature,
    const D3D12_SHADER_BYTECODE& vertexShader,
    const D3D12_SHADER_BYTECODE& pixelShader,
    const std::vector<D3D12_INPUT_ELEMENT_DESC>& inputLayout) {
    
    ComPtr<ID3D12PipelineState> pipelineState;
    
    // 创建 PSO 描述
    D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc = {};
    
    // 设置着色器
    psoDesc.VS = vertexShader;
    psoDesc.PS = pixelShader;
    
    // 设置混合状态
    psoDesc.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT);
    
    // 设置光栅器状态
    psoDesc.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT);
    
    // 设置深度模板状态
    psoDesc.DepthStencilState = CD3DX12_DEPTH_STENCIL_DESC(D3D12_DEFAULT);
    
    // 设置输入布局
    psoDesc.InputLayout = { inputLayout.data(), static_cast<UINT>(inputLayout.size()) };
    
    // 设置根签名
    psoDesc.pRootSignature = rootSignature;
    
    // 设置图元类型
    psoDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
    
    // 设置 MSAA 采样数
    psoDesc.SampleMask = UINT_MAX;
    psoDesc.SampleDesc.Count = 1;
    psoDesc.SampleDesc.Quality = 0;
    
    // 设置渲染目标格式和数量
    psoDesc.NumRenderTargets = 1;
    psoDesc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM;
    
    // 设置深度模板格式
    psoDesc.DSVFormat = DXGI_FORMAT_D24_UNORM_S8_UINT;
    
    // 创建 PSO
    device->CreateGraphicsPipelineState(&psoDesc, IID_PPV_ARGS(&pipelineState));
    
    return pipelineState;
}

6.9.2 创建线框渲染 PSO

cpp

// 创建线框渲染 PSO
ComPtr<ID3D12PipelineState> CreateWireframePSO(
    ID3D12Device* device,
    ID3D12RootSignature* rootSignature,
    const D3D12_SHADER_BYTECODE& vertexShader,
    const D3D12_SHADER_BYTECODE& pixelShader,
    const std::vector<D3D12_INPUT_ELEMENT_DESC>& inputLayout) {
    
    ComPtr<ID3D12PipelineState> pipelineState;
    
    // 创建 PSO 描述(基于上面的基本 PSO)
    D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc = {};
    // ... 设置与基本 PSO 相同的大多数参数
    
    // 设置线框光栅器状态
    D3D12_RASTERIZER_DESC wireframeRasterizerDesc = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT);
    wireframeRasterizerDesc.FillMode = D3D12_FILL_MODE_WIREFRAME;
    psoDesc.RasterizerState = wireframeRasterizerDesc;
    
    // 创建 PSO
    device->CreateGraphicsPipelineState(&psoDesc, IID_PPV_ARGS(&pipelineState));
    
    return pipelineState;
}

6.9.3 透明渲染 PSO

cpp

// 创建透明渲染 PSO
ComPtr<ID3D12PipelineState> CreateTransparentPSO(
    ID3D12Device* device,
    ID3D12RootSignature* rootSignature,
    const D3D12_SHADER_BYTECODE& vertexShader,
    const D3D12_SHADER_BYTECODE& pixelShader,
    const std::vector<D3D12_INPUT_ELEMENT_DESC>& inputLayout) {
    
    ComPtr<ID3D12PipelineState> pipelineState;
    
    // 创建 PSO 描述(基于基本 PSO)
    D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc = {};
    // ... 设置与基本 PSO 相同的大多数参数
    
    // 设置混合状态为 Alpha 混合
    D3D12_BLEND_DESC transparentBlendDesc = {};
    transparentBlendDesc.AlphaToCoverageEnable = FALSE;
    transparentBlendDesc.IndependentBlendEnable = FALSE;
    
    D3D12_RENDER_TARGET_BLEND_DESC rtBlendDesc = {};
    rtBlendDesc.BlendEnable = TRUE;
    rtBlendDesc.LogicOpEnable = FALSE;
    rtBlendDesc.SrcBlend = D3D12_BLEND_SRC_ALPHA;
    rtBlendDesc.DestBlend = D3D12_BLEND_INV_SRC_ALPHA;
    rtBlendDesc.BlendOp = D3D12_BLEND_OP_ADD;
    rtBlendDesc.SrcBlendAlpha = D3D12_BLEND_ONE;
    rtBlendDesc.DestBlendAlpha = D3D12_BLEND_ZERO;
    rtBlendDesc.BlendOpAlpha = D3D12_BLEND_OP_ADD;
    rtBlendDesc.LogicOp = D3D12_LOGIC_OP_NOOP;
    rtBlendDesc.RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_ALL;
    
    transparentBlendDesc.RenderTarget[0] = rtBlendDesc;
    psoDesc.BlendState = transparentBlendDesc;
    
    // 关闭深度写入(透明物体只读取深度,不写入)
    D3D12_DEPTH_STENCIL_DESC transparentDSDesc = CD3DX12_DEPTH_STENCIL_DESC(D3D12_DEFAULT);
    transparentDSDesc.DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ZERO;
    psoDesc.DepthStencilState = transparentDSDesc;
    
    // 创建 PSO
    device->CreateGraphicsPipelineState(&psoDesc, IID_PPV_ARGS(&pipelineState));
    
    return pipelineState;
}

6.10 几何图形辅助结构体

为了简化几何体的创建和管理,可以实现一些辅助类和函数:

cpp

// 几何体结构
struct MeshGeometry {
    std::string Name;
    
    // 资源
    ComPtr<ID3D12Resource> VertexBuffer;
    ComPtr<ID3D12Resource> IndexBuffer;
    ComPtr<ID3D12Resource> VertexUploadBuffer;
    ComPtr<ID3D12Resource> IndexUploadBuffer;
    
    // 视图
    D3D12_VERTEX_BUFFER_VIEW VertexBufferView;
    D3D12_INDEX_BUFFER_VIEW IndexBufferView;
    
    // 几何信息
    UINT VertexCount;
    UINT VertexStride;
    UINT IndexCount;
    DXGI_FORMAT IndexFormat;
    
    // 子网格(一个几何体可能包含多个子网格)
    struct Submesh {
        UINT IndexCount;
        UINT StartIndexLocation;
        UINT BaseVertexLocation;
        // 边界盒等其他数据
    };
    std::unordered_map<std::string, Submesh> Submeshes;
    
    // 绘制函数
    void Draw(ID3D12GraphicsCommandList* cmdList) {
        // 设置顶点和索引缓冲区
        cmdList->IASetVertexBuffers(0, 1, &VertexBufferView);
        cmdList->IASetIndexBuffer(&IndexBufferView);
        
        // 绘制整个几何体
        cmdList->DrawIndexedInstanced(IndexCount, 1, 0, 0, 0);
    }
    
    // 绘制子网格
    void DrawSubmesh(ID3D12GraphicsCommandList* cmdList, const std::string& submeshName) {
        auto submesh = Submeshes.find(submeshName);
        if (submesh != Submeshes.end()) {
            cmdList->IASetVertexBuffers(0, 1, &VertexBufferView);
            cmdList->IASetIndexBuffer(&IndexBufferView);
            
            cmdList->DrawIndexedInstanced(
                submesh->second.IndexCount,
                1,
                submesh->second.StartIndexLocation,
                submesh->second.BaseVertexLocation,
                0
            );
        }
    }
};

// 基础几何体工厂
class GeometryFactory {
public:
    // 创建一个立方体
    static MeshGeometry CreateBox(
        ID3D12Device* device,
        float width,
        float height,
        float depth) {
        
        // 顶点数据
        struct Vertex {
            XMFLOAT3 Position;
            XMFLOAT3 Normal;
            XMFLOAT2 TexCoord;
        };
        
        // 生成立方体的 8 个顶点
        std::vector<Vertex> vertices(24);  // 立方体有 6 个面,每个面 4 个唯一顶点
        
        float w2 = width / 2.0f;
        float h2 = height / 2.0f;
        float d2 = depth / 2.0f;
        
        // 前面 (+z)
        vertices[0] = {
           { -w2, -h2, d2 }, { 0.0f, 0.0f, 1.0f }, { 0.0f, 1.0f }};
        vertices[1] = {
           { -w2, h2, d2 }, { 0.0f, 0.0f, 1.0f }, { 0.0f, 0.0f }};
        vertices[2] = {
           { w2, h2, d2 }, { 0.0f, 0.0f, 1.0f }, { 1.0f, 0.0f }};
        vertices[3] = {
           { w2, -h2, d2 }, { 0.0f, 0.0f, 1.0f }, { 1.0f, 1.0f }};
        
        // 后面 (-z)
        vertices[4] = {
           { w2, -h2, -d2 }, { 0.0f, 0.0f, -1.0f }, { 0.0f, 1.0f }};
        vertices[5] = {
           { w2, h2, -d2 }, { 0.0f, 0.0f, -1.0f }, { 0.0f, 0.0f }};
        vertices[6] = {
           { -w2, h2, -d2 }, { 0.0f, 0.0f, -1.0f }, { 1.0f, 0.0f }};
        vertices[7] = {
           { -w2, -h2, -d2 }, { 0.0f, 0.0f, -1.0f }, { 1.0f, 1.0f }};
        
        // 左面 (-x)
        vertices[8] = {
           { -w2, -h2, -d2 }, { -1.0f, 0.0f, 0.0f }, { 0.0f, 1.0f }};
        vertices[9] = {
           { -w2, h2, -d2 }, { -1.0f, 0.0f, 0.0f }, { 0.0f, 0.0f }};
        vertices[10] = {
           { -w2, h2, d2 }, { -1.0f, 0.0f, 0.0f }, { 1.0f, 0.0f }};
        vertices[11] = {
           { -w2, -h2, d2 }, { -1.0f, 0.0f, 0.0f }, { 1.0f, 1.0f }};
        
        // 右面 (+x)
        vertices[12] = {
           { w2, -h2, d2 }, { 1.0f, 0.0f, 0.0f }, { 0.0f, 1.0f }};
        vertices[13] = {
           { w2, h2, d2 }, { 1.0f, 0.0f, 0.0f }, { 0.0f, 0.0f }};
        vertices[14] = {
           { w2, h2, -d2 }, { 1.0f, 0.0f, 0.0f }, { 1.0f, 0.0f }};
        vertices[15] = {
           { w2, -h2, -d2 }, { 1.0f, 0.0f, 0.0f }, { 1.0f, 1.0f }};
        
        // 上面 (+y)
        vertices[16] = {
           { -w2, h2, d2 }, { 0.0f, 1.0f, 0.0f }, { 0.0f, 1.0f }};
        vertices[17] = {
           { -w2, h2, -d2 }, { 0.0f, 1.0f, 0.0f }, { 0.0f, 0.0f }};
        vertices[18] = {
           { w2, h2, -d2 }, { 0.0f, 1.0f, 0.0f }, { 1.0f, 0.0f }};
        vertices[19] = {
           { w2, h2, d2 }, { 0.0f, 1.0f, 0.0f }, { 1.0f, 1.0f }};
        
        // 下面 (-y)
        vertices[20] = {
           { -w2, -h2, -d2 }, { 0.0f, -1.0f, 0.0f }, { 0.0f, 1.0f }};
        vertices[21] = {
           { -w2, -h2, d2 }, { 0.0f, -1.0f, 0.0f }, { 0.0f, 0.0f }};
        vertices[22] = {
           { w2, -h2, d2 }, { 0.0f, -1.0f, 0.0f }, { 1.0f, 0.0f }};
        vertices[23] = {
           { w2, -h2, -d2 }, { 0.0f, -1.0f, 0.0f }, { 1.0f, 1.0f }};
        
        // 索引数据 (6 个面,每个面 2 个三角形,每个三角形 3 个索引)
        std::vector<uint16_t> indices = {
            // 前面
            0, 1, 2, 0, 2, 3,
            // 后面
            4, 5, 6, 4, 6, 7,
            // 左面
            8, 9, 10, 8, 10, 11,
            // 右面
            12, 13, 14, 12, 14, 15,
            // 上面
            16, 17, 18, 16, 18, 19,
            // 下面
            20, 21, 22, 20, 22, 23
        };
        
        // 创建几何体结构
        MeshGeometry geo;
        geo.Name = "Box";
        geo.VertexCount = static_cast<UINT>(vertices.size());
        geo.VertexStride = sizeof(Vertex);
        geo.IndexCount = static_cast<UINT>(indices.size());
        geo.IndexFormat = DXGI_FORMAT_R16_UINT;
        
        // 创建顶点和索引缓冲区
        const UINT vbSize = geo.VertexCount * geo.VertexStride;
        const UINT ibSize = geo.IndexCount * sizeof(uint16_t);
        
        // 创建顶点缓冲区资源
        CD3DX12_HEAP_PROPERTIES defaultHeapProps(D3D12_HEAP_TYPE_DEFAULT);
        CD3DX12_RESOURCE_DESC vbDesc = CD3DX12_RESOURCE_DESC::Buffer(vbSize);
        
        device->CreateCommittedResource(
            &defaultHeapProps,
            D3D12_HEAP_FLAG_NONE,
            &vbDesc,
            D3D12_RESOURCE_STATE_COMMON,
            nullptr,
            IID_PPV_ARGS(&geo.VertexBuffer)
        );
        
        // 创建索引缓冲区资源
        CD3DX12_RESOURCE_DESC ibDesc = CD3DX12_RESOURCE_DESC::Buffer(ibSize);
        
        device->CreateCommittedResource(
            &defaultHeapProps,
            D3D12_HEAP_FLAG_NONE,
            &ibDesc,
            D3D12_RESOURCE_STATE_COMMON,
            nullptr,
            IID_PPV_ARGS(&geo.IndexBuffer)
        );
        
        // 创建上传缓冲区
        CD3DX12_HEAP_PROPERTIES uploadHeapProps(D3D12_HEAP_TYPE_UPLOAD);
        
        device->CreateCommittedResource(
            &uploadHeapProps,
            D3D12_HEAP_FLAG_NONE,
            &vbDesc,
            D3D12_RESOURCE_STATE_GENERIC_READ,
            nullptr,
            IID_PPV_ARGS(&geo.VertexUploadBuffer)
        );
        
        device->CreateCommittedResource(
            &uploadHeapProps,
            D3D12_HEAP_FLAG_NONE,
            &ibDesc,
            D3D12_RESOURCE_STATE_GENERIC_READ,
            nullptr,
            IID_PPV_ARGS(&geo.IndexUploadBuffer)
        );
        
        // 填充上传缓冲区
        void* vMappedData = nullptr;
        CD3DX12_RANGE vReadRange(0, 0);
        geo.VertexUploadBuffer->Map(0, &vReadRange, &vMappedData);
        memcpy(vMappedData, vertices.data(), vbSize);
        geo.VertexUploadBuffer->Unmap(0, nullptr);
        
        void* iMappedData = nullptr;
        CD3DX12_RANGE iReadRange(0, 0);
        geo.IndexUploadBuffer->Map(0, &iReadRange, &iMappedData);
        memcpy(iMappedData, indices.data(), ibSize);
        geo.IndexUploadBuffer->Unmap(0, nullptr);
        
        // 设置视图
        geo.VertexBufferView.BufferLocation = geo.VertexBuffer->GetGPUVirtualAddress();
        geo.VertexBufferView.SizeInBytes = vbSize;
        geo.VertexBufferView.StrideInBytes = geo.VertexStride;
        
        geo.IndexBufferView.BufferLocation = geo.IndexBuffer->GetGPUVirtualAddress();
        geo.IndexBufferView.SizeInBytes = ibSize;
        geo.IndexBufferView.Format = geo.IndexFormat;
        
        // 添加一个子网格(整个立方体)
        MeshGeometry::Submesh submesh;
        submesh.IndexCount = geo.IndexCount;
        submesh.StartIndexLocation = 0;
        submesh.BaseVertexLocation = 0;
        geo.Submeshes["Box"] = submesh;
        
        return geo;
    }
    
    // 复制数据命令
    static void CopyVertexAndIndexBuffers(
        ID3D12GraphicsCommandList* cmdList,
        MeshGeometry& geo) {
        
        // 将顶点缓冲区从上传堆复制到默认堆
        cmdList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(
            geo.VertexBuffer.Get(),
            D3D12_RESOURCE_STATE_COMMON,
            D3D12_RESOURCE_STATE_COPY_DEST
        ));
        
        cmdList->CopyBufferRegion(
            geo.VertexBuffer.Get(), 0,
            geo.VertexUploadBuffer.Get(), 0,
            geo.VertexCount * geo.VertexStride
        );
        
        cmdList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(
            geo.VertexBuffer.Get(),
            D3D12_RESOURCE_STATE_COPY_DEST,
            D3D12_RESOURCE_STATE_VERTEX_AND_CONSTANT_BUFFER
        ));
        
        // 将索引缓冲区从上传堆复制到默认堆
        cmdList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(
            geo.IndexBuffer.Get(),
            D3D12_RESOURCE_STATE_COMMON,
            D3D12_RESOURCE_STATE_COPY_DEST
        ));
        
        cmdList->CopyBufferRegion(
            geo.IndexBuffer.Get(), 0,
            geo.IndexUploadBuffer.Get(), 0,
            geo.IndexCount * (geo.IndexFormat == DXGI_FORMAT_R16_UINT ? 2 : 4)
        );
        
        cmdList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(
            geo.IndexBuffer.Get(),
            D3D12_RESOURCE_STATE_COPY_DEST,
            D3D12_RESOURCE_STATE_INDEX_BUFFER
        ));
    }
    
    // 其他基础几何体生成函数(球体、圆柱体等)
    // ...
};

6.11 立方体演示程序

下面是一个完整的立方体渲染示例,展示了如何使用上述技术实现一个旋转的立方体:

cpp

// 简化的立方体演示程序
class CubeDemo {
public:
    CubeDemo(HWND hwnd, int width, int height)
        : m_hwnd(hwnd), m_width(width), m_height(height) {
        // 初始化 Direct3D
        InitD3D();
        
        // 创建命令对象
        CreateCommandObjects();
        
        // 创建交换链
        CreateSwapChain();
        
        // 创建描述符堆
        CreateDescriptorHeaps();
        
        // 创建渲染目标视图
        CreateRenderTargetViews();
        
        // 创建深度缓冲区
        CreateDepthStencilBuffer();
        
        // 创建根签名
        CreateRootSignature();
        
        // 编译着色器
        CompileShaders();
        
        // 创建几何体
        CreateGeometry();
        
        // 创建常量缓冲区
        CreateConstantBuffers();
        
        // 创建流水线状态
        CreatePipelineState();
        
        // 等待初始化完成
        WaitForGpu();
    }
    
    ~CubeDemo() {
        // 释放资源
        WaitForGpu();
    }
    
    // 更新和渲染
    void Update(float deltaTime) {
        // 更新旋转角度
        m_rotationAngle += deltaTime;
        
        // 计算世界-视图-投影矩阵
        XMMATRIX world = XMMatrixRotationY(m_rotationAngle);
        XMMATRIX view = XMMatrixLookAtLH(
            XMVectorSet(0.0f, 3.0f, -5.0f, 1.0f),  // 相机位置
            XMVectorSet(0.0f, 0.0f, 0.0f, 1.0f),   // 相机目标
            XMVectorSet(0.0f, 1.0f, 0.0f, 1.0f)    // 相机向上方向
        );
        XMMATRIX proj = XMMatrixPerspectiveFovLH(
            XM_PIDIV4,                         // 视场角 (45 度)
            static_cast<float>(m_width) / m_height,  // 屏幕宽高比
            0.1f,                              // 近裁剪面
            100.0f                             // 远裁剪面
        );
        
        // 在 HLSL 中矩阵使用列优先,需要转置
        XMMATRIX worldViewProj = XMMatrixTranspose(world * view * proj);
        
        // 更新常量缓冲区
        TransformMatrices matrices;
        XMStoreFloat4x4(&matrices.worldViewProj, worldViewProj);
        m_transformCB->Update(matrices);
    }
    
    void Render() {
        // 重置命令分配器和命令列表
        m_commandAllocator->Reset();
        m_commandList->Reset(m_commandAllocator.Get(), m_pipelineState.Get());
        
        // 设置视口和裁剪矩形
        m_commandList->RSSetViewports(1, &m_viewport);
        m_commandList->RSSetScissorRects(1, &m_scissorRect);
        
        // 准备渲染目标
        UINT frameIndex = m_swapChain->GetCurrentBackBufferIndex();
        
        // 从呈现状态转换到渲染目标状态
        m_commandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(
            m_renderTargets[frameIndex].Get(),
            D3D12_RESOURCE_STATE_PRESENT,
            D3D12_RESOURCE_STATE_RENDER_TARGET
        ));
        
        // 获取当前后台缓冲和深度模板视图
        CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(
            m_rtvHeap->GetCPUDescriptorHandleForHeapStart(),
            frameIndex,
            m_rtvDescriptorSize
        );
        CD3DX12_CPU_DESCRIPTOR_HANDLE dsvHandle(
            m_dsvHeap->GetCPUDescriptorHandleForHeapStart()
        );
        
        // 清除渲染目标和深度缓冲区
        const float clearColor[] = { 0.0f, 0.2f, 0.4f, 1.0f };
        m_commandList->ClearRenderTargetView(rtvHandle, clearColor, 0, nullptr);
        m_commandList->ClearDepthStencilView(dsvHandle, D3D12_CLEAR_FLAG_DEPTH, 1.0f, 0, 0, nullptr);
        
        // 设置渲染目标
        m_commandList->OMSetRenderTargets(1, &rtvHandle, true, &dsvHandle);
        
        // 设置根签名
        m_commandList->SetGraphicsRootSignature(m_rootSignature.Get());
        
        // 绑定常量缓冲区
        m_commandList->SetGraphicsRootConstantBufferView(
            0,
            m_transformCB->GetGPUVirtualAddress()
        );
        
        // 设置图元类型
        m_commandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
        
        // 绘制立方体
        m_cube.Draw(m_commandList.Get());
        
        // 准备呈现
        m_commandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(
            m_renderTargets[frameIndex].Get(),
            D3D12_RESOURCE_STATE_RENDER_TARGET,
            D3D12_RESOURCE_STATE_PRESENT
        ));
        
        // 关闭命令列表
        m_commandList->Close();
        
        // 执行命令列表
        ID3D12CommandList* cmdLists[] = { m_commandList.Get() };
        m_commandQueue->ExecuteCommandLists(_countof(cmdLists), cmdLists);
        
        // 呈现并等待 VSync
        m_swapChain->Present(1, 0);
        
        // 等待命令队列完成
        WaitForGpu();
    }
    
private:
    // 初始化 DirectX 12
    void InitD3D() {
        // 启用调试层
#if defined(_DEBUG)
        ComPtr<ID3D12Debug> debugController;
        D3D12GetDebugInterface(IID_PPV_ARGS(&debugController));
        debugController->EnableDebugLayer();
#endif
        
        // 创建 DXGI 工厂
        CreateDXGIFactory1(IID_PPV_ARGS(&m_dxgiFactory));
        
        // 创建设备
        D3D12CreateDevice(
            nullptr,                   // 默认适配器
            D3D_FEATURE_LEVEL_11_0,    // 最低功能级别
            IID_PPV_ARGS(&m_device)
        );
        
        // 设置视口和裁剪矩形
        m_viewport = CD3DX12_VIEWPORT(0.0f, 0.0f, static_cast<float>(m_width), static_cast<float>(m_height));
        m_scissorRect = CD3DX12_RECT(0, 0, m_width, m_height);
        
        // 获取描述符大小
        m_rtvDescriptorSize = m_device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
        m_dsvDescriptorSize = m_device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_DSV);
    }
    
    // 创建命令队列、命令分配器和命令列表
    void CreateCommandObjects() {
        // 创建命令队列
        D3D12_COMMAND_QUEUE_DESC queueDesc = {};
        queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
        queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
        m_device->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&m_commandQueue));
        
        // 创建命令分配器
        m_device->CreateCommandAllocator(
            D3D12_COMMAND_LIST_TYPE_DIRECT,
            IID_PPV_ARGS(&m_commandAllocator)
        );
        
        // 创建命令列表
        m_device->CreateCommandList(
            0,
            D3D12_COMMAND_LIST_TYPE_DIRECT,
            m_commandAllocator.Get(),
            nullptr,
            IID_PPV_ARGS(&m_commandList)
        );
        
        // 关闭命令列表
        m_commandList->Close();
        
        // 创建围栏和事件
        m_device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&m_fence));
        m_fenceValue = 1;
        m_fenceEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);
    }
    
    // 创建交换链
    void CreateSwapChain() {
        // 释放任何现有的交换链
        m_swapChain.Reset();
        
        // 创建交换链描述
        DXGI_SWAP_CHAIN_DESC1 swapChainDesc = {};
        swapChainDesc.Width = m_width;
        swapChainDesc.Height = m_height;
        swapChainDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
        swapChainDesc.SampleDesc.Count = 1;
        swapChainDesc.SampleDesc.Quality = 0;
        swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
        swapChainDesc.BufferCount = 2;
        swapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
        swapChainDesc.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING;
        
        // 创建交换链
        ComPtr<IDXGISwapChain1> swapChain1;
        m_dxgiFactory->CreateSwapChainForHwnd(
            m_commandQueue.Get(),
            m_hwnd,
            &swapChainDesc,
            nullptr,
            nullptr,
            &swapChain1
        );
        
        // 获取 IDXGISwapChain3 接口
        swapChain1.As(&m_swapChain);
    }
    
    // 创建描述符堆
    void CreateDescriptorHeaps() {
        // 创建 RTV 堆
        D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc = {};
        rtvHeapDesc.NumDescriptors = 2;
        rtvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;
        rtvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
        m_device->CreateDescriptorHeap(&rtvHeapDesc, IID_PPV_ARGS(&m_rtvHeap));
        
        // 创建 DSV 堆
        D3D12_DESCRIPTOR_HEAP_DESC dsvHeapDesc = {};
        dsvHeapDesc.NumDescriptors = 1;
        dsvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_DSV;
        dsvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
        m_device->CreateDescriptorHeap(&dsvHeapDesc, IID_PPV_ARGS(&m_dsvHeap));
    }
    
    // 创建渲染目标视图
    void CreateRenderTargetViews() {
        CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(m_rtvHeap->GetCPUDescriptorHandleForHeapStart());
        
        // 为每个交换链缓冲区创建 RTV
        for (UINT i = 0; i < 2; i++) {
            // 获取后台缓冲区
            m_swapChain->GetBuffer(i, IID_PPV_ARGS(&m_renderTargets[i]));
            
            // 创建渲染目标视图
            m_device->CreateRenderTargetView(m_renderTargets[i].Get(), nullptr, rtvHandle);
            
            // 移动到下一个描述符
            rtvHandle.Offset(1, m_rtvDescriptorSize);
        }
    }
    
    // 创建深度缓冲区
    void CreateDepthStencilBuffer() {
        // 创建深度缓冲区资源
        D3D12_RESOURCE_DESC depthStencilDesc = CD3DX12_RESOURCE_DESC::Tex2D(
            DXGI_FORMAT_D24_UNORM_S8_UINT,
            m_width,
            m_height,
            1, 0, 1, 0,
            D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL
        );
        
        D3D12_CLEAR_VALUE clearValue = {};
        clearValue.Format = DXGI_FORMAT_D24_UNORM_S8_UINT;
        clearValue.DepthStencil.Depth = 1.0f;
        clearValue.DepthStencil.Stencil = 0;
        
        CD3DX12_HEAP_PROPERTIES heapProps(D3D12_HEAP_TYPE_DEFAULT);
        
        m_device->CreateCommittedResource(
            &heapProps,
            D3D12_HEAP_FLAG_NONE,
            &depthStencilDesc,
            D3D12_RESOURCE_STATE_DEPTH_WRITE,
            &clearValue,
            IID_PPV_ARGS(&m_depthStencilBuffer)
        );
        
        // 创建深度模板视图
        D3D12_DEPTH_STENCIL_VIEW_DESC dsvDesc = {};
        dsvDesc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT;
        dsvDesc.ViewDimension = D3D12_DSV_DIMENSION_TEXTURE2D;
        dsvDesc.Flags = D3D12_DSV_FLAG_NONE;
        
        m_device->CreateDepthStencilView(
            m_depthStencilBuffer.Get(),
            &dsvDesc,
            m_dsvHeap->GetCPUDescriptorHandleForHeapStart()
        );
    }
    
    // 创建根签名
    void CreateRootSignature() {
        // 根参数(常量缓冲区视图)
        CD3DX12_ROOT_PARAMETER rootParam;
        rootParam.InitAsConstantBufferView(0);
        
        // 根签名描述
        CD3DX12_ROOT_SIGNATURE_DESC rootSigDesc(
            1, &rootParam,
            0, nullptr,
            D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT
        );
        
        // 序列化根签名
        ComPtr<ID3DBlob> serializedRootSig = nullptr;
        ComPtr<ID3DBlob> errorBlob = nullptr;
        HRESULT hr = D3D12SerializeRootSignature(
            &rootSigDesc,
            D3D_ROOT_SIGNATURE_VERSION_1,
            &serializedRootSig,
            &errorBlob
        );
        
        if (errorBlob != nullptr) {
            OutputDebugStringA((char*)errorBlob->GetBufferPointer());
        }
        
        // 创建根签名
        m_device->CreateRootSignature(
            0,
            serializedRootSig->GetBufferPointer(),
            serializedRootSig->GetBufferSize(),
            IID_PPV_ARGS(&m_rootSignature)
        );
    }
    
    // 编译着色器
    void CompileShaders() {
        // 示例着色器代码
        const char* vertexShaderSrc = R"(
            cbuffer TransformMatrices : register(b0)
            {
                matrix worldViewProj;
            };
            
            struct VertexIn
            {
                float3 PosL : POSITION;
                float3 NormalL : NORMAL;
                float2 TexC : TEXCOORD;

                float2 TexC : TEXCOORD;
            };
            
            struct VertexOut
            {
                float4 PosH : SV_POSITION;
                float3 NormalW : NORMAL;
                float2 TexC : TEXCOORD;
                float4 Color : COLOR;
            };
            
            VertexOut main(VertexIn vin)
            {
                VertexOut vout;
                
                // 将顶点位置转换到裁剪空间
                vout.PosH = mul(float4(vin.PosL, 1.0f), worldViewProj);
                
                // 只是传递法线和纹理坐标
                vout.NormalW = vin.NormalL;
                vout.TexC = vin.TexC;
                
                // 根据法线生成简单的颜色
                vout.Color = float4(vin.NormalL * 0.5 + 0.5, 1.0f);
                
                return vout;
            }
        )";
        
        const char* pixelShaderSrc = R"(
            struct PixelIn
            {
                float4 PosH : SV_POSITION;
                float3 NormalW : NORMAL;
                float2 TexC : TEXCOORD;
                float4 Color : COLOR;
            };
            
            float4 main(PixelIn pin) : SV_TARGET
            {
                return pin.Color;
            }
        )";
        
        // 设置编译标志
        UINT compileFlags = 0;
#if defined(_DEBUG)
        compileFlags |= D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION;
#endif
        
        // 编译顶点着色器
        ComPtr<ID3DBlob> errors;
        D3DCompile(
            vertexShaderSrc,
            strlen(vertexShaderSrc),
            nullptr,
            nullptr,
            nullptr,
            "main",
            "vs_5_0",
            compileFlags,
            0,
            &m_vertexShader,
            &errors
        );
        
        if (errors != nullptr) {
            OutputDebugStringA((char*)errors->GetBufferPointer());
        }
        
        // 编译像素着色器
        errors = nullptr;
        D3DCompile(
            pixelShaderSrc,
            strlen(pixelShaderSrc),
            nullptr,
            nullptr,
            nullptr,
            "main",
            "ps_5_0",
            compileFlags,
            0,
            &m_pixelShader,
            &errors
        );
        
        if (errors != nullptr) {
            OutputDebugStringA((char*)errors->GetBufferPointer());
        }
    }
    
    // 创建几何体
    void CreateGeometry() {
        // 使用几何体工厂创建立方体
        m_cube = GeometryFactory::CreateBox(m_device.Get(), 2.0f, 2.0f, 2.0f);
        
        // 将顶点和索引缓冲区从上传堆复制到默认堆
        m_commandAllocator->Reset();
        m_commandList->Reset(m_commandAllocator.Get(), nullptr);
        
        GeometryFactory::CopyVertexAndIndexBuffers(m_commandList.Get(), m_cube);
        
        m_commandList->Close();
        ID3D12CommandList* cmdLists[] = { m_commandList.Get() };
        m_commandQueue->ExecuteCommandLists(_countof(cmdLists), cmdLists);
        
        // 等待复制完成
        WaitForGpu();
    }
    
    // 常量缓冲区结构
    struct TransformMatrices {
        XMFLOAT4X4 worldViewProj;
    };
    
    // 创建常量缓冲区
    void CreateConstantBuffers() {
        m_transformCB = std::make_unique<ConstantBuffer<TransformMatrices>>(m_device.Get());
    }
    
    // 创建流水线状态对象
    void CreatePipelineState() {
        // 定义输入布局
        std::vector<D3D12_INPUT_ELEMENT_DESC> inputLayout = {
            { "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 }
        };
        
        // 创建 PSO 描述
        D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc = {};
        
        // 设置输入布局
        psoDesc.InputLayout = { inputLayout.data(), static_cast<UINT>(inputLayout.size()) };
        
        // 设置根签名
        psoDesc.pRootSignature = m_rootSignature.Get();
        
        // 设置顶点着色器
        psoDesc.VS = CD3DX12_SHADER_BYTECODE(m_vertexShader.Get());
        
        // 设置像素着色器
        psoDesc.PS = CD3DX12_SHADER_BYTECODE(m_pixelShader.Get());
        
        // 设置光栅化状态
        psoDesc.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT);
        
        // 设置混合状态
        psoDesc.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT);
        
        // 设置深度模板状态
        psoDesc.DepthStencilState = CD3DX12_DEPTH_STENCIL_DESC(D3D12_DEFAULT);
        
        // 设置图元类型
        psoDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
        
        // 设置渲染目标格式和数量
        psoDesc.NumRenderTargets = 1;
        psoDesc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM;
        
        // 设置深度模板格式
        psoDesc.DSVFormat = DXGI_FORMAT_D24_UNORM_S8_UINT;
        
        // 设置MSAA
        psoDesc.SampleMask = UINT_MAX;
        psoDesc.SampleDesc.Count = 1;
        psoDesc.SampleDesc.Quality = 0;
        
        // 创建 PSO
        m_device->CreateGraphicsPipelineState(&psoDesc, IID_PPV_ARGS(&m_pipelineState));
    }
    
    // 等待 GPU 完成
    void WaitForGpu() {
        // 新的围栏点
        const UINT64 currentFenceValue = m_fenceValue;
        m_commandQueue->Signal(m_fence.Get(), currentFenceValue);
        m_fenceValue++;
        
        // 如果 GPU 还没有执行到当前围栏点,则等待
        if (m_fence->GetCompletedValue() < currentFenceValue) {
            m_fence->SetEventOnCompletion(currentFenceValue, m_fenceEvent);
            WaitForSingleObject(m_fenceEvent, INFINITE);
        }
    }
    
private:
    // 窗口句柄和尺寸
    HWND m_hwnd;
    int m_width;
    int m_height;
    
    // DXGI 对象
    ComPtr<IDXGIFactory4> m_dxgiFactory;
    ComPtr<IDXGISwapChain3> m_swapChain;
    
    // Direct3D 对象
    ComPtr<ID3D12Device> m_device;
    ComPtr<ID3D12CommandQueue> m_commandQueue;
    ComPtr<ID3D12CommandAllocator> m_commandAllocator;
    ComPtr<ID3D12GraphicsCommandList> m_commandList;
    
    // 描述符堆
    ComPtr<ID3D12DescriptorHeap> m_rtvHeap;
    ComPtr<ID3D12DescriptorHeap> m_dsvHeap;
    
    // 描述符大小
    UINT m_rtvDescriptorSize;
    UINT m_dsvDescriptorSize;
    
    // 渲染目标
    ComPtr<ID3D12Resource> m_renderTargets[2];
    ComPtr<ID3D12Resource> m_depthStencilBuffer;
    
    // 同步对象
    ComPtr<ID3D12Fence> m_fence;
    UINT64 m_fenceValue;
    HANDLE m_fenceEvent;
    
    // 着色器
    ComPtr<ID3DBlob> m_vertexShader;
    ComPtr<ID3DBlob> m_pixelShader;
    
    // 根签名和流水线状态
    ComPtr<ID3D12RootSignature> m_rootSignature;
    ComPtr<ID3D12PipelineState> m_pipelineState;
    
    // 几何体
    MeshGeometry m_cube;
    
    // 常量缓冲区
    std::unique_ptr<ConstantBuffer<TransformMatrices>> m_transformCB;
    
    // 旋转角度
    float m_rotationAngle = 0.0f;
    
    // 视口和裁剪矩形
    D3D12_VIEWPORT m_viewport;
    D3D12_RECT m_scissorRect;
};

6.12 小结

通过本章的学习,我们了解了如何使用 DirectX 12 绘制 3D 几何体。DirectX 12 提供了底层图形 API,使开发者能够精确控制 GPU 的渲染过程。

以下是本章的主要内容概述:

顶点与输入布局:顶点是 3D 几何体的基本构建单元,包含位置、法线、纹理坐标等属性。输入布局定义了顶点数据在内存中的结构和解释方式。

顶点缓冲区:用于存储顶点数据并提供给 GPU 访问。在 DirectX 12 中,需要显式管理顶点缓冲区资源的状态和内存。

索引和索引缓冲区:通过重用顶点减少内存占用和处理需求,提高渲染效率。

着色器程序:顶点着色器处理顶点数据并将其转换到裁剪空间,像素着色器计算每个像素的最终颜色。

常量缓冲区:用于向着色器传递应用程序中的变量数据,如变换矩阵、光照参数等。

流水线状态对象:封装了渲染流水线的所有固定和可编程状态,包括着色器、输入布局、光栅器状态等。

在本章中,我们通过一个完整的立方体渲染示例,展示了这些概念如何协同工作,实现一个简单的 3D 图形应用程序。

对于希望创建高效、高质量的 3D 图形应用程序的开发者,掌握这些基础知识至关重要。随着对 DirectX 12 的深入理解,可以进一步探索更高级的图形技术,如光照模型、纹理映射、阴影映射、后处理特效等。

6.13 练习

修改立方体演示程序,使立方体同时绕 X 轴和 Y 轴旋转。

实现一个球体几何体生成函数,并使用它创建一个地球仪模型。

创建一个支持多种填充模式(实体、线框、点)的应用程序,允许用户在运行时切换模式。

实现一个简单的着色器,通过使用正弦或余弦函数基于时间变化顶点位置,创建波浪效果。

添加纹理支持到立方体演示程序,使每个面显示不同的纹理。

实现一个可以动态添加和移除几何体的场景管理器。

创建一个使用索引缓冲区的地形网格,并通过高度图实现高程变化。

实现一个简单的光照模型,包括环境光、漫反射和镜面反射,使立方体呈现出更真实的外观。

这些练习将帮助你巩固本章所学的知识,并将这些概念应用到实际开发中。

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容