Published on

DX学习笔记(四):绘制几何体

Authors
  • avatar
    Name
    东哥
    Twitter

用DirectX绘制几何体

流程简述

顶点与输入布局
  1. 创建顶点结构体
  2. 设置输入布局描述D3D12_INPUT_ELEMENT_DESC
顶点/索引缓冲区
  1. 创建顶点数据/索引数据
  2. 创建缓冲区/索引缓冲区
顶点着色/像素着色
  1. 编写HLSL着色器文件
  2. 编译着色器
常量缓冲区(上传堆,CPU可写,GPU可读)
  1. 填充常量缓冲区所需的描述符(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV)
  2. 构造缓冲区对象(单独的类),该类创建常量缓冲区
  3. 设置缓冲区大小为256B的整数倍
  4. 创建常量缓冲区视图
  5. 创建根签名

后续在提交命令列表的时候讲根签名和常量缓冲区绑定到流水线

流水线状态对象 PSO
  1. 创建PSO对象 D3D12_GRAPHICS_PIPELINE_STATE_DESC
  2. 设置PSO对象的各类参数,包括输入布局描述符、根签名、PS、VS、光栅器对象等
  3. 通过md3dDevice->CreateGraphicsPipelineState(&psoDesc,IID_PPV_ARGS(&mPSO))创建PSO
构建几何体
  1. 顶点/索引数据,布局描述
  2. 创建Geo类,并且设置类内参数,这些参数在填充视口描述的时候需要
  3. 创建顶点/索引缓冲区
  4. 在绘制阶段通过命令列表将顶点和索引视图绑定到流水线插槽
  5. 通过DrawIndexedInstanced绘制几何体
刷新(update)
  1. 根据鼠标信息重新构建观察矩阵(相对坐标的变换)
  2. 刷新常量缓冲区
Resize
  1. 重新构建透视投影矩阵
  2. 然后反馈到Update的常量缓冲区上

世界矩阵随移动/旋转/缩放而改变

观察矩阵随虚拟摄像机的移动/旋转/缩放而改变

投影矩阵随窗口大小调整而改变

刷新的数据通过根签名设置到着色器

顶点与输入布局

  • 顶点结构体
struct Vertex
{
    XMFLOAT3 Pos;
    XMFLOAT4 Color;
};
struct Vertex2
{
    XMFLOAT3 Pos;
    XMFLOAT3 Normal;
    XMFLOAT2 Tex0;
    XMFLOAT2 Tex1;
}
  • 输入布局描述
typedef struct D3D12_INPUT_LAYOUT_DESC
    {
    _Field_size_full_(NumElements)  const D3D12_INPUT_ELEMENT_DESC *pInputElementDescs;
    UINT NumElements;//数量
    } 	D3D12_INPUT_LAYOUT_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:语义,用于将顶点结构体中的元素与顶点着色器输入签名(参数列表)中的元素映射起来
  2. SemanticIndex:附加到语义的索引。
  3. Format:顶点元素格式,如DXGI_FORMAT_R32_FLOAT1D32位浮点标量
  4. InputSlot:传递元素所用的输入槽,DirectX12一共16个,索引0-15,一般用同一个输入槽即输入槽0
  5. AlignedByteOffset:顶点结构体的首地址到某点元素起始地址的偏移量
struct Vertex2
{
    XMFLOAT3 Pos;//偏移0字节
    XMFLOAT3 Normal;//偏移12字节
    XMFLOAT2 Tex0;//偏移24字节
    XMFLOAT2 Tex1;//偏移32字节
}
  1. InputSlotClass:一般指定为D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA,另外一种D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA用于实现实例化的高级技术
  2. InstanceDataStepRate:一般设置0,如果采用实例化就用1
  • 示例
  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_R32G32B32A32_FLOAT, 0, 12, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
      { "TEXCOORD", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 24, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
        { "TEXCOORD", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 32, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }
    };

顶点缓冲区

为了使GPU可以访问顶点数组,就需要把它们放置在缓冲区的GPU资源(ID2D12Resource)

  • 顶点缓冲区:存储顶点的缓冲区

  • 缓冲区描述:CD3D12_RESOURCE_DESC

struct CD3DX12_RESOURCE_DESC : public D3D12_RESOURCE_DESC
{
    CD3DX12_RESOURCE_DESC()
    {}
    explicit CD3DX12_RESOURCE_DESC( const D3D12_RESOURCE_DESC& o ) :
        D3D12_RESOURCE_DESC( o )
    {}
    CD3DX12_RESOURCE_DESC( 
        D3D12_RESOURCE_DIMENSION dimension,
        UINT64 alignment,
        UINT64 width,
        UINT height,
        UINT16 depthOrArraySize,
        UINT16 mipLevels,
        DXGI_FORMAT format,
        UINT sampleCount,
        UINT sampleQuality,
        D3D12_TEXTURE_LAYOUT layout,
        D3D12_RESOURCE_FLAGS flags )
        /**********/
}

此描述继承自D3D_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 );
    }

其他还有比如CD3DX12_RESOURCE_DESC::Tex1D,CD3DX12_RESOURCE_DESC::2D等便捷方法

然后通过ID3D12Device::CreateCommittedResource方法创建ID3D12Resource对象

  • 示例:创建默认缓冲区函数申明
  // 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())));

DX12中所有资源都用ID3D12Resource接口表示,DX11用各种比如ID3D11Buffers等表示

为了优化性能,静态几何体的顶点缓冲区置于默认堆中

因为CPU不能向默认堆(D3D12_HEAP_TYPE_DEFAULT)中的顶点缓冲区写入数据,所以就需要用到一个中介

  • 上传缓冲区 为了将数据从CPU复制到GPU显存中,即将顶点数据从系统内存复制到上传缓冲区,然后再复制到真正的顶点缓冲区中

  • 示例:创建默认缓冲区函数实现

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

    //创建实际的默认缓冲区资源
    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())));

    //为了将CPU端内存中的数据复制到缓冲区,我们就创建处于中介位置的上传堆
    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())));


    // 描述我们希望复制到默认缓冲区的数据
    D3D12_SUBRESOURCE_DATA subResourceData = {};
    subResourceData.pData = initData;
    subResourceData.RowPitch = byteSize;
    subResourceData.SlicePitch = subResourceData.RowPitch;

    //将数据复制到默认缓冲区的流程
    //UpdateSubresources辅助函数将数据从CPU复制到上传堆
    //再通过ID3D12CommandList::CopySubresourceRegion函数复制到mBuff中
	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));

    // uploadBuffer必须在调用方法以后依然存在,因为命令列表的复制操作可能未执行,等调用者复制完消息以后才可以释放


    return defaultBuffer;
}
  • D3D12_SUBRESOURCE_DATA
typedef struct D3D12_SUBRESOURCE_DATA
    {
    const void *pData;//初始化缓冲区所用的数据指针
    LONG_PTR RowPitch;//对于缓冲区,此参数为欲复制数据的字节数
    LONG_PTR SlicePitch;//同上
    } 	D3D12_SUBRESOURCE_DATA;
  • 示例:创建有立方体8个顶点的默认缓冲区
 std::array<Vertex, 8> vertices =
    {
        Vertex({ XMFLOAT3(-1.0f, -1.0f, -1.0f), XMFLOAT4(Colors::White) }),
		Vertex({ XMFLOAT3(-1.0f, +1.0f, -1.0f), XMFLOAT4(Colors::Black) }),
		Vertex({ XMFLOAT3(+1.0f, +1.0f, -1.0f), XMFLOAT4(Colors::Red) }),
		Vertex({ XMFLOAT3(+1.0f, -1.0f, -1.0f), XMFLOAT4(Colors::Green) }),
		Vertex({ XMFLOAT3(-1.0f, -1.0f, +1.0f), XMFLOAT4(Colors::Blue) }),
		Vertex({ XMFLOAT3(-1.0f, +1.0f, +1.0f), XMFLOAT4(Colors::Yellow) }),
		Vertex({ XMFLOAT3(+1.0f, +1.0f, +1.0f), XMFLOAT4(Colors::Cyan) }),
		Vertex({ XMFLOAT3(+1.0f, -1.0f, +1.0f), XMFLOAT4(Colors::Magenta) })
    };

const UINT vbByteSize = (UINT)vertices.size() * sizeof(Vertex);
Microsoft::WRL::`ComPtr<ID3D12Resource>` VertexBufferGPU = nullptr;
Microsoft::WRL::`ComPtr<ID3D12Resource>` VertexBufferUploader = nullptr;

mBoxGeo->VertexBufferGPU = d3dUtil::CreateDefaultBuffer(md3dDevice.Get(),
		mCommandList.Get(), vertices.data(), vbByteSize, mBoxGeo->VertexBufferUploader);

image-20200630173313574

  • 顶点缓冲区视图:为了将顶点缓冲区绑定到渲染流水线
typedef struct D3D12_VERTEX_BUFFER_VIEW
    {
    D3D12_GPU_VIRTUAL_ADDRESS BufferLocation;
    UINT SizeInBytes;
    UINT StrideInBytes;
    } 
  1. BufferLocation:顶点缓冲区的虚拟地址,通过ID3D12Resource::GetGPUVirtualAddress方法得到此地址
  2. SizeInBytes:顶点缓冲区大小(字节)
  3. StrideInBytes:每个顶点元素占用的字节

然后将顶点缓冲区视图和渲染流水线上的一个输入槽绑定

//第一个参数:所用的输入槽,一般用0
//第二个参数:与输入槽绑定的缓冲区数量,即pView数组中视图的数量
//视图数组的第一个元素的地址
mCommandList->IASetVertexBuffers(0, 1, &mBoxGeo->VertexBufferView());

将顶点缓冲区置入输入槽并不会进行绘制操作,仅仅是为顶点数据送至渲染流水线做好准备

真正绘制顶点的方法

//参数1:每个实例绘制的顶点数
//参数2:实例化高级技术,目前给1
//参数3:第一个被绘制顶点的索引
//参数4:实例化技术,目前给0
mCommandList->DrawIndexedInstanced(
		mBoxGeo->DrawArgs["box"].IndexCount, 
		1, 0, 0, 0);

索引和索引缓冲区

  • 索引缓冲区

与顶点类似,为了使GPU可以访问索引数组,需要将它们放置于GPU的缓冲区资源内,此缓冲区即索引缓冲区

同样用CreateDefaultBuffer方法创建索引缓冲区

索引缓冲区用到结构体D3D12_INDEX_BUFFER_VIEW描述

typedef struct D3D12_INDEX_BUFFER_VIEW
    {
    D3D12_GPU_VIRTUAL_ADDRESS BufferLocation;//带创建索引缓冲区虚拟地址,用ID3D12Resource::GetGUPVirtualAddress()得到
    UINT SizeInBytes;//索引缓冲区大小
    DXGI_FORMAT Format;//必须为DXGI_FORMAT_R16_UINT类型或者DXGI_FORMAT_R32_UINT
    } 

同顶点缓冲区,也需要绑定到渲染流水线,通过ID3D12GraphicsCommandList::IASetIndexBuffer方法将索引缓冲区绑定到输入装配器阶段

	std::array<std::uint16_t, 36> 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 = (UINT)indices.size() * sizeof(std::uint16_t);

Microsoft::WRL::`ComPtr<ID3D12Resource>` IndexBufferGPU = nullptr;
Microsoft::WRL::`ComPtr<ID3D12Resource>` IndexBufferUploader = nullptr;

mBoxGeo->IndexBufferGPU = d3dUtil::CreateDefaultBuffer(md3dDevice.Get(),
		mCommandList.Get(), indices.data(), ibByteSize, mBoxGeo->IndexBufferUploader);

使用索引时,要用ID3D12GraphicsCommandList::DrawIndexedInstanced代替DrawInstanced方法

//第4个参数:读取顶点之前,要为每个索引都加上的整数值
//其他参考顶点绘制
mCommandList->DrawIndexedInstanced(
		mBoxGeo->DrawArgs["box"].IndexCount, 
		1, 0, 0, 0);

将多个顶点缓冲区和索引缓冲区合并以后会需要用到第4个参数

mCommandList->DrawIndexedInstanced(numOfBox, 1, 0, 0, 0);
mCommandList->DrawIndexedInstanced(numOfSph, 1, firstSphIdx, firstSphIdx, 0);
mCommandList->DrawIndexedInstanced(numOfCyl, 1, firstCylIdx, firstCylIdx, 0);

image-20200624172043571

顶点着色器示例

  • 高级着色器语言:HLSL

示例

cbuffer cbPerObject:register(b0)
{
    float4x4 gWorldViewProj;
}
void VS(float3 iPosL:POSITION,float4 iColor:COLOR,out float4 oPosH:SV_POSTION,out float4 oColor:COLOR)
{
    //把顶点变换到齐次裁剪空间
    oPosH = mul(float4(iPosL,1.0f),gWolrdViewProj);
    //将顶点颜色信息传至像素着色器
    oColor=iColor;
}

HLSL没有指针和引用,用out表示输出参数

HLSL所有函数都是lnline函数

参数POSITIONCOLOR语义将元素映射到顶点着色器对应的输入参数;同理输出参数的语义将参数映射到下一个阶段(几何着色器或者像素着色器)对应的输入参数;

SV_POSTIONSV代表系统值,此数据存有齐次裁剪空间顶点信息,指定了此语义以后使GPU进行如裁剪、深度测试等处理时,实现其他属性无法介入的有关运算

任何没有系统修饰的参数,都可以根据需求以合法的语义修饰

可以把函数的参数封装成结构体,下面是另一种实现方式

struct VertexIn
{
    float3 PosL:POSTION;
    float4 Color:COLOR;
};
struct VertexOut
{
    float3 PosH:SV_POSTION;
    float4 Color:COLOR;
};
VextexOut VS(VertexIn vin)
{
    VertexOut vout;
    vout.PosH=mul(float4(vin.PosL,1.0f),gWolrdViewProj);
    vout.Color=vin.Color;
    return vout;
}

如果没有使用几何着色器,那么顶点着色器必须用SV_POSITION来输出顶点在齐次裁剪空间的位置

如果使用了,可以把得到齐次裁剪空间位置的工作交给它来处理

顶点着色器/几何着色器无法完成透视除法,此阶段只能完成投影矩阵运算,透视除法由后续硬件执行

布局描述符与签名匹配问题

ID3D12Device::CreateGraphicsPipelineState函数时,需要指定布局描述符和顶点着色器,这就涉及到参数匹配问题

  • 不匹配
struct Vertex
{
    XMFLOAT3 Pos;
    XMFLOAT4 Color;
    //XMFLOAT3 Normal;//缺少此行输入
}

 D3D12_INPUT_ELEMENT_DESC[] =
    {
        { "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 },
      { "NORMAL", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 24, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }
    };

struct VertexIn
{
    float3 PosL:POSTION;
    float4 Color:COLOR;
    float3 Normal:NORMAL;
};
struct VertexOut
{
    float3 PosH:SV_POSTION;
    float4 Color:COLOR;
};

VertexOut VS(VertexIn vin){...};
  • 匹配
struct Vertex
{
    XMFLOAT3 Pos;
    XMFLOAT4 Color;
    XMFLOAT3 Normal;
}

 D3D12_INPUT_ELEMENT_DESC[] =
    {
        { "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 },
      { "NORMAL", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 24, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }
    };

struct VertexIn
{
    float3 PosL:POSTION;
    float4 Color:COLOR;
    //float3 Normal:NORMAL;//输入有多余的参数可以匹配
};
struct VertexOut
{
    float3 PosH:SV_POSTION;
    float4 Color:COLOR;
};

VertexOut VS(VertexIn vin){...};
  • 匹配
struct Vertex
{
    XMFLOAT3 Pos;
    XMFLOAT4 Color;
    XMFLOAT3 Normal;
}

 D3D12_INPUT_ELEMENT_DESC[] =
    {
        { "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 },
      { "NORMAL", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 24, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }
    };

struct VertexIn
{
    float3 PosL:POSTION;
    float4 Color:COLOR;
    int3  Normal:NORMAL;//Direct3D会对数据类型重新解释,但会报警告
};
struct VertexOut
{
    float3 PosH:SV_POSTION;
    float4 Color:COLOR;
};

VertexOut VS(VertexIn vin){...};

像素着色器示例

光栅化阶段先对顶点着色器进行插值计算,然后把数据传递至像素着色器作为输出

像素着色器针对每一个像素片段而运行的函数,但是部分像素片段不会传入或者留存在后台缓冲区,会在深度测试等情况被丢弃掉;即像素是最终写入后台缓冲区的数据,像素片段是候选像素

示例

cbuffer cbPerObject:register(b0)
{
    float4x4 gWorldViewProj;
}
void VS(float3 iPosL:POSITION,float4 iColor:COLOR,out float4 oPosH:SV_POSTION,out float4 oColor:COLOR)
{
    //把顶点变换到齐次裁剪空间
    oPosH = mul(float4(iPosL,1.0f),gWolrdViewProj);
    //将顶点颜色信息传至像素着色器
    oColor=iColor;
}

float4 PS(float4 posH:SV_POSITION,float4 color:COLOR):SV_TARGET
{
    return color;
}

参数列表后的SV_TARGET表示该返回值的类型樱岛与渲染目标格式相匹配(render target format )1

同样可以利用输入输出结构体来重写上述代码

struct VertexIn
{
    float3 PosL:POSTION;
    float4 Color:COLOR;
  
};
struct VertexOut
{
    float3 PosH:SV_POSTION;
    float4 Color:COLOR;
};
VextexOut VS(VertexIn vin)
{
    VertexOut vout;
    vout.PosH=mul(float4(vin.PosL,1.0f),gWolrdViewProj);
    vout.Color=vin.Color;
    return vout;
}
float4 PS(VertexOut pin):VS_TARGET
{
    return pin.Color;
}

常量缓冲区

创建常量缓冲区

常量缓冲区也是一种GPU资源ID3D12Resource

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

此代码中的cbuffer对象(常量缓冲区)的名称就是cbPerObject,存储一个4x4矩阵gWorldViewProj

此矩阵表示把一个点从局部空间变换到齐次裁剪空间所用到的由世界、视图和投影3种变换组合而成的矩阵

与顶点和索引缓冲区不同,从常量缓冲区由CPU每帧更新顶一次

常量缓冲区创建到上传堆而非默认堆,使得我们从CPU端更新常量

常量缓冲区的硬件有特别要求,大小必须为256B的整数倍

示例

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

 UINT mElementByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof(T));//换算使得缓冲区大小保持在256B的倍数

Microsoft::WRL::`ComPtr<ID3D12Resource>` mUploadBuffer;
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)));

mUploadCBuffer存储了一个ObjectConstants类型的常量缓冲区数组

绘制物体时,只要将常量缓冲区视图CBV绑定到那个存有物体相应常量缓冲区的子区域即可

  • 着色器模型5.1
struct ObjectConstants
{
    float4x4 gWorldViewProj;
    uint matIndex;
};
`ConstantBuffer<ObjectConstants>` gObjConstants : register(b0);

常量缓冲区的数据元素被定义在一个单独的结构体中,随后用这个结构来创建一个常量缓冲区,然后就可以在着色器里访问常量缓冲区中的各个字段

uint index = gObjConstants.matIndex;

着色器模型定义了HLSL的编写规范,确定了其内置函数、着色器属性等一切语言元素

更新常量缓冲区

常量缓冲区是用D3D12_HEAP_TYPE_UPLOAD类型创建的,所以可以用CPU为常量缓冲区更新数据

Microsoft::WRL::`ComPtr<ID3D12Resource>` mUploadBuffer;
BYTE* mMappedData = nullptr;
//参数1:预映射的子资源,对于缓冲区来说设置为0
//参数2:指向D3D12_RANGE结构体的可选项,描述了映射范围,如果是空就对整个资源映射
//参数3:双重指针返回待映射资源书的目标内存块
ThrowIfFailed(mUploadBuffer->Map(0, nullptr, reinterpret_cast<void**>(&mMappedData)));
memcpy(mMappedData,&data,dataSizeInBytes);//用此函数将数据从系统内存复制到常量缓冲区

等更新完成,然后在释放映射内存之前对其进行Unmap(取消映射)操作

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

Unmap的第一个参数是子资源索引,第二个是指向D3D12_RANGE结构体的指针,描述取消映射的内存范围,如空则整个资源映射。

上传缓冲区辅助函数
  • UploadBuffer类
template<typename T>
class UploadBuffer
{
public:
    UploadBuffer(ID3D12Device* device, UINT elementCount, bool isConstantBuffer) : 
        mIsConstantBuffer(isConstantBuffer)
    {
        mElementByteSize = sizeof(T);

        //常量缓冲区的大小为256B的整数倍
        //长度者两种规格来查看常量数据
        //UINT64 SizeInBytes;//256的整数倍
        //UINT OffsetInBytes;//256的整数倍
        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<void**>(&mMappedData)));

        //只要还会需改当前的资源,我们就无需取消映射
        // 但是在资源被GPU使用期间,我们千万不可向该资源进行写操作(所以需要借助同步技术)
    }

    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<ID3D12Resource>` mUploadBuffer;
    BYTE* mMappedData = nullptr;

    UINT mElementByteSize = 0;
    bool mIsConstantBuffer = false;
};

世界矩阵随移动/旋转/缩放而改变

观察矩阵随虚拟摄像机的移动/旋转/缩放而改变

投影矩阵随窗口大小调整而改变

  • OnMouseMove
void BoxApp::OnMouseMove(WPARAM btnState, int x, int y)
{
    if((btnState & MK_LBUTTON) != 0)
    {
        // 根据鼠标的移动距离计算旋转角度,令每个像素按此角度的1/4旋转
        float dx = XMConvertToRadians(0.25f*static_cast<float>(x - mLastMousePos.x));
        float dy = XMConvertToRadians(0.25f*static_cast<float>(y - mLastMousePos.y));

        // 根据鼠标输入来更新摄像机绕立方体旋转的角度
        mTheta += dx;
        mPhi += dy;

        // 限制角度mPhi的范围
        mPhi = MathHelper::Clamp(mPhi, 0.1f, MathHelper::Pi - 0.1f);
    }
    else if((btnState & MK_RBUTTON) != 0)
    {
        // 使场景中的每个像素按鼠标移动距离的0.005倍进行缩放
        float dx = 0.005f*static_cast<float>(x - mLastMousePos.x);
        float dy = 0.005f*static_cast<float>(y - mLastMousePos.y);

        // 根据鼠标的输入更新摄像机的可视范围半径
        mRadius += dx - dy;

        // 限制可视半径范围
        mRadius = MathHelper::Clamp(mRadius, 3.0f, 15.0f);
    }

    mLastMousePos.x = x;
    mLastMousePos.y = y;
}
  • Update
void BoxApp::Update(const GameTimer& gt)
{
    // 将球坐标(球面坐标)转换为笛卡尔坐标系
    float x = mRadius*sinf(mPhi)*cosf(mTheta);
    float z = mRadius*sinf(mPhi)*sinf(mTheta);
    float y = mRadius*cosf(mPhi);

    // 构建观察矩阵
    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;

	// 用最新的 worldViewProj 矩阵来更新常量缓冲区
	ObjectConstants objConstants;
    XMStoreFloat4x4(&objConstants.WorldViewProj, XMMatrixTranspose(worldViewProj));
    mObjectCB->CopyData(0, objConstants);
}
常量缓冲区描述符

常量缓冲区描述符都存放在以D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV类型的描述符堆里,这个堆混合存储了常量缓冲区、着色器资源和无序访问描述符

 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;
    ThrowIfFailed(md3dDevice->CreateDescriptorHeap(&cbvHeapDesc,
        IID_PPV_ARGS(&mCbvHeap)));

在创建着色器程序访问的描述符时,需要把Flags指定为DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE

然后通过如下代码创建常量缓冲区

void BoxApp::BuildConstantBuffers()
{
    //此常量缓冲区存储了绘制n个物体所需的常量数据
	std::unique_ptr<`UploadBuffer<ObjectConstants>`> mObjectCB = std::make_unique<`UploadBuffer<ObjectConstants>`>(md3dDevice.Get(), 1, true);

	UINT objCBByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof(ObjectConstants));
	//缓冲区的起始地址	
	D3D12_GPU_VIRTUAL_ADDRESS cbAddress = mObjectCB->Resource()->GetGPUVirtualAddress();
    //偏移到常量缓冲区中绘制第i个物体所需的常量数据
    int boxCBufIndex = 0;
	cbAddress += boxCBufIndex*objCBByteSize;

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

	md3dDevice->CreateConstantBufferView(
		&cbvDesc,
		mCbvHeap->GetCPUDescriptorHandleForHeapStart());
}
根签名和描述符表
  • 根签名
//将纹理资源绑定到纹理寄存器槽0
Texture2D gDiffuseMap : register(t0);
//把下列采样器资源一次绑定到采样器槽0-5
SamplerState gsamPointWrap : register(s0);
SamplerState gsamPointClamp : register(s1);
//将常量缓冲区资源绑定到常量缓冲区寄存器槽0
cbuffer cbPerObject : register(b0)
{
    float4x4 gWorld;
    float4x4 gTexTransform;
}
//绘制每种材质所需的各种不同的常量数据
cbuffer cbMaterial : register(b2)
{
    float4 gDiffuseAlbedo;
    float3 gFresnelR0;
    float gRoughness;
    float4x4 gMatTransform;
}

register(*#)中的*表示寄存器传递的资源类型。

t:着色器资源视图;

s:采样器

u:无序访问视图

b:常量缓冲区视图

在执行绘制命令之前,那些应用程序将绑定到渲染流水线上的资源,它们会被映射到着色器的对应输入寄存器。

根签名一定要与使用它的着色器相兼容,在创建流水线状态对象时会对此进行验证

不同的绘制调用可能会用到一组不同的着色器程序,意味着要用到不同的根签名

在Direct3D中,根签名由ID3D12RootSignature接口表示,并用一组描述绘制调用过程中着色器所需资源的根参数定义而成。

根参数可以是根常量根描述符或者描述符表

如果把着色器程序看成是一个大函数,那顶点数据和常量数据就是从CPU传入着色器函数的参数,而根签名就好比这些参数的函数签名。所以根签名其实就是将着色器需要用到的数据绑定到对应的寄存器槽上,供着色器访问。

示例

oid BoxApp::BuildRootSignature()
{
	// Shader programs typically require resources as input (constant buffers,
	// textures, samplers).  The root signature defines the resources the shader
	// programs expect.  If we think of the shader programs as a function, and
	// the input resources as function parameters, then the root signature can be
	// thought of as defining the function signature.  

	// 根参数可以是根常量、根描述符或者描述符表
	CD3DX12_ROOT_PARAMETER slotRootParameter[1];

	// 创建一个只存有一个CBV的描述符表
	CD3DX12_DESCRIPTOR_RANGE cbvTable;
	cbvTable.Init(D3D12_DESCRIPTOR_RANGE_TYPE_CBV, 1, 0);//1:表中的描述符数量,0:将此段描述符区域绑定至此基准着色器寄存器
	slotRootParameter[0].InitAsDescriptorTable(1, &cbvTable);//1:描述符区域的数量;table:指向描述符区域数组的指针

	// 根签名由一组根参数构成
	CD3DX12_ROOT_SIGNATURE_DESC rootSigDesc(1, slotRootParameter, 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.GetAddressOf(), errorBlob.GetAddressOf());

	if(errorBlob != nullptr)
	{
		::OutputDebugStringA((char*)errorBlob->GetBufferPointer());
	}
	ThrowIfFailed(hr);

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

// Root parameter can be a table, root descriptor or root constants.
	CD3DX12_ROOT_PARAMETER slotRootParameter[1];
cbvTable.Init(D3D12_DESCRIPTOR_RANGE_TYPE_CBV,//描述符表的类型
              1, //表中描述符的数量
              0);//将这段描述符区域绑定至此基础着色器寄存器
	slotRootParameter[0].InitAsDescriptorTable(1,//描述符区域的数量
                                               &cbvTable);//指向描述符区域数组的指针

上述代码创建了一个根参数,目的是将含有一个CBV的描述符表绑定到常量缓冲区寄存器0,即HLSL代码中的register(b0)


根签名至定义了应用程序要绑定到渲染流水线的资源,没有真正的执行任何资源绑定操作

只有通过命令列表设置根签名,才可以用ID3D12GraphicsCommandList::SetGraphicsRootDescriptorTable 方法令描述符表与渲染流水线绑定

mCommandList->SetGraphicsRootDescriptorTable(0, //将根参数按此索引进行设置
mCbvHeap->GetGPUDescriptorHandleForHeapStart());//此参数指定的是将要想着色器绑定的描述符表中的第一个描述符位于描述符堆中的句柄

下列代码先将根签名和CBV堆设置到命令列表上,并随后通过设置描述符表来指定我们希望绑定到渲染流水线的资源

mCommandList->SetGraphicsRootSignature(mRootSignature.Get());

ID3D12DescriptorHeap* descriptorHeaps[] = { mCbvHeap.Get() };
	mCommandList->SetDescriptorHeaps(_countof(descriptorHeaps), descriptorHeaps);
//偏移到此次绘制调用所需的CBV处
CD3DX12_GPU_DESCRIPTOR_HANDLE cbv(mCbvHeap->GetGPUDescriptorHandleForHeapStart());
cbv.Offset(cbvIndex,mCbvSrvUavDescriptorSize);

mCommandList->SetGraphicsRootDescriptorTable(0,cbv);

·

编译着色器

着色器程序必须先被编译为一种可移植的字节码,接下来图形驱动程序将获取这些字节码,并将其重新便以为针对当前系统GPU所优化的本地指令[ATI1]

可以用如下函数在运行期间对着色器进行编译

HRESULT D3DCompileFromFile(
	LPCWSTR pFileName,//希望编译的以.hlsl作为扩展名的源代码文件
    const D3D_SHADER_MACRO *pDefines,//本书中用nullptr
    ID3DInclude *pInclude,//同上
    LPOCSTR pEntrypoint,//着色器入口点函数,一个.hlsl文件可能存有多个着色器程序
    LPCSTR pTarget,//指定所用着色器类型和版本的字符串,本书用的是5.0和5.1
    UINT Flags1,//一般用2种:D3DCOMPLIE_DEBUG/D3DCOMPILE_SKIP_OPTIMIZATIOIN,详见SDK
    UINT Flags2,//本书不用
    ID3DBlob **ppCode,//返回一个数据结构指针,存储着编译好的着色器对象字节码
    ID3DBlob **ppErrorMsgs//返回报错信息               
);
  • ID3DBlob

一段普通的内存块,有2个接口方法

  1. LPVOID GetBufferPointer:返回对象中数据的void*类型的指针,使用之前需要转换为适当类型
  2. SIZE_T GetBufferSize:返回缓冲区字节大小
  • 运行时编译着色器函数
`ComPtr<ID3DBlob>`  CompileShader(
	const std::wstring& fileName, 
	const D3D_SHADER_MACRO* defines, 
	const std::string& enteryPoint, 
	const std::string& target)
{
	//若处于调试模式,则使用调试标志
	UINT compileFlags = 0;
#if defined(DEBUG) || defined(_DEBUG)
	//用调试模式来编译着色器 | 指示编译器跳过优化阶段
	compileFlags = D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION;
#endif // defined(DEBUG) || defined(_DEBUG)

	HRESULT hr = S_OK;

	`ComPtr<ID3DBlob>` byteCode = nullptr;
	`ComPtr<ID3DBlob>` errors;
	hr = D3DCompileFromFile(fileName.c_str(), //hlsl源文件名
		defines,	//高级选项,指定为空指针
		D3D_COMPILE_STANDARD_FILE_INCLUDE,	//高级选项,可以指定为空指针
		enteryPoint.c_str(),	//着色器的入口点函数名
		target.c_str(),		//指定所用着色器类型和版本的字符串
		compileFlags,	//指示对着色器断代码应当如何编译的标志
		0,	//高级选项
		&byteCode,	//编译好的字节码
		&errors);	//错误信息

	if (errors != nullptr)
	{
		OutputDebugStringA((char*)errors->GetBufferPointer());
	}
	ThrowIfFailed(hr);

	return byteCode;
}
离线编译

离线编译的原因

  1. 对于复杂的着色器,编译耗时太长,因此借助离线编译即可缩短应用程序的加载时间
  2. 以便在早于运行时的构建处理期间提早发现编译错误
  3. 对于Windows9应用商店的应用而言,必须采取离线编译这种方式

一般用.cso为已编译的着色器对象的扩展名

使用DirectX自带的FXC命令行编译工具编译

  • 如用CMD输入代码编译
C:\Program Files (x86)\Windows Kits\10\bin\x86>fxc.exe C:\Users\Administrator\Desktop\color.hlsl /T vs_5_0 /Fo "color.cso" /E "VS"

成功输出color.cso文件

compilation object save succeeded; see C:\Program Files (x86)\Windows Kits\10\bin\x86\color.cso

  • 常用的编译参数
参数描述
/Od禁用优化(利于调试)
/Zi开启调试信息
/T "string"着色器模型版本,如输入vs_5_0
/E "string"着色器入口,如案例中的VS,PS
/Fo "string"经过编译的着色器对象字节码
/Fc "string"输出一个着色器汇编语言清单(调试、查阅细节)

其他清单可以参考微软SDK文档

光栅器状态

光栅器状态只接受配置,非可编程,由结构体D3D12_RASTERIZER_DESC表示

typedef struct D3D12_RASTERIZER_DESC
    {
    D3D12_FILL_MODE FillMode;//默认:D3D12_FILL_MODE_SOLID
    D3D12_CULL_MODE CullMode;//默认: D3D12_CULL_MODE_BACK
    BOOL FrontCounterClockwise;//默认:false
    INT DepthBias;
    FLOAT DepthBiasClamp;
    FLOAT SlopeScaledDepthBias;
    BOOL DepthClipEnable;
    BOOL MultisampleEnable;
    BOOL AntialiasedLineEnable;
    UINT ForcedSampleCount;
    D3D12_CONSERVATIVE_RASTERIZATION_MODE ConservativeRaster;
    } 	D3D12_RASTERIZER_DESC;

FillMode:默认是实体渲染,如果用D3D12_FILL_MODE_WIREFRAME则是线框渲染

CullMode:默认剔除背面,D3D12_CULL_MODE_NONE不剔除,D3D12_CULL_MODE_FRONT剔除正面

FrontCounterClockwise:默认false,根据观察视角,将定点顺序为顺时针方向的三角形看作正面;如true则相反

示例

CD3DX12_RASTERIZER_DESC rsDesc(D3D12_DEFAULT);
reDesc.FillMode = D3D12_FILL_MODE_WIREFRAME;
reDesc.CullMode = D3D12_CULL_MODE_NONE;

CD3DX12_RASTERIZER_DESC是扩展自D3DX12_RASTERIZER_DESC的结构,添加了一些辅助构造函数的工具类,如接受CD3DX12_DEFAULT参数的构造函数。

CD3DX12_DEFAULT是一个哑类型(dummy),将需要被初始化的成员重载为默认值

struct CD3DX12_DEFAULT {};
extern const DECLSPEC_SELECTANY CD3DX12_DEFAULT D3D12_DEFAULT;

流水线状态对象

ID3D12PipelineState表示流水线状态对象(PSO)

结构体

typedef struct D3D12_GRAPHICS_PIPELINE_STATE_DESC
    {
    ID3D12RootSignature *pRootSignature;//指向一个于此PSO绑定的根签名,该根签名必须与PSO指定的着色器兼容
    D3D12_SHADER_BYTECODE VS;//待绑定的顶点着色器,用结构体 D3D12_SHADER_BYTECODE表示
    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;//混合,默认指定CD3DX12_BLEND_DESC(D3D12_DEFAULT)
    UINT SampleMask;//禁止的采样,默认0xffffffff都进行采样
    D3D12_RASTERIZER_DESC RasterizerState;//指定用来配置光栅器的光栅化状态
    D3D12_DEPTH_STENCIL_DESC DepthStencilState;//用于配置深度/模板测试的深度/模板状态,默认用D3D12_DEPTH_STENCIL_DESC(D3D12_DEFAULT)
    D3D12_INPUT_LAYOUT_DESC InputLayout;//输入布局描述,有两个成员,D3D12_INPUT_ELEMENT_DESC数组和一个表示元素个数的无符号整数
    D3D12_INDEX_BUFFER_STRIP_CUT_VALUE IBStripCutValue;
    D3D12_PRIMITIVE_TOPOLOGY_TYPE PrimitiveTopologyType;//图元的拓扑类型,D3D12_PRIMITIVE_TOPOLOGY_TYPE
    UINT NumRenderTargets;//同时所用的渲染目标数量
    DXGI_FORMAT RTVFormats[ 8 ];//渲染目标格式
    DXGI_FORMAT DSVFormat;//深度/模板缓冲的格式
    DXGI_SAMPLE_DESC SampleDesc;//多重采样对每个像素采样的数量和质量级别
    UINT NodeMask;
    D3D12_CACHED_PIPELINE_STATE CachedPSO;
    D3D12_PIPELINE_STATE_FLAGS Flags;
    } 	D3D12_GRAPHICS_PIPELINE_STATE_DESC;

示例

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<BYTE*>(mvsByteCode->GetBufferPointer()), 
		mvsByteCode->GetBufferSize() 
	};
    psoDesc.PS = 
	{ 
		reinterpret_cast<BYTE*>(mpsByteCode->GetBufferPointer()), 
		mpsByteCode->GetBufferSize() 
	};
    psoDesc.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT);
    psoDesc.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT);
    psoDesc.DepthStencilState = CD3DX12_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;
    ThrowIfFailed(md3dDevice->CreateGraphicsPipelineState(&psoDesc, IID_PPV_ARGS(&mPSO)));

PSO的验证和创建过于耗时,所以一般在初始化期间就生成PSO

视口和裁剪矩形等属性独立于PSO

用不同的PSO绘制不同的物体

mCommandList->Reset(mDirectCmdListAlloc.Get(),mPSO1.Get());

mCommandList->Reset(mDirectCmdListAlloc.Get(),mPSO2.Get());

mCommandList->Reset(mDirectCmdListAlloc.Get(),mPSO3.Get());

几何图形辅助结构体

此结构体定义了MeshGeometry中存储的单个几何体

struct SubmeshGeometry
{
	UINT IndexCount = 0;
	UINT StartIndexLocation = 0;
	INT BaseVertexLocation = 0;

    // 通过此子网格来定义当前结构体中所存几何体的包围盒 bounding box ,后续章节会讲述
	DirectX::BoundingBox Bounds;
};
struct MeshGeometry
{
	// 几何体网格集的名称
	std::string Name;

	// 系统内存中的副本,由于顶点/索引可以是泛型格式,所以用Blob类型
	Microsoft::WRL::`ComPtr<ID3DBlob>` VertexBufferCPU = nullptr;
	Microsoft::WRL::`ComPtr<ID3DBlob>` IndexBufferCPU  = nullptr;

	Microsoft::WRL::`ComPtr<ID3D12Resource>` VertexBufferGPU = nullptr;
	Microsoft::WRL::`ComPtr<ID3D12Resource>` IndexBufferGPU = nullptr;

	Microsoft::WRL::`ComPtr<ID3D12Resource>` VertexBufferUploader = nullptr;
	Microsoft::WRL::`ComPtr<ID3D12Resource>` IndexBufferUploader = nullptr;

    // 缓冲区数据
	UINT VertexByteStride = 0;
	UINT VertexBufferByteSize = 0;
	DXGI_FORMAT IndexFormat = DXGI_FORMAT_R16_UINT;
	UINT IndexBufferByteSize = 0;

	// 一个结构体能够存储一组顶点/索引缓冲区中的多个几何体
    //用下列容器来定义子网格几何体,就能单独绘制其中的子网格
	std::unordered_map<std::string, SubmeshGeometry> 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;
	}

	// 等数据上传GPU后,我们就能释放内存了
	void DisposeUploaders()
	{
		VertexBufferUploader = nullptr;
		IndexBufferUploader = nullptr;
	}
};

绘制Box

#include <Windows.h>
#include "../Common/d3dUtil.h"
#include "DXApp.h"
#include "../Common/UploadBuffer.h"
#include "../Common/MathHelper.h"


using namespace DirectX;
using namespace DirectX::PackedVector;
using Microsoft::WRL::ComPtr;

#define  _DEBUG 1
struct Vertex
{
	XMFLOAT3 Pos;
	XMFLOAT4 Color;
};

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

class MyBox:public DXApp
{
public:
	MyBox(HINSTANCE instance);
	MyBox(const MyBox& rhs)=delete;
	MyBox& operator=(const MyBox& rhs)=delete;
	~MyBox();
	virtual bool Initialize()override;

	

	
private:
	virtual void OnMouseDown(WPARAM btnState, int x, int y)override;
	virtual void OnMouseUp(WPARAM btnState, int x, int y)override;
	virtual void OnMouseMove(WPARAM btnState, int x, int y)override;
	
	void BuildDescriptorHeaps();//常量缓冲区描述符堆
	void BuildConstantBuffers();
	void BuildRootSignature();
	void BuildShadersAndInputLayout();
	void BuildBoxGeometry();
	void BuildPSO();

	virtual void Update(const GameTimer& gt)override;
	virtual void Draw(const GameTimer& gt)override;
	virtual void OnResize()override;

	`ComPtr<ID3D12DescriptorHeap>` mCbvHeap = nullptr;
	`ComPtr<ID3D12RootSignature>` mRootSignature = nullptr;
	std::unique_ptr<`UploadBuffer<ObjectConstants>`> mObjectCB = nullptr;
	//封装的几何体结构体的指针
	std::unique_ptr<MeshGeometry> mBoxGeo = nullptr;

	//编译材质参数
	`ComPtr<ID3DBlob>` mvsByteCode = nullptr;
	`ComPtr<ID3DBlob>` mpsByteCode = nullptr;
	//输入布局描述
	std::vector<D3D12_INPUT_ELEMENT_DESC> mInputLayout;
	
	`ComPtr<ID3D12PipelineState>` mPSO = nullptr;
	

	XMFLOAT4X4 mWorld = MathHelper::Identity4x4();
	XMFLOAT4X4 mView = MathHelper::Identity4x4();
	XMFLOAT4X4 mProj = MathHelper::Identity4x4();

	float mTheta = 1.5f*XM_PI;
	float mPhi = XM_PIDIV2;
	float mRadius = 5.0f;
	POINT mLastMousePos;
};

MyBox::MyBox(HINSTANCE instance):DXApp(instance)
{
	
}



MyBox::~MyBox()
{

}

void MyBox::Update(const GameTimer& gt)
{
	// 将球坐标(球面坐标)转换为笛卡尔坐标系
	float x = mRadius * sinf(mPhi)*cosf(mTheta);
	float z = mRadius * sinf(mPhi)*sinf(mTheta);
	float y = mRadius * cosf(mPhi);

	// 构建观察矩阵
	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;

	// 用最新的 worldViewProj 矩阵来更新常量缓冲区
	ObjectConstants objConstants;
	XMStoreFloat4x4(&objConstants.WorldViewProj, XMMatrixTranspose(worldViewProj));
	mObjectCB->CopyData(0, objConstants);

}

void MyBox::Draw(const GameTimer& gt)
{
	ThrowIfFailed(mDirectCmdListAlloc->Reset());
	ThrowIfFailed(mCommandList->Reset(mDirectCmdListAlloc.Get(), mPSO.Get()));

	mCommandList->RSSetViewports(1,&mScreenViewport);
	mCommandList->RSSetScissorRects(1,&mScissorRect);
	//指示资源使用情况的状态转换。
	mCommandList->ResourceBarrier(1,&CD3DX12_RESOURCE_BARRIER::Transition(CurrentBackBuffer(),D3D12_RESOURCE_STATE_PRESENT,D3D12_RESOURCE_STATE_RENDER_TARGET));
	//清理后台缓冲区和深度缓冲区
	mCommandList->ClearRenderTargetView(CurrentBackBufferView(),Colors::LightSteelBlue,0,nullptr);
	mCommandList->ClearDepthStencilView(DepthStencilView(),D3D12_CLEAR_FLAG_DEPTH | D3D12_CLEAR_FLAG_STENCIL,1.0f,0,0,nullptr);
	//指定要渲染的缓冲区
	mCommandList->OMSetRenderTargets(1 , &CurrentBackBufferView(),true, &DepthStencilView());

	//指定cbv描述符堆成员为一个
	ID3D12DescriptorHeap* descriptorHeaps[]={mCbvHeap.Get()};
	mCommandList->SetDescriptorHeaps(_countof(descriptorHeaps),descriptorHeaps);
	//设置根签名
	mCommandList->SetGraphicsRootSignature(mRootSignature.Get());

	//顶点缓冲区视图和渲染流水线上的一个输入槽绑定
	mCommandList->IASetVertexBuffers(0,1,&mBoxGeo->VertexBufferView());
	mCommandList->IASetIndexBuffer(&mBoxGeo->IndexBufferView());
	mCommandList->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);//这里是DX11版本,没有12版本

	//描述符表与渲染流水线绑定
	mCommandList->SetGraphicsRootDescriptorTable(0,mCbvHeap->GetGPUDescriptorHandleForHeapStart());

	mCommandList->DrawIndexedInstanced(mBoxGeo->DrawArgs["box"].IndexCount,1,0,0,0);

	mCommandList->ResourceBarrier(1,&CD3DX12_RESOURCE_BARRIER::Transition(CurrentBackBuffer(), D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT));


	ThrowIfFailed(mCommandList->Close());

	//提交命令列表,交换前后台缓冲区
	ID3D12CommandList* cmdLists[]={mCommandList.Get()};
	mCommandQueue->ExecuteCommandLists(_countof(cmdLists),cmdLists);


	ThrowIfFailed(mSwapChain->Present(0,0));
	mCurrBackBuffer=(mCurrBackBuffer+1)%SwapChainBufferCount;

	FlushCommandQueue();

}

void MyBox::OnResize()
{
	DXApp::OnResize();

	// The window resized, so update the aspect ratio and recompute the projection matrix.
	XMMATRIX P = XMMatrixPerspectiveFovLH(0.25f*MathHelper::Pi, AspectRatio(), 1.0f, 1000.0f);
	XMStoreFloat4x4(&mProj, P);
}

void MyBox::OnMouseDown(WPARAM btnState, int x, int y)
{
	mLastMousePos.x = x;
	mLastMousePos.y = y;

	SetCapture(mhMainWnd);
}

void MyBox::OnMouseUp(WPARAM btnState, int x, int y)
{
	ReleaseCapture();
}

void MyBox::OnMouseMove(WPARAM btnState, int x, int y)
{
	if ((btnState & MK_LBUTTON) != 0)
	{
		//  根据鼠标的移动距离计算旋转角度,令每个像素按此角度的1/4旋转
		float dx = XMConvertToRadians(0.25f*static_cast<float>(x - mLastMousePos.x));
		float dy = XMConvertToRadians(0.25f*static_cast<float>(y - mLastMousePos.y));

		// 根据鼠标输入来更新摄像机绕立方体旋转的角度
		mTheta -= dx;
		mPhi -= dy;

		//限制角度mPhi的范围
		mPhi = MathHelper::Clamp(mPhi, 0.1f, MathHelper::Pi - 0.1f);
	}
	else if ((btnState & MK_RBUTTON) != 0)
	{
		// 使场景中的每个像素按鼠标移动距离的0.005倍进行缩放
		float dx = 0.005f*static_cast<float>(x - mLastMousePos.x);
		float dy = 0.005f*static_cast<float>(y - mLastMousePos.y);

		// 根据鼠标的输入更新摄像机的可视范围半径
		mRadius += dx - dy;

		// 限制可视半径范围
		mRadius = MathHelper::Clamp(mRadius, 3.0f, 15.0f);
	}

	mLastMousePos.x = x;
	mLastMousePos.y = y;
}

void MyBox::BuildDescriptorHeaps()
{
	D3D12_DESCRIPTOR_HEAP_DESC cbvHeapDesc;
	cbvHeapDesc.Flags=D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;//创建着色器程序访问的描述符时需要指定visible
	cbvHeapDesc.NodeMask=0;
	cbvHeapDesc.NumDescriptors=1;
	cbvHeapDesc.Type=D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
	ThrowIfFailed(md3dDevice->CreateDescriptorHeap(&cbvHeapDesc,IID_PPV_ARGS(&mCbvHeap)));

}

void MyBox::BuildConstantBuffers()
{ 
	//此常量缓冲区存储了绘制n个物体所需的常量数据
	mObjectCB=std::make_unique<`UploadBuffer<ObjectConstants>`>(md3dDevice.Get(),1,true);

	UINT objCBByteSize=d3dUtil::CalcConstantBufferByteSize(sizeof(ObjectConstants));
	//缓冲区的起始地址	
	D3D12_GPU_VIRTUAL_ADDRESS cbAddress=mObjectCB->Resource()->GetGPUVirtualAddress();

	int boxCBufIndex=0;
	cbAddress += boxCBufIndex*objCBByteSize;

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

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

void MyBox::BuildRootSignature()
{
	CD3DX12_ROOT_PARAMETER slotRootParameter[1];//根签名表

	CD3DX12_DESCRIPTOR_RANGE cbvTable;
	//1:表中的描述符数量,0:将此段描述符区域绑定至此基准着色器寄存器
	cbvTable.Init(D3D12_DESCRIPTOR_RANGE_TYPE_CBV,1,0);
	//1:描述符区域的数量;table:指向描述符区域数组的指针
	slotRootParameter[0].InitAsDescriptorTable(1,&cbvTable);
	// 根签名由一组根参数构成
	CD3DX12_ROOT_SIGNATURE_DESC rootSigDesc(1,slotRootParameter,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.GetAddressOf(),errorBlob.GetAddressOf());

	if (errorBlob!=nullptr)
	{
		::OutputDebugStringA((char*)errorBlob->GetBufferPointer());
	}
	ThrowIfFailed(hr);

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

void MyBox::BuildShadersAndInputLayout()
{
	HRESULT hr = S_OK;
	mvsByteCode = d3dUtil::CompileShader(L"Shaders\\color.hlsl", nullptr, "VS", "vs_5_0");
	mpsByteCode = d3dUtil::CompileShader(L"Shaders\\color.hlsl", nullptr, "PS", "ps_5_0");

	mInputLayout=
	{
		{"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}
	};
}

void MyBox::BuildBoxGeometry()
{
	std::array<Vertex,8> verteices=
	{
		Vertex({ XMFLOAT3(-1.0f, -1.0f, -1.0f), XMFLOAT4(Colors::Red) }),
		Vertex({ XMFLOAT3(-1.0f, +1.0f, -1.0f), XMFLOAT4(Colors::Green) }),
		Vertex({ XMFLOAT3(+1.0f, +1.0f, -1.0f), XMFLOAT4(Colors::Blue) }),
		Vertex({ XMFLOAT3(+1.0f, -1.0f, -1.0f), XMFLOAT4(Colors::White) }),
		Vertex({ XMFLOAT3(-1.0f, -1.0f, +1.0f), XMFLOAT4(Colors::Blue) }),
		Vertex({ XMFLOAT3(-1.0f, +1.0f, +1.0f), XMFLOAT4(Colors::White) }),
		Vertex({ XMFLOAT3(+1.0f, +1.0f, +1.0f), XMFLOAT4(Colors::Red) }),
		Vertex({ XMFLOAT3(+1.0f, -1.0f, +1.0f), XMFLOAT4(Colors::Green) })
	};
	std::array<std::uint16_t, 36> 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 vbByteSize = (UINT)verteices.size() * sizeof(Vertex);
	const UINT ibByteSize = (UINT)indices.size() * sizeof(std::uint16_t);

	mBoxGeo=std::make_unique<MeshGeometry>();
	mBoxGeo->Name = "boxGeo";

	ThrowIfFailed(D3DCreateBlob(vbByteSize,&mBoxGeo->VertexBufferCPU));
	CopyMemory(mBoxGeo->VertexBufferCPU->GetBufferPointer(),verteices.data(),vbByteSize);

	ThrowIfFailed(D3DCreateBlob(ibByteSize, &mBoxGeo->IndexBufferCPU));
	CopyMemory(mBoxGeo->IndexBufferCPU->GetBufferPointer(), indices.data(), ibByteSize);

	//创建顶点/索引默认缓冲区
	mBoxGeo->VertexBufferGPU = d3dUtil::CreateDefaultBuffer(
	md3dDevice.Get(),mCommandList.Get(),verteices.data(),vbByteSize,mBoxGeo->VertexBufferUploader);
	mBoxGeo->IndexBufferGPU= d3dUtil::CreateDefaultBuffer(
	md3dDevice.Get(),mCommandList.Get(),indices.data(),ibByteSize,mBoxGeo->IndexBufferUploader);
	//缓冲区数据
	//字节跨度
	mBoxGeo->VertexByteStride=sizeof(Vertex);
	mBoxGeo->VertexBufferByteSize=vbByteSize;
	mBoxGeo->IndexFormat=DXGI_FORMAT_R16_UINT;
	mBoxGeo->IndexBufferByteSize=ibByteSize;

	SubmeshGeometry subMesh;
	subMesh.IndexCount=(UINT) indices.size();
	subMesh.StartIndexLocation=0;
	subMesh.BaseVertexLocation=0;
	

	// 一个结构体能够存储一组顶点/索引缓冲区中的多个几何体
	//用下列容器来定义子网格几何体,就能单独绘制其中的子网格
	mBoxGeo->DrawArgs["box"]=subMesh;
}

void MyBox::BuildPSO()
{
	//流水线状态描述
	D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc;
	//将数据的缓冲区用0来填充,使用一块内存区域前对其进行清空处理可以避免内存数据的不确定性。
	ZeroMemory(&psoDesc,sizeof(D3D12_GRAPHICS_PIPELINE_STATE_DESC));
	psoDesc.InputLayout={mInputLayout.data(),(UINT)mInputLayout.size()};
	psoDesc.pRootSignature=mRootSignature.Get();
	psoDesc.VS=
	{
		reinterpret_cast<BYTE*>(mvsByteCode->GetBufferPointer()),
		mvsByteCode->GetBufferSize()
	};
	psoDesc.PS=
	{
		reinterpret_cast<BYTE*>(mpsByteCode->GetBufferPointer()),
		mpsByteCode->GetBufferSize()
	};
	psoDesc.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT);	//光栅化状态
	psoDesc.BlendState= CD3DX12_BLEND_DESC(D3D12_DEFAULT);
	psoDesc.DepthStencilState = CD3DX12_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?(m4xMsaaState-1):0;
	psoDesc.DSVFormat=mDepthStencilFormat;
	ThrowIfFailed(md3dDevice->CreateGraphicsPipelineState(&psoDesc,IID_PPV_ARGS(&mPSO)));


}

bool MyBox::Initialize()
{
	if (!DXApp::Initialize())
	{
		return false;
	}
	//准备初始化命令之前线重置命令列表
	ThrowIfFailed(mCommandList->Reset(mDirectCmdListAlloc.Get(),nullptr));

	BuildDescriptorHeaps();
	BuildConstantBuffers();
	BuildRootSignature();
	BuildShadersAndInputLayout();
	BuildBoxGeometry();
	BuildPSO();

	ThrowIfFailed(mCommandList->Close());
	ID3D12CommandList* cmdLists[]={mCommandList.Get()};
	mCommandQueue->ExecuteCommandLists(_countof(cmdLists),cmdLists);

	FlushCommandQueue();

	return true;
}

int WINAPI WinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPSTR lpCmdLine, _In_ int nShowCmd)
{

	// Enable run-time memory check for debug builds.
#if defined(DEBUG) | defined(_DEBUG)
	_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
#endif
	try
	{
		MyBox theApp(hInstance);
		if (!theApp.Initialize())
			return 0;

		return theApp.Run();
	}
	catch (DxException& e)
	{
	
		MessageBox(nullptr, e.ToString().c_str(), L"HR Failed", MB_OK);

		return 0;
	}



}