DX12 快速教程(7) —— 渲染火柴盒


在这里插入图片描述




新建项目 “007-RenderMatchBox”


在这里插入图片描述


在项目文件夹下新建一个叫 “resource” 的文件夹,解压纹理压缩包 “007-RenderMatchBox.zip” 到文件夹里:


在这里插入图片描述

在这里插入图片描述


如果读者需要找项目源代码 (文件夹下项目源代码是 “DX12 项目源代码 (xx.xx.xx 更新).zip”),也可以点击下方的链接,我们之后更新代码,都会发布到下面这个链接中:
纹理压缩包地址:https://wwek.lanzoue.com/b002uxwd3a 密码:dgaf


画方块


如何让方块画上不同的纹理: SRVHandle


在这里插入图片描述


如果要画不同的纹理,只需要更改对应的 SRV_GPUHandle,让 GPUHandle 指向 Heap 上不同的 SRV 描述符,然后执行 SetGraphicsRootDescriptorTable 改变根参数绑定的描述符,再记录一次 DrawCall 就行:


在这里插入图片描述

// 渲染上面
SRV_GPUHandle += SRVDescriptorSize;
pCommandList->SetGraphicsRootDescriptorTable(1, SRV_GPUHandle);
pCommandList->DrawIndexedInstanced(6, 1, 24, 0, 0);

// 渲染下面
SRV_GPUHandle += SRVDescriptorSize;
pCommandList->SetGraphicsRootDescriptorTable(1, SRV_GPUHandle);
pCommandList->DrawIndexedInstanced(6, 1, 30, 0, 0);

// 渲染左右正背面
SRV_GPUHandle += SRVDescriptorSize;
pCommandList->SetGraphicsRootDescriptorTable(1, SRV_GPUHandle);
pCommandList->DrawIndexedInstanced(24, 1, 0, 0, 0);

顺带一提,在描述符堆创建完成时,堆上 CPU 和 GPU 描述符的地址就已经确定了,只要描述符堆没有释放重建,那么这些描述符的 CPU 与 GPU 地址就不会发生改变,而且是一一对应的。利用这一性质,下文我们创建纹理,绑定到每个模型上时,直接将对应的 GPUHandle 传递给模型存储,这样就不用再麻烦地进行 SRV_GPUHandle += SRVDescriptorSize 从头获取起始句柄,一个一个偏移了,加速渲染。


万物起源:Model 模型类 (抽象层)


在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


涉及到 3D 的领域都会出现很多奇形怪状,大小不一,材质多样的 3D 模型,例如人物模型,动物模型,武器模型,机械模型等等,方块也属于模型。将来我们画的可不只是方块,还有球体,柱体,甚至更复杂的人物模型。

万事开头难,MC 有各种各样的方块,光靠一个 class 是没办法统一绘制的,难道我们需要一个一个麻烦地绘制吗?有没有一种方法能通用地表示方块?不用着急,我们可以利用 Abstract Class 抽象类,先设计一个通用的抽象类 Model 模型类 ,再设计派生类实现 Model 的抽象方法,这样就方便对方块进行拓展与复用了:


在这里插入图片描述


// 顶点
struct VERTEX
{
	XMFLOAT4 position;			// 顶点在模型坐标系的坐标
	XMFLOAT2 texcoordUV;		// 顶点纹理 UV 坐标
};


// 模型类,这是个抽象类,有两个纯虚函数,派生类需要实现下面两个纯虚函数才能创建实例
class Model
{
protected:	// 保护项,仅在自身和派生类可见

	XMMATRIX ModelMatrix = XMMatrixIdentity();			// 模型矩阵,模型空间 -> 世界空间

	ComPtr<ID3D12Resource> m_VertexResource;			// D3D12 顶点资源
	ComPtr<ID3D12Resource> m_ModelMatrixResource;		// D3D12 模型矩阵资源
	ComPtr<ID3D12Resource> m_IndexResource;				// D3D12 索引资源

	// 每个模型的 VBV 顶点信息描述符数组,数组每个元素占用一个输入槽,多槽输入可以加速 CPU-GPU 的传递
	// VertexBufferView[0] 描述每个顶点的顶点信息 (position 位置,texcoordUV 纹理 UV 坐标)
	// VertexBufferView[1] 描述每个顶点对应的模型矩阵,模型矩阵会在 IA 阶段拆分成四个行向量进行输入,之后在 VS 阶段重新组装成矩阵
	D3D12_VERTEX_BUFFER_VIEW VertexBufferView[2] = {};

	// 每个模型的 IBV 顶点索引描述符,一个模型只有一个索引描述符
	D3D12_INDEX_BUFFER_VIEW IndexBufferView = {};

	// 纹理名 - GPU 句柄映射表,用于索引纹理,设置根参数
	std::unordered_map<std::string, D3D12_GPU_DESCRIPTOR_HANDLE> Texture_GPUHandle_Map;

	// 添加新纹理 (key) 到映射表,对应的 GPUHandle 先设置为 nullptr,仅限派生类可用,对外不公开
	void AppendTextureKey(std::string&& TextureName)
	{
		Texture_GPUHandle_Map[TextureName] = {};
	}

public:		// 公共项,全局均可见

	// 类外获取模型需要的纹理,返回映射表的只读引用
	const std::unordered_map<std::string, D3D12_GPU_DESCRIPTOR_HANDLE>& RequestForTextureMap()
	{
		return Texture_GPUHandle_Map;
	}

	// 模型获取类外已经创建纹理 SRV 描述符的 SRVHandle
	void SetTextureGPUHandle(std::string TextureName, D3D12_GPU_DESCRIPTOR_HANDLE GPUHandle)
	{
		Texture_GPUHandle_Map[TextureName] = GPUHandle;
	}

	// 获取模型矩阵
	XMMATRIX GetModelMatrix()
	{
		return ModelMatrix;
	}

	// 设置模型矩阵
	void SetModelMatrix(XMMATRIX Matrix)
	{
		ModelMatrix = Matrix;
	}

	// 创建资源与描述符,这个是纯虚函数,实例类需要实现
	virtual void CreateResourceAndDescriptor(ComPtr<ID3D12Device4>& pD3D12Device) = 0;

	// 绘制模型,这个也是纯虚函数,实例类需要实现
	virtual void DrawModel(ComPtr<ID3D12GraphicsCommandList>& pCommandList) = 0;

};

注意看这两个函数:

	// 创建资源与描述符,这个是纯虚函数,实例类需要实现
	virtual void CreateResourceAndDescriptor(ComPtr<ID3D12Device4>& pD3D12Device) = 0;

	// 绘制模型,这个也是纯虚函数,实例类需要实现
	virtual void DrawModel(ComPtr<ID3D12GraphicsCommandList>& pCommandList) = 0;

这两个函数叫 Abstract Function 抽象函数 (也叫 Pure Virtual Function 纯虚函数),表示派生类拥有、需要实现的共用方法,这两个抽象函数不能在模型类定义,是需要我们在派生类手动实现的。Model 类拥有两个抽象函数,所以 Model 类不能被实例化 (不能用 new 创建,不能调用构造函数),这种类我们叫 Abstract Class 抽象类。

利用抽象类,我们就能表示一个模型了,不管模型长什么样。所以 Model 属于第一层:抽象层。



中流砥柱:SoildBlock, SoildStair (半实现层)


在这里插入图片描述


观察上图,很多方块的顶点信息和顶点索引是相同的,为了节省内存资源,我们可以写两个类 SoildBlock (固体方块类)、SoildStair (台阶方块类) 来继承 Model 类,添加两个静态成员 VertexArray、IndexArray,这样很多方块就能共用顶点,不用重复创建了。

我们还可以实现上文 Model 类的抽象方法 CreateResourceAndDescriptor 来创建顶点、索引资源与 VertexBufferView,IndexBufferView 这些描述符:

// 全遮挡固体方块类 (抽象类),继承自模型类,只定义 CreateResourceAndDescriptor 这个函数,DrawModel 仍然需要派生类实现
class SoildBlock : public Model
{
protected:	// 保护项,仅在自身和派生类可见

	// CPU 高速缓存上的顶点信息数组 (静态成员,属于类,不属于类实例对象,只初始化一次)
	// 注意 DirectX 使用的是左手坐标系,写顶点信息时,请比一比你的左手!
	inline static VERTEX VertexArray[24] =
	{
		// 正面
		{{0,1,0,1},{0,0}},
		{{1,1,0,1},{1,0}},
		{{1,0,0,1},{1,1}},
		{{0,0,0,1},{0,1}},

		// 背面
		{{1,1,1,1},{0,0}},
		{{0,1,1,1},{1,0}},
		{{0,0,1,1},{1,1}},
		{{1,0,1,1},{0,1}},

		// 左面
		{{0,1,1,1},{0,0}},
		{{0,1,0,1},{1,0}},
		{{0,0,0,1},{1,1}},
		{{0,0,1,1},{0,1}},

		// 右面
		{{1,1,0,1},{0,0}},
		{{1,1,1,1},{1,0}},
		{{1,0,1,1},{1,1}},
		{{1,0,0,1},{0,1}},

		// 上面
		{{0,1,1,1},{0,0}},
		{{1,1,1,1},{1,0}},
		{{1,1,0,1},{1,1}},
		{{0,1,0,1},{0,1}},

		// 下面
		{{0,0,0,1},{0,0}},
		{{1,0,0,1},{1,0}},
		{{1,0,1,1},{1,1}},
		{{0,0,1,1},{0,1}}
	};

	// 顶点索引数组 (静态成员,属于类,不属于类实例对象,只初始化一次)
	// 注意这里的 UINT == UINT32,后面填的格式 (步长) 必须是 DXGI_FORMAT_R32_UINT,否则会出错
	inline static UINT IndexArray[36] =
	{
		// 正面
		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
	};


public:		// 公共项,全局均可见

	// 创建资源与描述符,函数声明的 override 在编译期能检查虚函数是否被重写,重写/实现函数常写 override 是一种好习惯
	virtual void CreateResourceAndDescriptor(ComPtr<ID3D12Device4>& pD3D12Device) override
	{
		// 临时设置 XMFLOAT4X4 类型的模型矩阵,XMFLOAT4X4 擅长存储与传递,XMMATRIX 擅长并行运算
		XMFLOAT4X4 _temp_ModelMatrix = {};
		XMStoreFloat4x4(&_temp_ModelMatrix, ModelMatrix);

		// 用于批量复制模型矩阵的 vector,vector 的底层是一块连续内存,memcpy 复制连续内存有 CPU 优化,能快很多
		std::vector<XMFLOAT4X4> _temp_ModelMatrixGroup;
		// 批量填充 ModelMatrix 到 ModelMatrixGroup
		_temp_ModelMatrixGroup.assign(24, _temp_ModelMatrix);


		// 用于创建上传堆资源的 D3D12Resource 信息结构体,这个结构体可复用
		D3D12_RESOURCE_DESC UploadResourceDesc = {};
		UploadResourceDesc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER;		// 上传堆资源类型都是 BUFFER 缓冲
		UploadResourceDesc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR;			// row_major 行主序,上传堆资源都是按行存储的
		UploadResourceDesc.Height = 1;										// 上传堆资源高度都是 1 (线性存储)
		UploadResourceDesc.Format = DXGI_FORMAT_UNKNOWN;					// 上传堆资源都是 DXGI_FORMAT_UNKNOWN
		UploadResourceDesc.MipLevels = 1;									// 上传堆资源没有 Mipmap,所以都设 1
		UploadResourceDesc.DepthOrArraySize = 1;							// 上传堆资源都设 1
		UploadResourceDesc.SampleDesc.Count = 1;							// 采样次数,上传堆资源都设 1
		UploadResourceDesc.SampleDesc.Quality = 0;							// 采样质量,上传堆资源都设 0

		// 上传堆属性
		D3D12_HEAP_PROPERTIES HeapProperties = { D3D12_HEAP_TYPE_UPLOAD };

		// 创建 VertexResource
		UploadResourceDesc.Width = 24 * sizeof(VERTEX);		// 宽度就是 VertexGroup 总元素大小

		// 以隐式堆方式创建资源,好处是简单方便,坏处是隐式堆由操作系统全权管理,开发者无法手动管理隐式堆的属性和生命周期
		pD3D12Device->CreateCommittedResource(&HeapProperties, D3D12_HEAP_FLAG_NONE,
			&UploadResourceDesc, D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&m_VertexResource));

		// 创建 ModelMatrix
		UploadResourceDesc.Width = 24 * sizeof(XMFLOAT4X4);

		pD3D12Device->CreateCommittedResource(&HeapProperties, D3D12_HEAP_FLAG_NONE,
			&UploadResourceDesc, D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&m_ModelMatrixResource));

		// 创建 IndexResource
		UploadResourceDesc.Width = 36 * sizeof(XMFLOAT4X4);

		pD3D12Device->CreateCommittedResource(&HeapProperties, D3D12_HEAP_FLAG_NONE,
			&UploadResourceDesc, D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&m_IndexResource));


		// 将数据进行转移: CPU 高速缓存 -> CPU 共享内存

		BYTE* TransmitPointer = nullptr;	// 用于传递数据的指针

		// 映射资源,获取 D3D12 资源的地址,同时 D3D12 资源开放写权限
		m_VertexResource->Map(0, nullptr, reinterpret_cast<void**>(&TransmitPointer));
		// 用 memcpy 将数据复制到 D3D12 资源
		memcpy(TransmitPointer, VertexArray, 24 * sizeof(VERTEX));
		// 关闭映射,资源只写,这样静态资源读取效率会快很多,动态资源无需关闭映射
		m_VertexResource->Unmap(0, nullptr);


		m_ModelMatrixResource->Map(0, nullptr, reinterpret_cast<void**>(&TransmitPointer));
		memcpy(TransmitPointer, &_temp_ModelMatrixGroup[0], 24 * sizeof(XMFLOAT4X4));
		m_ModelMatrixResource->Unmap(0, nullptr);


		m_IndexResource->Map(0, nullptr, reinterpret_cast<void**>(&TransmitPointer));
		memcpy(TransmitPointer, IndexArray, 36 * sizeof(UINT));
		m_IndexResource->Unmap(0, nullptr);


		// 创建完所有资源,就可以填写 VBV 和 IBV 描述符了

		// VBV[0]: 描述顶点位置与纹理 UV 坐标,占据第 0 号输入槽
		VertexBufferView[0].BufferLocation = m_VertexResource->GetGPUVirtualAddress();		// D3D12 资源地址
		VertexBufferView[0].SizeInBytes = 24 * sizeof(VERTEX);								// D3D12 资源总大小
		VertexBufferView[0].StrideInBytes = sizeof(VERTEX);									// D3D12 资源单个元素的大小 (步长)

		// VBV[1]: 描述顶点的模型矩阵,占据第 1 号输入槽
		VertexBufferView[1].BufferLocation = m_ModelMatrixResource->GetGPUVirtualAddress();	// D3D12 资源地址
		VertexBufferView[1].SizeInBytes = 24 * sizeof(XMFLOAT4X4);							// D3D12 资源总大小
		VertexBufferView[1].StrideInBytes = sizeof(XMFLOAT4X4);								// D3D12 资源单个元素的大小 (步长)

		// IBV: 描述顶点的索引
		IndexBufferView.BufferLocation = m_IndexResource->GetGPUVirtualAddress();			// D3D12 资源地址
		IndexBufferView.SizeInBytes = 36 * sizeof(UINT);									// D3D12 资源总大小
		IndexBufferView.Format = DXGI_FORMAT_R32_UINT;										// D3D12 资源单个元素的大小 (步长)
	}
};


// 台阶方块类 (抽象类),继承自模型类,只定义 CreateResourceAndDescriptor 这个函数,DrawModel 仍然需要派生类实现
class SoildStair : public Model
{
protected:	// 保护项,仅在自身和派生类可见

	// CPU 高速缓存上的顶点信息数组 (静态成员,属于类,不属于类实例对象,只初始化一次)
	// 注意 DirectX 使用的是左手坐标系,写顶点信息时,请比一比你的左手!
	inline static VERTEX VertexArray[40] =
	{
		// 台阶底面
		{{0,0,0,1},{0,0}},
		{{1,0,0,1},{1,0}},
		{{1,0,1,1},{1,1}},
		{{0,0,1,1},{0,1}},

		// 台阶背面
		{{1,1,1,1},{0,0}},
		{{0,1,1,1},{1,0}},
		{{0,0,1,1},{1,1}},
		{{1,0,1,1},{0,1}},

		// 台阶正面
		{{0,0.5,0,1},{0,0.5}},
		{{1,0.5,0,1},{1,0.5}},
		{{1,0,0,1},{1,1}},
		{{0,0,0,1},{0,1}},

		{{0,1,0.5,1},{0,0}},
		{{1,1,0.5,1},{1,0}},
		{{1,0.5,0.5,1},{1,0.5}},
		{{0,0.5,0.5,1},{0,0.5}},

		// 台阶顶面
		{{0,0.5,0.5,1},{0,0.5}},
		{{1,0.5,0.5,1},{1,0.5}},
		{{1,0.5,0,1},{1,1}},
		{{0,0.5,0,1},{0,1}},

		{{0,1,1,1},{0,0}},
		{{1,1,1,1},{1,0}},
		{{1,1,0.5,1},{1,0.5}},
		{{0,1,0.5,1},{0,0.5}},

		// 台阶左面
		{{0,1,1,1},{0,0}},
		{{0,1,0.5,1},{0.5,0}},
		{{0,0,0.5,1},{0.5,1}},
		{{0,0,1,1},{0,1}},

		{{0,0.5,0.5,1},{0.5,0.5}},
		{{0,0.5,0,1},{1,0.5}},
		{{0,0,0,1},{1,1}},
		{{0,0,0.5,1},{0.5,1}},

		// 台阶右面
		{{1,1,0.5,1},{0.5,0}},
		{{1,1,1,1},{1,0}},
		{{1,0,1,1},{1,1}},
		{{1,0,0.5,1},{0.5,1}},

		{{1,0.5,0,1},{0,0.5}},
		{{1,0.5,0.5,1},{0.5,0.5}},
		{{1,0,0.5,1},{0.5,1}},
		{{1,0,0,1},{0,1}}
	};

	// 顶点索引数组 (静态成员,属于类,不属于类实例对象,只初始化一次)
	// 注意这里的 UINT == UINT32,后面填的格式 (步长) 必须是 DXGI_FORMAT_R32_UINT,否则会出错
	inline static UINT IndexArray[60] =
	{
		// 台阶底面
		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,
		// 台阶左面
		24,25,26,24,26,27,
		28,29,30,28,30,31,
		// 台阶右面
		32,33,34,32,34,35,
		36,37,38,36,38,39
	};


public:		// 公共项,全局均可见

	// 创建资源与描述符,函数声明的 override 在编译期能检查虚函数是否被重写,重写/实现函数常写 override 是一种好习惯
	virtual void CreateResourceAndDescriptor(ComPtr<ID3D12Device4>& pD3D12Device) override
	{
		// 临时设置 XMFLOAT4X4 类型的模型矩阵,XMFLOAT4X4 擅长存储与传递,XMMATRIX 擅长并行运算
		XMFLOAT4X4 _temp_ModelMatrix = {};
		XMStoreFloat4x4(&_temp_ModelMatrix, ModelMatrix);

		// 用于批量复制模型矩阵的 vector,vector 的底层是一块连续内存,memcpy 复制连续内存有 CPU 优化,能快很多
		std::vector<XMFLOAT4X4> _temp_ModelMatrixGroup;
		// 批量填充 ModelMatrix 到 ModelMatrixGroup
		_temp_ModelMatrixGroup.assign(40, _temp_ModelMatrix);


		// 用于创建上传堆资源的 D3D12Resource 信息结构体,这个结构体可复用
		D3D12_RESOURCE_DESC UploadResourceDesc = {};
		UploadResourceDesc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER;		// 上传堆资源类型都是 BUFFER 缓冲
		UploadResourceDesc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR;			// row_major 行主序,上传堆资源都是按行存储的
		UploadResourceDesc.Height = 1;										// 上传堆资源高度都是 1 (线性存储)
		UploadResourceDesc.Format = DXGI_FORMAT_UNKNOWN;					// 上传堆资源都是 DXGI_FORMAT_UNKNOWN
		UploadResourceDesc.MipLevels = 1;									// 上传堆资源没有 Mipmap,所以都设 1
		UploadResourceDesc.DepthOrArraySize = 1;							// 上传堆资源都设 1
		UploadResourceDesc.SampleDesc.Count = 1;							// 采样次数,上传堆资源都设 1
		UploadResourceDesc.SampleDesc.Quality = 0;							// 采样质量,上传堆资源都设 0

		// 上传堆属性
		D3D12_HEAP_PROPERTIES HeapProperties = { D3D12_HEAP_TYPE_UPLOAD };

		// 创建 VertexResource
		UploadResourceDesc.Width = 40 * sizeof(VERTEX);		// 宽度就是 VertexGroup 总元素大小

		// 以隐式堆方式创建资源,好处是简单方便,坏处是隐式堆由操作系统全权管理,开发者无法手动管理隐式堆的属性和生命周期
		pD3D12Device->CreateCommittedResource(&HeapProperties, D3D12_HEAP_FLAG_NONE,
			&UploadResourceDesc, D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&m_VertexResource));

		// 创建 ModelMatrix
		UploadResourceDesc.Width = 40 * sizeof(XMFLOAT4X4);

		pD3D12Device->CreateCommittedResource(&HeapProperties, D3D12_HEAP_FLAG_NONE,
			&UploadResourceDesc, D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&m_ModelMatrixResource));

		// 创建 IndexResource
		UploadResourceDesc.Width = 60 * sizeof(XMFLOAT4X4);

		pD3D12Device->CreateCommittedResource(&HeapProperties, D3D12_HEAP_FLAG_NONE,
			&UploadResourceDesc, D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&m_IndexResource));


		// 将数据进行转移: CPU 高速缓存 -> CPU 共享内存

		BYTE* TransmitPointer = nullptr;	// 用于传递数据的指针

		// 映射资源,获取 D3D12 资源的地址,同时 D3D12 资源开放写权限
		m_VertexResource->Map(0, nullptr, reinterpret_cast<void**>(&TransmitPointer));
		// 用 memcpy 将数据复制到 D3D12 资源
		memcpy(TransmitPointer, VertexArray, 40 * sizeof(VERTEX));
		// 关闭映射,资源只写,这样静态资源读取效率会快很多,动态资源无需关闭映射
		m_VertexResource->Unmap(0, nullptr);


		m_ModelMatrixResource->Map(0, nullptr, reinterpret_cast<void**>(&TransmitPointer));
		memcpy(TransmitPointer, &_temp_ModelMatrixGroup[0], 40 * sizeof(XMFLOAT4X4));
		m_ModelMatrixResource->Unmap(0, nullptr);


		m_IndexResource->Map(0, nullptr, reinterpret_cast<void**>(&TransmitPointer));
		memcpy(TransmitPointer, IndexArray, 60 * sizeof(UINT));
		m_IndexResource->Unmap(0, nullptr);


		// 创建完所有资源,就可以填写 VBV 和 IBV 描述符了

		// VBV[0]: 描述顶点位置与纹理 UV 坐标,占据第 0 号输入槽
		VertexBufferView[0].BufferLocation = m_VertexResource->GetGPUVirtualAddress();		// D3D12 资源地址
		VertexBufferView[0].SizeInBytes = 40 * sizeof(VERTEX);								// D3D12 资源总大小
		VertexBufferView[0].StrideInBytes = sizeof(VERTEX);									// D3D12 资源单个元素的大小 (步长)

		// VBV[1]: 描述顶点的模型矩阵,占据第 1 号输入槽
		VertexBufferView[1].BufferLocation = m_ModelMatrixResource->GetGPUVirtualAddress();	// D3D12 资源地址
		VertexBufferView[1].SizeInBytes = 40 * sizeof(XMFLOAT4X4);							// D3D12 资源总大小
		VertexBufferView[1].StrideInBytes = sizeof(XMFLOAT4X4);								// D3D12 资源单个元素的大小 (步长)

		// IBV: 描述顶点的索引
		IndexBufferView.BufferLocation = m_IndexResource->GetGPUVirtualAddress();			// D3D12 资源地址
		IndexBufferView.SizeInBytes = 60 * sizeof(UINT);									// D3D12 资源总大小
		IndexBufferView.Format = DXGI_FORMAT_R32_UINT;										// D3D12 资源单个元素的大小 (步长)
	}
};

可是它们仍然不能实例化,原因是还没实现 DrawModel 这个函数,为什么我们不在这两个类上实现这个函数呢?原因是不同的方块要画不同的纹理,需求不一,有的只需要画一个,有的需要画很多个,甚至有些还要实现附加功能。设计中间层并分离能降低类之间的耦合性,能让设计和使用变得简单,功能更好拓展。

SoildBlock 和 SoildStair 为方块定义了通用的顶点和索引数据,仅实现方块资源的加载,不实现方块的绘制,所以它们属于第二层:半实现层。


百川归海:各种实例方块类 (实现层)


在这里插入图片描述


最后,我们继承半实现层那两个类,在构造函数上用 AppendTextureKey 记录要渲染的纹理,实现 DrawModel 函数并进行拓展,就大功告成了:


// 泥土 (实例类),继承自全遮挡固体方块
class Dirt : public SoildBlock
{
public:

	// 构造函数,调用 AppendTextureKey 添加需要的纹理
	Dirt()
	{
		this->AppendTextureKey("dirt");
	}

	// 绘制模型,函数声明的 override 在编译期能检查虚函数是否被重写,重写/实现函数常写 override 是一种好习惯
	virtual void DrawModel(ComPtr<ID3D12GraphicsCommandList>& pCommandList) override
	{
		// 设置 IBV 索引缓冲描述符
		pCommandList->IASetIndexBuffer(&IndexBufferView);
		// 设置 VBV 顶点缓冲描述符,注意我们这里使用了多槽输入!
		pCommandList->IASetVertexBuffers(0, 2, VertexBufferView);

		// 设置根参数,将纹理 SRV 描述符设置到 GPU 的寄存器上,这样着色器就可以找到纹理了
		pCommandList->SetGraphicsRootDescriptorTable(1, Texture_GPUHandle_Map["dirt"]);

		// Draw Call 渲染!
		pCommandList->DrawIndexedInstanced(36, 1, 0, 0, 0);
	}
};


// 橡木木板 (实例类),继承自全遮挡固体方块
class Planks_Oak : public SoildBlock
{
public:

	// 构造函数,调用 AppendTextureKey 添加需要的纹理
	Planks_Oak()
	{
		this->AppendTextureKey("planks_oak");
	}

	// 绘制模型,函数声明的 override 在编译期能检查虚函数是否被重写,重写/实现函数常写 override 是一种好习惯
	virtual void DrawModel(ComPtr<ID3D12GraphicsCommandList>& pCommandList) override
	{
		// 设置 IBV 索引缓冲描述符
		pCommandList->IASetIndexBuffer(&IndexBufferView);
		// 设置 VBV 顶点缓冲描述符,注意我们这里使用了多槽输入!
		pCommandList->IASetVertexBuffers(0, 2, VertexBufferView);

		// 设置根参数,将纹理 SRV 描述符设置到 GPU 的寄存器上,这样着色器就可以找到纹理了
		pCommandList->SetGraphicsRootDescriptorTable(1, Texture_GPUHandle_Map["planks_oak"]);

		// Draw Call 渲染!
		pCommandList->DrawIndexedInstanced(36, 1, 0, 0, 0);
	}
};


// 熔炉 (实例类),继承自全遮挡固体方块
class Furnace : public SoildBlock
{
public:

	// 构造函数,调用 AppendTextureKey 添加需要的纹理
	Furnace()
	{
		this->AppendTextureKey("furnace_front_off");
		this->AppendTextureKey("furnace_side");
		this->AppendTextureKey("furnace_top");
	}

	// 绘制模型,函数声明的 override 在编译期能检查虚函数是否被重写,重写/实现函数常写 override 是一种好习惯
	virtual void DrawModel(ComPtr<ID3D12GraphicsCommandList>& pCommandList) override
	{
		// 设置 IBV 索引缓冲描述符
		pCommandList->IASetIndexBuffer(&IndexBufferView);
		// 设置 VBV 顶点缓冲描述符,注意我们这里使用了多槽输入!
		pCommandList->IASetVertexBuffers(0, 2, VertexBufferView);

		// 设置根参数,将纹理 SRV 描述符设置到 GPU 的寄存器上,这样着色器就可以找到纹理了
		// 要更换纹理,可以通过 SetGraphicsRootDescriptorTable 改变根参数绑定的 GPUHandle

		// 渲染上下面
		pCommandList->SetGraphicsRootDescriptorTable(1, Texture_GPUHandle_Map["furnace_top"]);
		pCommandList->DrawIndexedInstanced(12, 1, 24, 0, 0);

		// 渲染左右背面
		pCommandList->SetGraphicsRootDescriptorTable(1, Texture_GPUHandle_Map["furnace_side"]);
		pCommandList->DrawIndexedInstanced(18, 1, 6, 0, 0);

		// 渲染正面
		pCommandList->SetGraphicsRootDescriptorTable(1, Texture_GPUHandle_Map["furnace_front_off"]);
		pCommandList->DrawIndexedInstanced(6, 1, 0, 0, 0);
	}
};


// 工作台 (实例类),继承自全遮挡固体方块
class Crafting_Table : public SoildBlock
{
public:

	// 构造函数,调用 AppendTextureKey 添加需要的纹理
	Crafting_Table()
	{
		this->AppendTextureKey("crafting_table_front");
		this->AppendTextureKey("crafting_table_side");
		this->AppendTextureKey("crafting_table_top");
	}

	// 绘制模型,函数声明的 override 在编译期能检查虚函数是否被重写,重写/实现函数常写 override 是一种好习惯
	virtual void DrawModel(ComPtr<ID3D12GraphicsCommandList>& pCommandList) override
	{
		// 设置 IBV 索引缓冲描述符
		pCommandList->IASetIndexBuffer(&IndexBufferView);
		// 设置 VBV 顶点缓冲描述符,注意我们这里使用了多槽输入!
		pCommandList->IASetVertexBuffers(0, 2, VertexBufferView);

		// 设置根参数,将纹理 SRV 描述符设置到 GPU 的寄存器上,这样着色器就可以找到纹理了
		// 要更换纹理,可以通过 SetGraphicsRootDescriptorTable 改变根参数绑定的 GPUHandle

		// 渲染上下面
		pCommandList->SetGraphicsRootDescriptorTable(1, Texture_GPUHandle_Map["crafting_table_top"]);
		pCommandList->DrawIndexedInstanced(12, 1, 24, 0, 0);

		// 渲染左右背面
		pCommandList->SetGraphicsRootDescriptorTable(1, Texture_GPUHandle_Map["crafting_table_side"]);
		pCommandList->DrawIndexedInstanced(18, 1, 6, 0, 0);

		// 渲染正面
		pCommandList->SetGraphicsRootDescriptorTable(1, Texture_GPUHandle_Map["crafting_table_front"]);
		pCommandList->DrawIndexedInstanced(6, 1, 0, 0, 0);
	}
};


// 橡树原木 (实例类),继承自全遮挡固体方块
class Log_Oak : public SoildBlock
{
public:

	// 构造函数,调用 AppendTextureKey 添加需要的纹理
	Log_Oak()
	{
		this->AppendTextureKey("log_oak");
		this->AppendTextureKey("log_oak_top");
	}

	// 绘制模型,函数声明的 override 在编译期能检查虚函数是否被重写,重写/实现函数常写 override 是一种好习惯
	virtual void DrawModel(ComPtr<ID3D12GraphicsCommandList>& pCommandList) override
	{
		// 设置 IBV 索引缓冲描述符
		pCommandList->IASetIndexBuffer(&IndexBufferView);
		// 设置 VBV 顶点缓冲描述符,注意我们这里使用了多槽输入!
		pCommandList->IASetVertexBuffers(0, 2, VertexBufferView);

		// 设置根参数,将纹理 SRV 描述符设置到 GPU 的寄存器上,这样着色器就可以找到纹理了
		// 要更换纹理,可以通过 SetGraphicsRootDescriptorTable 改变根参数绑定的 GPUHandle

		// 渲染上下面
		pCommandList->SetGraphicsRootDescriptorTable(1, Texture_GPUHandle_Map["log_oak_top"]);
		pCommandList->DrawIndexedInstanced(12, 1, 24, 0, 0);

		// 渲染左右正背面
		pCommandList->SetGraphicsRootDescriptorTable(1, Texture_GPUHandle_Map["log_oak"]);
		pCommandList->DrawIndexedInstanced(24, 1, 0, 0, 0);
	}
};


// 草方块 (实例类),继承自全遮挡固体方块
class Grass : public SoildBlock
{
public:

	// 构造函数,调用 AppendTextureKey 添加需要的纹理
	Grass()
	{
		this->AppendTextureKey("grass_side");
		this->AppendTextureKey("grass_top");
		this->AppendTextureKey("dirt");
	}

	// 绘制模型,函数声明的 override 在编译期能检查虚函数是否被重写,重写/实现函数常写 override 是一种好习惯
	virtual void DrawModel(ComPtr<ID3D12GraphicsCommandList>& pCommandList) override
	{
		// 设置 IBV 索引缓冲描述符
		pCommandList->IASetIndexBuffer(&IndexBufferView);
		// 设置 VBV 顶点缓冲描述符,注意我们这里使用了多槽输入!
		pCommandList->IASetVertexBuffers(0, 2, VertexBufferView);

		// 设置根参数,将纹理 SRV 描述符设置到 GPU 的寄存器上,这样着色器就可以找到纹理了
		// 要更换纹理,可以通过 SetGraphicsRootDescriptorTable 改变根参数绑定的 GPUHandle

		// 渲染上面
		pCommandList->SetGraphicsRootDescriptorTable(1, Texture_GPUHandle_Map["grass_top"]);
		pCommandList->DrawIndexedInstanced(6, 1, 24, 0, 0);

		// 渲染下面
		pCommandList->SetGraphicsRootDescriptorTable(1, Texture_GPUHandle_Map["dirt"]);
		pCommandList->DrawIndexedInstanced(6, 1, 30, 0, 0);

		// 渲染左右正背面
		pCommandList->SetGraphicsRootDescriptorTable(1, Texture_GPUHandle_Map["grass_side"]);
		pCommandList->DrawIndexedInstanced(24, 1, 0, 0, 0);
	}
};


// 橡木完整台阶 (实例类),继承自完整台阶方块
class Planks_Oak_SoildStair : public SoildStair
{
public:

	// 构造函数,调用 AppendTextureKey 添加需要的纹理
	Planks_Oak_SoildStair()
	{
		this->AppendTextureKey("planks_oak");
	}

	// 绘制模型,函数声明的 override 在编译期能检查虚函数是否被重写,重写/实现函数常写 override 是一种好习惯
	virtual void DrawModel(ComPtr<ID3D12GraphicsCommandList>& pCommandList) override
	{
		// 设置 IBV 索引缓冲描述符
		pCommandList->IASetIndexBuffer(&IndexBufferView);
		// 设置 VBV 顶点缓冲描述符,注意我们这里使用了多槽输入!
		pCommandList->IASetVertexBuffers(0, 2, VertexBufferView);

		// 渲染
		pCommandList->SetGraphicsRootDescriptorTable(1, Texture_GPUHandle_Map["planks_oak"]);
		pCommandList->DrawIndexedInstanced(60, 1, 0, 0, 0);
	}
};


管理并渲染方块


管理层:ModelManager 模型管理器类


那么,如何管理这些模型呢?

抽象类虽然不能创建实例,但我们可以利用抽象类指针实现多态,让指针指向实例方块类的内存,这样就可以用一个 vector 表示各种各样的方块了:

// 模型组,存储 Model 类指针的 vector,注意存储的是指针,指针可以指向不同类的对象
std::vector<Model*> ModelGroup;


// 模型管理器
class ModelManager
{
public:

	// 纹理映射表元素结构体
	struct TEXTURE_MAP_INFO
	{
		std::wstring TextureFilePath;			// 文件路径
		
		// 位于默认堆上纹理资源
		ComPtr<ID3D12Resource> DefaultHeapTextureResource;
		// 位于上传堆的纹理资源
		ComPtr<ID3D12Resource> UploadHeapTextureResource;

		D3D12_CPU_DESCRIPTOR_HANDLE CPUHandle;	// SRV 描述符堆的 CPU 句柄,用于创建纹理 SRV 描述符,将纹理与描述符绑定
		D3D12_GPU_DESCRIPTOR_HANDLE GPUHandle;	// SRV 描述符堆的 GPU 句柄,用于设置根参数,索引到对应纹理

		// 不用担心,创建 SRV 堆描述符堆,就会确定 CPU 句柄和 GPU 句柄的地址,后续只要描述符堆不销毁重新构造,句柄地址就是固定的
		// CPU 句柄和 GPU 句柄只有位置和用法不同的区别 (CPU 句柄在 CPU 端,GPU 句柄在 GPU 端)
		// 同一索引的 CPU 和 GPU 句柄能共享数据,所以只需创建 CPU 句柄,GPU 句柄无需创建
		// SetDescriptorHeap 会把描述符堆映射到 GPU 端的渲染管线,对应的 GPU 句柄就会有数据 (和同索引的 CPU 句柄一致)
	};

	// 纹理映射表
	std::unordered_map<std::string, TEXTURE_MAP_INFO> Texture_SRV_Map;


	// 模型组,存储 Model 类指针的 vector,注意存储的是指针,指针可以指向不同类的对象
	std::vector<Model*> ModelGroup;


public:

	// 构造函数,我们在构造函数上创建纹理映射表
	ModelManager()
	{
		Texture_SRV_Map["dirt"].TextureFilePath = L"resource/dirt.png";
		Texture_SRV_Map["grass_top"].TextureFilePath = L"resource/grass_top.png";
		Texture_SRV_Map["grass_side"].TextureFilePath = L"resource/grass_side.png";
		Texture_SRV_Map["log_oak"].TextureFilePath = L"resource/log_oak.png";
		Texture_SRV_Map["log_oak_top"].TextureFilePath = L"resource/log_oak_top.png";
		Texture_SRV_Map["furnace_front_off"].TextureFilePath = L"resource/furnace_front_off.png";
		Texture_SRV_Map["furnace_side"].TextureFilePath = L"resource/furnace_side.png";
		Texture_SRV_Map["furnace_top"].TextureFilePath = L"resource/furnace_top.png";
		Texture_SRV_Map["crafting_table_front"].TextureFilePath = L"resource/crafting_table_front.png";
		Texture_SRV_Map["crafting_table_side"].TextureFilePath = L"resource/crafting_table_side.png";
		Texture_SRV_Map["crafting_table_top"].TextureFilePath = L"resource/crafting_table_top.png";
		Texture_SRV_Map["planks_oak"].TextureFilePath = L"resource/planks_oak.png";
	}

	// 创建方块,我们在这里写上创建方块的代码
	void CreateBlock()
	{
		// 两层泥土地基,y 是高度
		for (int x = 0; x < 10; x++)
		{
			for (int z = -4; z < 10; z++)
			{
				for (int y = -2; y < 0; y++)
				{
					Model* dirt = new Dirt();							// 创建对象指针,调用时会根据虚函数表调用不同的函数
					dirt->SetModelMatrix(XMMatrixTranslation(x, y, z));	// 设置不同的模型矩阵,XMMatrixTranslation 平移模型
					ModelGroup.push_back(dirt);							// 将新模型添加到模型组
				}
			}
		}

		// 一层草方块地基
		for (int x = 0; x < 10; x++)
		{
			for (int z = -4; z < 10; z++)
			{
				Model* grass = new Grass();
				grass->SetModelMatrix(XMMatrixTranslation(x, 0, z));
				ModelGroup.push_back(grass);
			}
		}

		// 4x4 木板房基

		for (int x = 3; x < 7; x++)
		{
			for (int z = 3; z < 7; z++)
			{
				Model* plank = new Planks_Oak();
				plank->SetModelMatrix(XMMatrixTranslation(x, 2, z));
				ModelGroup.push_back(plank);
			}
		}


		// 8 柱原木 

		for (int y = 1; y < 7; y++)
		{
			Model* log_oak = new Log_Oak();
			log_oak->SetModelMatrix(XMMatrixTranslation(3, y, 2));
			ModelGroup.push_back(log_oak);
		}

		for (int y = 1; y < 7; y++)
		{
			Model* log_oak = new Log_Oak();
			log_oak->SetModelMatrix(XMMatrixTranslation(2, y, 3));
			ModelGroup.push_back(log_oak);
		}

		for (int y = 1; y < 7; y++)
		{
			Model* log_oak = new Log_Oak();
			log_oak->SetModelMatrix(XMMatrixTranslation(6, y, 2));
			ModelGroup.push_back(log_oak);
		}

		for (int y = 1; y < 7; y++)
		{
			Model* log_oak = new Log_Oak();
			log_oak->SetModelMatrix(XMMatrixTranslation(7, y, 3));
			ModelGroup.push_back(log_oak);
		}

		for (int y = 1; y < 7; y++)
		{
			Model* log_oak = new Log_Oak();
			log_oak->SetModelMatrix(XMMatrixTranslation(7, y, 6));
			ModelGroup.push_back(log_oak);
		}

		for (int y = 1; y < 7; y++)
		{
			Model* log_oak = new Log_Oak();
			log_oak->SetModelMatrix(XMMatrixTranslation(6, y, 7));
			ModelGroup.push_back(log_oak);
		}

		for (int y = 1; y < 7; y++)
		{
			Model* log_oak = new Log_Oak();
			log_oak->SetModelMatrix(XMMatrixTranslation(2, y, 6));
			ModelGroup.push_back(log_oak);
		}

		for (int y = 1; y < 7; y++)
		{
			Model* log_oak = new Log_Oak();
			log_oak->SetModelMatrix(XMMatrixTranslation(3, y, 7));
			ModelGroup.push_back(log_oak);
		}


		// 其他木板与门前台阶
		{
			Model* plank = new Planks_Oak();
			plank->SetModelMatrix(XMMatrixTranslation(4, 2, 2));
			ModelGroup.push_back(plank);

			plank = new Planks_Oak();
			plank->SetModelMatrix(XMMatrixTranslation(5, 2, 2));
			ModelGroup.push_back(plank);

			for (int y = 5; y < 7; y++)
			{
				for (int x = 4; x < 6; x++)
				{
					plank = new Planks_Oak();
					plank->SetModelMatrix(XMMatrixTranslation(x, y, 2));
					ModelGroup.push_back(plank);
				}
			}

			for (int y = 2; y < 4; y++)
			{
				for (int z = 4; z < 6; z++)
				{
					plank = new Planks_Oak();
					plank->SetModelMatrix(XMMatrixTranslation(2, y, z));
					ModelGroup.push_back(plank);
				}
			}

			for (int y = 2; y < 4; y++)
			{
				for (int x = 4; x < 6; x++)
				{
					plank = new Planks_Oak();
					plank->SetModelMatrix(XMMatrixTranslation(x, y, 7));
					ModelGroup.push_back(plank);
				}
			}

			for (int y = 2; y < 4; y++)
			{
				for (int z = 4; z < 6; z++)
				{
					plank = new Planks_Oak();
					plank->SetModelMatrix(XMMatrixTranslation(7, y, z));
					ModelGroup.push_back(plank);
				}
			}

			plank = new Planks_Oak();
			plank->SetModelMatrix(XMMatrixTranslation(2, 6, 4));
			ModelGroup.push_back(plank);

			plank = new Planks_Oak();
			plank->SetModelMatrix(XMMatrixTranslation(2, 6, 5));
			ModelGroup.push_back(plank);

			plank = new Planks_Oak();
			plank->SetModelMatrix(XMMatrixTranslation(4, 6, 7));
			ModelGroup.push_back(plank);

			plank = new Planks_Oak();
			plank->SetModelMatrix(XMMatrixTranslation(5, 6, 7));
			ModelGroup.push_back(plank);

			plank = new Planks_Oak();
			plank->SetModelMatrix(XMMatrixTranslation(7, 6, 4));
			ModelGroup.push_back(plank);

			plank = new Planks_Oak();
			plank->SetModelMatrix(XMMatrixTranslation(7, 6, 5));
			ModelGroup.push_back(plank);

			Model* stair = new Planks_Oak_SoildStair();
			stair->SetModelMatrix(XMMatrixTranslation(4, 2, 1));
			ModelGroup.push_back(stair);

			stair = new Planks_Oak_SoildStair();
			stair->SetModelMatrix(XMMatrixTranslation(5, 2, 1));
			ModelGroup.push_back(stair);

			stair = new Planks_Oak_SoildStair();
			stair->SetModelMatrix(XMMatrixTranslation(4, 1, 0));
			ModelGroup.push_back(stair);

			stair = new Planks_Oak_SoildStair();
			stair->SetModelMatrix(XMMatrixTranslation(5, 1, 0));
			ModelGroup.push_back(stair);
		}

		// 4x4 木板房顶

		for (int x = 3; x < 7; x++)
		{
			for (int z = 3; z < 7; z++)
			{
				Model* plank = new Planks_Oak();
				plank->SetModelMatrix(XMMatrixTranslation(x, 6, z));
				ModelGroup.push_back(plank);
			}
		}

		// 屋顶

		{
			// 第一层

			for (int x = 3; x < 7; x++)
			{
				Model* stair = new Planks_Oak_SoildStair();
				stair->SetModelMatrix(XMMatrixTranslation(x, 6, 1));
				ModelGroup.push_back(stair);
			}

			for (int x = 3; x < 7; x++)
			{
				// 旋转橡木台阶用的模型矩阵
				// 这里本来是可以不用 XMMatrixTranslation(-0.5, -0.5, -0.5) 平移到模型中心的
				// 因为作者本人 (我) 的设计失误,把模型坐标系原点建立在模型左下角了 (见上文的 VertexArray)
				// 导致还要先把原点平移到模型中心,旋转完再还原,增大计算量,这个是完全可以规避的
				// 读者可以自行修改 VertexArray,使方块以自身中心为原点建系,这样就可以直接 XMMatrixRotationY() 进行旋转了
				XMMATRIX transform = XMMatrixTranslation(-0.5, -0.5, -0.5);
				transform *= XMMatrixRotationY(XM_PI);						// 平移中心后,再旋转,否则会出错 (旋转角度是弧度)
				transform *= XMMatrixTranslation(0.5, 0.5, 0.5);			// 旋转完再还原
				transform *= XMMatrixTranslation(x, 6, 8);					// 再平移到对应的坐标
				Model* stair = new Planks_Oak_SoildStair();
				stair->SetModelMatrix(transform);
				ModelGroup.push_back(stair);
			}

			for (int z = 3; z < 7; z++)
			{
				XMMATRIX transform = XMMatrixTranslation(-0.5, -0.5, -0.5);
				transform *= XMMatrixRotationY(XM_PIDIV2);					// 旋转 90°
				transform *= XMMatrixTranslation(0.5, 0.5, 0.5);
				transform *= XMMatrixTranslation(1, 6, z);
				Model* stair = new Planks_Oak_SoildStair();
				stair->SetModelMatrix(transform);
				ModelGroup.push_back(stair);
			}

			for (int z = 3; z < 7; z++)
			{
				XMMATRIX transform = XMMatrixTranslation(-0.5, -0.5, -0.5);
				transform *= XMMatrixRotationY(XM_PI + XM_PIDIV2);			// 旋转 270°
				transform *= XMMatrixTranslation(0.5, 0.5, 0.5);
				transform *= XMMatrixTranslation(8, 6, z);
				Model* stair = new Planks_Oak_SoildStair();
				stair->SetModelMatrix(transform);
				ModelGroup.push_back(stair);
			}

			// 第二层

			for (int x = 3; x < 7; x++)
			{
				Model* stair = new Planks_Oak_SoildStair();
				stair->SetModelMatrix(XMMatrixTranslation(x, 7, 2));
				ModelGroup.push_back(stair);
			}

			for (int x = 3; x < 7; x++)
			{
				XMMATRIX transform = XMMatrixTranslation(-0.5, -0.5, -0.5);
				transform *= XMMatrixRotationY(XM_PI);
				transform *= XMMatrixTranslation(0.5, 0.5, 0.5);
				transform *= XMMatrixTranslation(x, 7, 7);
				Model* stair = new Planks_Oak_SoildStair();
				stair->SetModelMatrix(transform);
				ModelGroup.push_back(stair);
			}

			for (int z = 3; z < 7; z++)
			{
				XMMATRIX transform = XMMatrixTranslation(-0.5, -0.5, -0.5);
				transform *= XMMatrixRotationY(XM_PIDIV2);
				transform *= XMMatrixTranslation(0.5, 0.5, 0.5);
				transform *= XMMatrixTranslation(2, 7, z);
				Model* stair = new Planks_Oak_SoildStair();
				stair->SetModelMatrix(transform);
				ModelGroup.push_back(stair);
			}

			for (int z = 3; z < 7; z++)
			{
				XMMATRIX transform = XMMatrixTranslation(-0.5, -0.5, -0.5);
				transform *= XMMatrixRotationY(XM_PI + XM_PIDIV2);
				transform *= XMMatrixTranslation(0.5, 0.5, 0.5);
				transform *= XMMatrixTranslation(7, 7, z);
				Model* stair = new Planks_Oak_SoildStair();
				stair->SetModelMatrix(transform);
				ModelGroup.push_back(stair);
			}

			// 补上屋顶空位

			for (int x = 3; x < 7; x++)
			{
				for (int z = 3; z < 7; z++)
				{
					Model* plank = new Planks_Oak();
					plank->SetModelMatrix(XMMatrixTranslation(x, 7, z));
					ModelGroup.push_back(plank);
				}
			}
		}

		// 工作台和熔炉
		{
			Model* craft_table = new Crafting_Table();
			craft_table->SetModelMatrix(XMMatrixTranslation(3, 3, 6));
			ModelGroup.push_back(craft_table);

			Model* furnace = new Furnace();
			furnace->SetModelMatrix(XMMatrixTranslation(4, 3, 6));
			ModelGroup.push_back(furnace);

			furnace = new Furnace();
			furnace->SetModelMatrix(XMMatrixTranslation(5, 3, 6));
			ModelGroup.push_back(furnace);
		}

	}

	// 当一切准备就绪后,就可以正式创建模型资源,准备渲染了
	// 调用该函数的前提是: 依次完成 DX12Engine::CreateModelTextureResource (读取并创建纹理资源),CreateBlock (创建方块,设置模型矩阵)
	void CreateModelResource(ComPtr<ID3D12Device4>& pD3D12Device)
	{
		// 遍历模型组
		for (auto& model : ModelGroup)
		{
			// 创建模型资源
			model->CreateResourceAndDescriptor(pD3D12Device);
			// 遍历模型自身的映射表,设置模型需要用到的纹理
			for (const auto& texture : model->RequestForTextureMap())
			{
				// 设置模型的 SRV 描述符
				model->SetTextureGPUHandle(texture.first, Texture_SRV_Map[texture.first].GPUHandle);
			}
		}
	}

	// 渲染全部模型!
	// 调用该函数的前提是: 完成上面的 CreateModelResource
	void RenderAllModel(ComPtr<ID3D12GraphicsCommandList>& pCommandList)
	{
		// 遍历模型组
		for (const auto& model : ModelGroup)
		{
			model->DrawModel(pCommandList);
		}
	}

};

这个类主要用于存储模型和纹理资源,以及它们的映射关系。我们在这个类的 CreateBlock 函数写上要新建的方块,然后在 DX12Engine 上调用 ModelManager 的公共函数就行了。

也许读者会很奇怪,为什么 Model 一个纹理映射表,ModelManager 也有一个纹理映射表呢?这是因为 ModelManager 的纹理映射表存储的是应用程序用到的所有纹理,而 Model 存储的是该模型用到的纹理,两个是子集关系,Model 是 ModelManager 的子集。


设备层:DX12Engine


在这里插入图片描述


接下来,我们要在 DX12Engine 上继续完成 ModelManager 的任务,我们现在 DX12Engine 上弄一个模型管理器:

ModelManager m_ModelManager;							// 模型管理器,帮助管理并渲染模型

修改 SRV 描述符堆创建:CreateSRVHeap


在这里插入图片描述


修改 NumDescriptors 就行:

	// 创建 SRV Descriptor Heap 着色器资源描述符堆 (Shader Visible)
	void CreateSRVHeap()
	{
		// 创建 SRV 描述符堆 (Shader Resource View,着色器资源描述符)
		D3D12_DESCRIPTOR_HEAP_DESC SRVHeapDesc = {};
		SRVHeapDesc.NumDescriptors = m_ModelManager.Texture_SRV_Map.size();	// 描述符堆的容量
		SRVHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;			// 描述符堆类型,CBV、SRV、UAV 这三种描述符可以放在同一种描述符堆上
		SRVHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;		// 描述符堆标志,Shader-Visible 表示对着色器可见

		// 创建 SRV 描述符堆
		m_D3D12Device->CreateDescriptorHeap(&SRVHeapDesc, IID_PPV_ARGS(&m_SRVHeap));
	}

修改纹理加载函数:LoadTextureFromFile


因为要画多幅纹理,我们现在要修改纹理加载函数:

// 加载纹理到内存中
bool LoadTextureFromFile(std::wstring TextureFilename)
{
	// 如果还没创建 WIC 工厂,就新建一个 WIC 工厂实例。注意!WIC 工厂不可以重复释放与创建!
	if (m_WICFactory == nullptr) CoCreateInstance(CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&m_WICFactory));

	// 创建图片解码器,并将图片读入到解码器中
	HRESULT hr = m_WICFactory->CreateDecoderFromFilename(TextureFilename.c_str(), nullptr, GENERIC_READ, WICDecodeMetadataCacheOnDemand, &m_WICBitmapDecoder);

	std::wostringstream output_str;		// 用于格式化字符串
	switch (hr)
	{
	case S_OK: break;	// 解码成功,直接 break 进入下一步即可

	case HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND):	// 文件找不到
		output_str << L"找不到文件 " << TextureFilename << L" !请检查文件路径是否有误!";
		MessageBox(NULL, output_str.str().c_str(), L"错误", MB_OK | MB_ICONERROR);
		return false;

	case HRESULT_FROM_WIN32(ERROR_FILE_CORRUPT):	// 文件句柄正在被另一个应用进程占用
		output_str << L"文件 " << TextureFilename << L" 已经被另一个应用进程打开并占用了!请先关闭那个应用进程!";
		MessageBox(NULL, output_str.str().c_str(), L"错误", MB_OK | MB_ICONERROR);
		return false;

	case WINCODEC_ERR_COMPONENTNOTFOUND:			// 找不到可解码的组件,说明这不是有效的图像文件
		output_str << L"文件 " << TextureFilename << L" 不是有效的图像文件,无法解码!请检查文件是否为图像文件!";
		MessageBox(NULL, output_str.str().c_str(), L"错误", MB_OK | MB_ICONERROR);
		return false;

	default:			// 发生其他未知错误
		output_str << L"文件 " << TextureFilename << L" 解码失败!发生了其他错误,错误码:" << hr << L" ,请查阅微软官方文档。";
		MessageBox(NULL, output_str.str().c_str(), L"错误", MB_OK | MB_ICONERROR);
		return false;
	}

	// 获取图片数据的第一帧,这个 GetFrame 可以用于 gif 这种多帧动图
	m_WICBitmapDecoder->GetFrame(0, &m_WICBitmapDecodeFrame);


	// 获取图片格式,并将它转化为 DX12 能接受的纹理格式
	// 如果碰到格式无法支持的错误,可以用微软提供的 画图3D 来转换,强力推荐!
	WICPixelFormatGUID SourceFormat = {};				// 源图格式
	GUID TargetFormat = {};								// 目标格式

	m_WICBitmapDecodeFrame->GetPixelFormat(&SourceFormat);						// 获取源图格式

	if (DX12TextureHelper::GetTargetPixelFormat(&SourceFormat, &TargetFormat))	// 获取目标格式
	{
		TextureFormat = DX12TextureHelper::GetDXGIFormatFromPixelFormat(&TargetFormat);	// 获取 DX12 支持的格式
	}
	else	// 如果没有可支持的目标格式
	{
		::MessageBox(NULL, L"此纹理不受支持!", L"提示", MB_OK);
		return false;
	}


	// 获取目标格式后,将纹理转换为目标格式,使其能被 DX12 使用
	m_WICFactory->CreateFormatConverter(&m_WICFormatConverter);		// 创建图片转换器
	// 初始化转换器,实际上是把位图进行了转换
	m_WICFormatConverter->Initialize(m_WICBitmapDecodeFrame.Get(), TargetFormat, WICBitmapDitherTypeNone,
		nullptr, 0.0f, WICBitmapPaletteTypeCustom);
	// 将位图数据继承到 WIC 位图资源,我们要在这个 WIC 位图资源上获取信息
	m_WICFormatConverter.As(&m_WICBitmapSource);



	m_WICBitmapSource->GetSize(&TextureWidth, &TextureHeight);		// 获取纹理宽高

	ComPtr<IWICComponentInfo> _temp_WICComponentInfo = {};			// 用于获取 BitsPerPixel 纹理图像深度
	ComPtr<IWICPixelFormatInfo> _temp_WICPixelInfo = {};			// 用于获取 BitsPerPixel 纹理图像深度
	m_WICFactory->CreateComponentInfo(TargetFormat, &_temp_WICComponentInfo);
	_temp_WICComponentInfo.As(&_temp_WICPixelInfo);
	_temp_WICPixelInfo->GetBitsPerPixel(&BitsPerPixel);				// 获取 BitsPerPixel 图像深度

	return true;
}

修改资源创建函数:CreateUploadAndDefaultResource


在这里插入图片描述

// 创建用于上传的 UploadResource 与用于放纹理的 DefaultResource
void CreateUploadAndDefaultResource(ModelManager::TEXTURE_MAP_INFO& Info)
{
	// 计算纹理每行数据的真实数据大小 (单位:Byte 字节),因为纹理图片在内存中是线性存储的
	// 想获取纹理的真实大小、正确读取纹理数据、上传到 GPU,必须先获取纹理的 BitsPerPixel 图像深度,因为不同位图深度可能不同
	// 然后再计算每行像素占用的字节,除以 8 是因为 1 Byte = 8 bits
	BytePerRowSize = TextureWidth * BitsPerPixel / 8;

	// 纹理的真实大小 (单位:字节)
	TextureSize = BytePerRowSize * TextureHeight;

	// 上传堆资源每行的大小 (单位:字节),注意这里要进行 256 字节对齐!
	// 因为 GPU 与 CPU 架构不同,GPU 注重并行计算,注重结构化数据的快速读取,读取数据都是以 256 字节为一组来读的
	// 因此要先要对 BytePerRowSize 进行对齐,判断需要有多少组才能容纳纹理每行像素,不对齐的话数据会读错的。
	UploadResourceRowSize = Ceil(BytePerRowSize, 256) * 256;

	// 上传堆资源的总大小 (单位:字节),分配空间必须只多不少,否则会报 D3D12 MinimumAlloc Error 资源内存创建错误
	// 注意最后一行不用内存对齐 (因为后面没其他行了,不用内存对齐也能正确读取),所以要 (TextureHeight - 1) 再加 BytePerRowSize
	UploadResourceSize = UploadResourceRowSize * (TextureHeight - 1) + BytePerRowSize;


	// 用于中转纹理的上传堆资源结构体
	D3D12_RESOURCE_DESC UploadResourceDesc = {};
	UploadResourceDesc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER;		// 资源类型,上传堆的资源类型都是 buffer 缓冲
	UploadResourceDesc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR;			// 资源布局,指定资源的存储方式,上传堆的资源都是 row major 按行线性存储
	UploadResourceDesc.Width = UploadResourceSize;						// 资源宽度,上传堆的资源宽度是资源的总大小,注意资源大小必须只多不少
	UploadResourceDesc.Height = 1;										// 资源高度,上传堆仅仅是传递线性资源的,所以高度必须为 1
	UploadResourceDesc.Format = DXGI_FORMAT_UNKNOWN;					// 资源格式,上传堆资源的格式必须为 UNKNOWN
	UploadResourceDesc.DepthOrArraySize = 1;							// 资源深度,这个是用于纹理数组和 3D 纹理的,上传堆资源必须为 1
	UploadResourceDesc.MipLevels = 1;									// Mipmap 等级,这个是用于纹理的,上传堆资源必须为 1
	UploadResourceDesc.SampleDesc.Count = 1;							// 资源采样次数,上传堆资源都是填 1

	// 上传堆属性的结构体,上传堆位于 CPU 和 GPU 的共享内存
	D3D12_HEAP_PROPERTIES UploadHeapDesc = { D3D12_HEAP_TYPE_UPLOAD };

	// 创建上传堆资源
	m_D3D12Device->CreateCommittedResource(&UploadHeapDesc, D3D12_HEAP_FLAG_NONE, &UploadResourceDesc,
		D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&Info.UploadHeapTextureResource));


	// 用于放纹理的默认堆资源结构体
	D3D12_RESOURCE_DESC DefaultResourceDesc = {};
	DefaultResourceDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;	// 资源类型,这里指定为 Texture2D 2D纹理
	DefaultResourceDesc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN;			// 纹理资源的布局都是 UNKNOWN
	DefaultResourceDesc.Width = TextureWidth;							// 资源宽度,这里填纹理宽度
	DefaultResourceDesc.Height = TextureHeight;							// 资源高度,这里填纹理高度
	DefaultResourceDesc.Format = TextureFormat;							// 资源格式,这里填纹理格式,要和纹理一样
	DefaultResourceDesc.DepthOrArraySize = 1;							// 资源深度,我们只有一副纹理,所以填 1
	DefaultResourceDesc.MipLevels = 1;									// Mipmap 等级,我们暂时不使用 Mipmap,所以填 1
	DefaultResourceDesc.SampleDesc.Count = 1;							// 资源采样次数,这里我们填 1 就行

	// 默认堆属性的结构体,默认堆位于显存
	D3D12_HEAP_PROPERTIES DefaultHeapDesc = { D3D12_HEAP_TYPE_DEFAULT };

	// 创建默认堆资源
	m_D3D12Device->CreateCommittedResource(&DefaultHeapDesc, D3D12_HEAP_FLAG_NONE, &DefaultResourceDesc,
		D3D12_RESOURCE_STATE_COPY_DEST, nullptr, IID_PPV_ARGS(&Info.DefaultHeapTextureResource));
}

启动命令列表:StartCommandRecord


在这里插入图片描述


重点来了,反复调用复制引擎并不是一件好事,我们可以先将纹理复制命令先全部记录一遍,记录完毕再送到命令队列执行:

// 启动命令列表,准备录制复制命令
void StartCommandRecord()
{
	// 复制资源需要使用 GPU 的 CopyEngine 复制引擎,所以需要向命令队列发出复制命令

	m_CommandAllocator->Reset();								// 先重置命令分配器
	m_CommandList->Reset(m_CommandAllocator.Get(), nullptr);	// 再重置命令列表,复制命令不需要 PSO 状态,所以第二个参数填 nullptr
}

录制复制命令:CopyTextureDataToDefaultResource


在这里插入图片描述


// CommandList 录制命令,录制将纹理数据复制到默认堆资源的命令
void CopyTextureDataToDefaultResource(ModelManager::TEXTURE_MAP_INFO& Info)
{
	// 用于暂时存储纹理数据的指针,这里要用 malloc 分配空间
	BYTE* TextureData = (BYTE*)malloc(TextureSize);

	// 将整块纹理数据读到 TextureData 中,方便后文的 memcpy 复制操作
	m_WICBitmapSource->CopyPixels(nullptr, BytePerRowSize, TextureSize, TextureData);

	// 用于传递资源的指针
	BYTE* TransferPointer = nullptr;

	// Map 开始映射,Map 方法会得到上传堆资源的地址 (在共享内存上),传递给指针,这样我们就能通过 memcpy 操作复制数据了
	Info.UploadHeapTextureResource->Map(0, nullptr, reinterpret_cast<void**>(&TransferPointer));

	// 这里我们要逐行复制数据!注意两个指针偏移的长度不同!
	for (UINT i = 0; i < TextureHeight; i++)
	{
		// 向上传堆资源逐行复制纹理数据 (CPU 高速缓存 -> 共享内存)
		memcpy(TransferPointer, TextureData, BytePerRowSize);
		// 纹理指针偏移到下一行
		TextureData += BytePerRowSize;
		// 上传堆资源指针偏移到下一行,注意偏移长度不同!
		TransferPointer += UploadResourceRowSize;
	}

	// Unmap 结束映射,因为我们无法直接读写默认堆资源,需要上传堆复制到那里,在复制之前,我们需要先结束映射,让上传堆处于只读状态
	Info.UploadHeapTextureResource->Unmap(0, nullptr);

	TextureData -= TextureSize;		// 纹理资源指针偏移回初始位置
	free(TextureData);				// 释放上文 malloc 分配的空间,后面我们用不到它,不要让它占内存

	D3D12_PLACED_SUBRESOURCE_FOOTPRINT PlacedFootprint = {};								// 资源脚本,用来描述要复制的资源
	D3D12_RESOURCE_DESC DefaultResourceDesc = Info.DefaultHeapTextureResource->GetDesc();	// 默认堆资源结构体

	// 获取纹理复制脚本,用于下文的纹理复制
	m_D3D12Device->GetCopyableFootprints(&DefaultResourceDesc, 0, 1, 0, &PlacedFootprint, nullptr, nullptr, nullptr);

	D3D12_TEXTURE_COPY_LOCATION DstLocation = {};						// 复制目标位置 (默认堆资源) 结构体
	DstLocation.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX;		// 纹理复制类型,这里必须指向纹理
	DstLocation.SubresourceIndex = 0;									// 指定要复制的子资源索引
	DstLocation.pResource = Info.DefaultHeapTextureResource.Get();		// 要复制到的资源

	D3D12_TEXTURE_COPY_LOCATION SrcLocation = {};						// 复制源位置 (上传堆资源) 结构体
	SrcLocation.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT;		// 纹理复制类型,这里必须指向缓冲区
	SrcLocation.PlacedFootprint = PlacedFootprint;						// 指定要复制的资源脚本信息
	SrcLocation.pResource = Info.UploadHeapTextureResource.Get();		// 被复制数据的缓冲



	// 记录复制资源到默认堆的命令 (共享内存 -> 显存) 
	m_CommandList->CopyTextureRegion(&DstLocation, 0, 0, 0, &SrcLocation, nullptr);
}

创建 SRV 描述符:CreateSRV


一定要注意:我们把 ModelManager 和 DX12Engine 分离了,所以 DX12Engine (设备层) 创建完描述符,一定要返回给 ModelManager (管理层),否则管理层是没办法分派句柄到模型上的。

// 最终创建 SRV 着色器资源描述符,用于描述默认堆资源为一块纹理,创建完 SRV 描述符,会将描述符句柄存储到纹理映射表中
void CreateSRV(ModelManager::TEXTURE_MAP_INFO& Info, 
	D3D12_CPU_DESCRIPTOR_HANDLE CPUHandle, D3D12_GPU_DESCRIPTOR_HANDLE GPUHandle)
{
	// SRV 描述符信息结构体
	D3D12_SHADER_RESOURCE_VIEW_DESC SRVDescriptorDesc = {};
	// SRV 描述符类型,这里我们指定 Texture2D 2D纹理
	SRVDescriptorDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
	// SRV 描述符的格式也要填纹理格式
	SRVDescriptorDesc.Format = TextureFormat;
	// 纹理采样后每个纹理像素 RGBA 分量的顺序,D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING 表示纹理采样后分量顺序不改变
	SRVDescriptorDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
	// 这里我们不使用 Mipmap,所以填 1
	SRVDescriptorDesc.Texture2D.MipLevels = 1;

	// 创建 SRV 描述符,注意这里要用参数中的 CPUHandle
	m_D3D12Device->CreateShaderResourceView(Info.DefaultHeapTextureResource.Get(), &SRVDescriptorDesc, CPUHandle);

	// 将当前 SRV 描述符句柄存储到 ModelManager 的纹理映射表中,注意我们传的是引用参数,可以直接对参数进行修改
	Info.CPUHandle = CPUHandle;
	Info.GPUHandle = GPUHandle;
}

提交命令:StartCommandExecute


// 关闭命令列表,启动命令队列,正式开始将纹理复制到默认堆资源中
void StartCommandExecute()
{
	// 关闭命令列表
	m_CommandList->Close();

	// 用于传递命令用的临时 ID3D12CommandList 数组
	ID3D12CommandList* _temp_cmdlists[] = { m_CommandList.Get() };

	// 提交复制命令!GPU 开始复制!
	m_CommandQueue->ExecuteCommandLists(1, _temp_cmdlists);


	// 将围栏预定值设定为下一帧,注意复制资源也需要围栏等待,否则会发生资源冲突
	FenceValue++;
	// 在命令队列 (命令队列在 GPU 端) 设置围栏预定值,此命令会加入到命令队列中
	// 命令队列执行到这里会修改围栏值,表示复制已完成,"击中"围栏
	m_CommandQueue->Signal(m_Fence.Get(), FenceValue);
	// 设置围栏的预定事件,当复制完成时,围栏被"击中",激发预定事件,将事件由无信号状态转换成有信号状态
	m_Fence->SetEventOnCompletion(FenceValue, RenderEvent);
}

读取并创建纹理资源:CreateModelTextureResource


我们将上面七个函数直接整合到 CreateModelTextureResource 里,在 DX12Engine::Run 上调用这个函数就行:

// 读取并创建纹理资源
void CreateModelTextureResource()
{
	CreateSRVHeap();	// 创建 SRV 描述符堆,创建时就会确定描述符的 CPU 和 GPU 地址,无需担心

	// 当前元素的 CPU 句柄
	D3D12_CPU_DESCRIPTOR_HANDLE CurrentCPUHandle = m_SRVHeap->GetCPUDescriptorHandleForHeapStart();
	// 当前元素的 GPU 句柄
	D3D12_GPU_DESCRIPTOR_HANDLE CurrentGPUHandle = m_SRVHeap->GetGPUDescriptorHandleForHeapStart();
	// SRV 描述符的大小
	UINT SRVDescriptorSize = m_D3D12Device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);

	StartCommandRecord();	// 启动命令列表,开始录制命令

	// 对纹理映射表进行遍历
	for (auto& CurrentElem : m_ModelManager.Texture_SRV_Map)
	{
		// 从纹理文件中加载数据
		LoadTextureFromFile(CurrentElem.second.TextureFilePath);
		// 创建上传堆和默认堆资源
		CreateUploadAndDefaultResource(CurrentElem.second);
		// 将纹理数据复制到上传堆,并记录一条上传堆复制到默认堆的命令
		CopyTextureDataToDefaultResource(CurrentElem.second);
		// 最终创建 SRV 描述符
		CreateSRV(CurrentElem.second, CurrentCPUHandle, CurrentGPUHandle);

		// CPU 和 GPU 句柄偏移,准备下一个纹理
		CurrentCPUHandle.ptr += SRVDescriptorSize;
		CurrentGPUHandle.ptr += SRVDescriptorSize;
	}

	StartCommandExecute();	// 关闭命令列表,交给命令队列执行
}

创建模型顶点与索引资源:CreateModelVertexAndIndexResource


创建完纹理资源,我们还需要创建顶点索引资源,这样才能完成模型创建工作:

// 创建模型顶点与索引资源
void CreateModelVertexAndIndexResource()
{
	m_ModelManager.CreateBlock();
	m_ModelManager.CreateModelResource(m_D3D12Device);
}

修改根签名与渲染管线状态


修改根签名


根参数变化频率高的应该设置在根签名前面,可以优化性能,这个就叫根签名的 Version Control 版本控制,CBV 描述符的变动频率最高,我们把 CBV 描述符放第一个根参数,把 SRV 描述符放第二个根参数上:

// 创建根签名
void CreateRootSignature()
{
	ComPtr<ID3DBlob> SignatureBlob;			// 根签名字节码
	ComPtr<ID3DBlob> ErrorBlob;				// 错误字节码,根签名创建失败时用 OutputDebugStringA((const char*)ErrorBlob->GetBufferPointer()); 可以获取报错信息

	D3D12_ROOT_PARAMETER RootParameters[2] = {};							// 根参数数组

	// 把更新频率高的根参数放前面,低的放后面,可以优化性能 (微软官方文档建议)
	// 因为 DirectX API 能对根签名进行 Version Control 版本控制,在根签名越前面的根参数,访问速度更快

	// 第一个根参数:CBV 根描述符,根描述符是内联描述符,所以下文绑定根参数时,只需要传递常量缓冲资源的地址即可

	D3D12_ROOT_DESCRIPTOR CBVRootDescriptorDesc = {};					// 常量缓冲根描述符信息结构体
	CBVRootDescriptorDesc.ShaderRegister = 0;							// 要绑定的寄存器编号,这里对应 HLSL 的 b0 寄存器
	CBVRootDescriptorDesc.RegisterSpace = 0;							// 要绑定的命名空间,这里对应 HLSL 的 space0

	RootParameters[0].ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL;	// 常量缓冲对整个渲染管线都可见
	RootParameters[0].ParameterType = D3D12_ROOT_PARAMETER_TYPE_CBV;	// 第二个根参数的类型:CBV 根描述符
	RootParameters[0].Descriptor = CBVRootDescriptorDesc;				// 填上文的结构体


	// 第二个根参数:根描述表 (Range: SRV)

	D3D12_DESCRIPTOR_RANGE SRVDescriptorRangeDesc = {};						// Range 描述符范围结构体,一块 Range 表示一堆连续的同类型描述符
	SRVDescriptorRangeDesc.RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_SRV;		// Range 类型,这里指定 SRV 类型,CBV_SRV_UAV 在这里分流
	SRVDescriptorRangeDesc.NumDescriptors = 1;								// Range 里面的描述符数量 N,一次可以绑定多个描述符到多个寄存器槽上
	SRVDescriptorRangeDesc.BaseShaderRegister = 0;							// Range 要绑定的起始寄存器槽编号 i,绑定范围是 [s(i),s(i+N)],我们绑定 s0
	SRVDescriptorRangeDesc.RegisterSpace = 0;								// Range 要绑定的寄存器空间,整个 Range 都会绑定到同一寄存器空间上,我们绑定 space0
	SRVDescriptorRangeDesc.OffsetInDescriptorsFromTableStart = 0;			// Range 到根描述表开头的偏移量 (单位:描述符),根签名需要用它来寻找 Range 的地址,我们这填 0 就行

	D3D12_ROOT_DESCRIPTOR_TABLE RootDescriptorTableDesc = {};				// RootDescriptorTable 根描述表信息结构体,一个 Table 可以有多个 Range
	RootDescriptorTableDesc.pDescriptorRanges = &SRVDescriptorRangeDesc;	// Range 描述符范围指针
	RootDescriptorTableDesc.NumDescriptorRanges = 1;						// 根描述表中 Range 的数量

	RootParameters[1].ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL;				// 根参数在着色器中的可见性,这里指定仅在像素着色器可见 (只有像素着色器用到了纹理)
	RootParameters[1].ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE;	// 根参数类型,这里我们选 Table 根描述表,一个根描述表占用 1 DWORD
	RootParameters[1].DescriptorTable = RootDescriptorTableDesc;					// 根参数指针



	D3D12_STATIC_SAMPLER_DESC StaticSamplerDesc = {};						// 静态采样器结构体,静态采样器不会占用根签名
	StaticSamplerDesc.ShaderRegister = 0;									// 要绑定的寄存器槽,对应 s0
	StaticSamplerDesc.RegisterSpace = 0;									// 要绑定的寄存器空间,对应 space0
	StaticSamplerDesc.ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL;		// 静态采样器在着色器中的可见性,这里指定仅在像素着色器可见 (只有像素着色器用到了纹理采样)
	StaticSamplerDesc.Filter = D3D12_FILTER_COMPARISON_MIN_MAG_MIP_POINT;	// 纹理过滤类型,这里我们直接选 邻近点采样 就行
	StaticSamplerDesc.AddressU = D3D12_TEXTURE_ADDRESS_MODE_BORDER;			// 在 U 方向上的纹理寻址方式
	StaticSamplerDesc.AddressV = D3D12_TEXTURE_ADDRESS_MODE_BORDER;			// 在 V 方向上的纹理寻址方式
	StaticSamplerDesc.AddressW = D3D12_TEXTURE_ADDRESS_MODE_BORDER;			// 在 W 方向上的纹理寻址方式 (3D 纹理会用到)
	StaticSamplerDesc.MinLOD = 0;											// 最小 LOD 细节层次,这里我们默认填 0 就行
	StaticSamplerDesc.MaxLOD = D3D12_FLOAT32_MAX;							// 最大 LOD 细节层次,这里我们默认填 D3D12_FLOAT32_MAX (没有 LOD 上限)
	StaticSamplerDesc.MipLODBias = 0;										// 基础 Mipmap 采样偏移量,我们这里我们直接填 0 就行
	StaticSamplerDesc.MaxAnisotropy = 1;									// 各向异性过滤等级,我们不使用各向异性过滤,需要默认填 1
	StaticSamplerDesc.ComparisonFunc = D3D12_COMPARISON_FUNC_NEVER;			// 这个是用于阴影贴图的,我们不需要用它,所以填 D3D12_COMPARISON_FUNC_NEVER


	D3D12_ROOT_SIGNATURE_DESC rootsignatureDesc = {};			// 根签名信息结构体,上限 64 DWORD,静态采样器不占用根签名
	rootsignatureDesc.NumParameters = 2;						// 根参数数量
	rootsignatureDesc.pParameters = RootParameters;				// 根参数指针
	rootsignatureDesc.NumStaticSamplers = 1;					// 静态采样器数量
	rootsignatureDesc.pStaticSamplers = &StaticSamplerDesc;		// 静态采样器指针
	// 根签名标志,可以设置渲染管线不同阶段下的输入参数状态。注意这里!我们要从 IA 阶段输入顶点数据,所以要通过根签名,设置渲染管线允许从 IA 阶段读入数据
	rootsignatureDesc.Flags = D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT;

	// 编译根签名,让根签名先编译成 GPU 可读的二进制字节码
	D3D12SerializeRootSignature(&rootsignatureDesc, D3D_ROOT_SIGNATURE_VERSION_1_0, &SignatureBlob, &ErrorBlob);
	if (ErrorBlob)		// 如果根签名编译出错,ErrorBlob 可以提供报错信息
	{
		OutputDebugStringA((const char*)ErrorBlob->GetBufferPointer());
		OutputDebugStringA("\n");
	}


	// 用这个二进制字节码创建根签名对象
	m_D3D12Device->CreateRootSignature(0, SignatureBlob->GetBufferPointer(), SignatureBlob->GetBufferSize(), IID_PPV_ARGS(&m_RootSignature));
}

修改 shader.hlsl


上一章教程我们说过,每一个模型都应该拥有一个模型矩阵。所以我们把 Camera 类的 ModelMatrix 移到 Model 类上面。既然模型矩阵已经分离了,我们要修改 shader,把模型矩阵重新乘回去:


// (7) RenderMatchbox:渲染一个火柴盒,初步认识 Depth Stencil Buffer 深度模板缓冲,进一步认识顶点、模型与模型矩阵

struct VSInput      // VS 阶段输入顶点数据
{
    float4 position : POSITION;         // 输入顶点的位置,POSITION 语义对应 C++ 端输入布局中的 POSITION
    float2 texcoordUV : TEXCOORD;       // 输入顶点的纹理坐标,TEXCOORD 语义对应 C++ 端输入布局中的 TEXCOORD
    
    // 如果我们需要向 IA 阶段传递矩阵,矩阵太大没法直接传,我们可以把矩阵分割成一个一个行向量,再到 VS 阶段重新组装
    // MATRIX 是自定义语义,语义后面的数字表示同一个输入槽下,同语义名 (MATRIX) 的第 i 号数据
    float4 Matrix_Row0 : MATRIX0;
    float4 Matrix_Row1 : MATRIX1;
    float4 Matrix_Row2 : MATRIX2;
    float4 Matrix_Row3 : MATRIX3;
    
    // 其实语义只是个标识东西的字符串...
};

struct VSOutput     // VS 阶段输出顶点数据
{
    float4 position : SV_Position;      // 输出顶点的位置,SV_POSITION 是系统语义,指定顶点坐标已经位于齐次裁剪空间,通知光栅化阶段对顶点进行透视除法和屏幕映射
    float2 texcoordUV : TEXCOORD;       // 输出顶点纹理坐标时,仍然需要 TEXCOORD 语义
};

// Constant Buffer 常量缓冲,常量缓冲是预先分配的一段高速显存,存放每一帧都要变换的数据,例如我们这里的 MVP 变换矩阵
// 常量缓冲对所有着色器都是只读的,着色器不可以修改常量缓冲里面的内容
cbuffer GlobalData : register(b0, space0) // 常量缓冲,b 表示 buffer 缓冲,b0 表示 0 号 CBV 寄存器,space0 表示使用 b0 的 0 号空间
{
    row_major float4x4 MVP; // MVP 矩阵,用于将顶点坐标从模型空间变换到齐次裁剪空间,HLSL 默认按列存储,row_major 表示数据按行存储
}


// Vertex Shader 顶点着色器入口函数 (逐顶点输入),接收来自 IA 阶段输入的顶点数据,处理并返回齐次裁剪空间下的顶点坐标
// 上一阶段:Input Assembler 输入装配阶段
// 下一阶段:Rasterization 光栅化阶段
VSOutput VSMain(VSInput input)
{
    float4x4 ModelMatrix;   // VS 阶段要用到的模型矩阵
    VSOutput output;        // 输出给光栅化阶段的结构体变量
    
    // 将 IA 阶段得到的行数据组装成矩阵
    ModelMatrix[0] = input.Matrix_Row0;
    ModelMatrix[1] = input.Matrix_Row1;
    ModelMatrix[2] = input.Matrix_Row2;
    ModelMatrix[3] = input.Matrix_Row3;
    
    // 注意 cbuffer 常量缓冲对着色器是只读的!所以我们不能在这里对常量缓冲进行修改!
    output.position = mul(input.position, ModelMatrix);     // 先乘 模型矩阵
    output.position = mul(output.position, MVP);            // 再乘 观察矩阵 和 投影矩阵,注意 mul 左操作数是 output.position
    output.texcoordUV = input.texcoordUV;                   // 纹理 UV 不用变化,照常输出即可
    
    return output;
}

// register(*#,spaceN) *表示资源类型,#表示所用的寄存器编号,spaceN 表示使用的 N 号寄存器空间

Texture2D m_texure : register(t0, space0);      // 纹理,t 表示 SRV 着色器资源,t0 表示 0 号 SRV 寄存器,space0 表示使用 t0 的 0 号空间
SamplerState m_sampler : register(s0, space0);  // 纹理采样器,s 表示采样器,s0 表示 0 号 sampler 寄存器,space0 表示使用 s0 的 0 号空间

// Pixel Shader 像素着色器入口函数 (逐像素输入),接收来自光栅化阶段经过插值后的每个片元,返回像素颜色
// 上一阶段:Rasterization 光栅化阶段
// 下一阶段:Output Merger 输出合并阶段
float4 PSMain(VSOutput input) : SV_Target // SV_Target 也是系统语义,通知输出合并阶段将 PS 阶段返回的颜色写入到渲染目标(颜色缓冲)上
{
    return m_texure.Sample(m_sampler, input.texcoordUV); // 在像素着色器根据光栅化插值得到的 UV 坐标对纹理进行采样
}

修改 PSO:CreatePSO


聪明的读者会发现:VSOutput 上传递矩阵是要分成四个行向量传的,矩阵体积太大,没有对应的 DXGI 格式,而且直接传递会很慢。shader (GPU 端) 修改了,我们也要修改 PSO (CPU 端),修改 Input Assembler Layout,你会发现语义其实仅仅是个标识东西的字符串。

// Input Assembler 输入装配阶段
D3D12_INPUT_LAYOUT_DESC InputLayoutDesc = {};			// 输入样式信息结构体
D3D12_INPUT_ELEMENT_DESC InputElementDesc[6] = {};		// 输入元素信息结构体数组

// 第 0 号输入槽: 输入顶点位置与纹理 UV 坐标

InputElementDesc[0].SemanticName = "POSITION";					// 要锚定的语义
InputElementDesc[0].SemanticIndex = 0;							// 语义索引,目前我们填 0 就行
InputElementDesc[0].Format = DXGI_FORMAT_R32G32B32A32_FLOAT;	// 输入格式
InputElementDesc[0].InputSlot = 0;								// 输入槽编号,目前我们填 0 就行
InputElementDesc[0].AlignedByteOffset = 0;						// 在输入槽中的偏移
// 输入流类型,一种是我们现在用的 D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA 逐顶点输入流,还有一种叫逐实例输入流,后面再学
InputElementDesc[0].InputSlotClass = D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA;
InputElementDesc[0].InstanceDataStepRate = 0;					// 实例数据步进率,目前我们没有用到实例化,填 0


InputElementDesc[1].SemanticName = "TEXCOORD";										// 要锚定的语义
InputElementDesc[1].SemanticIndex = 0;												// 语义索引
InputElementDesc[1].Format = DXGI_FORMAT_R32G32_FLOAT;								// 输入格式
InputElementDesc[1].InputSlot = 0;													// 输入槽编号
// 在输入槽中的偏移,因为 position 与 texcoord 在同一输入槽(0号输入槽)
// position 是 float4,有 4 个 float ,每个 float 占 4 个字节,所以要偏移 4*4=16 个字节,这样才能确定 texcoord 参数的位置,不然装配的时候会覆盖原先 position 的数据
InputElementDesc[1].AlignedByteOffset = 16;											// 在输入槽中的偏移
InputElementDesc[1].InputSlotClass = D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA;	// 输入流类型
InputElementDesc[1].InstanceDataStepRate = 0;										// 实例数据步进率


// 第 1 号输入槽: 输入模型矩阵 (因为 4x4 矩阵太大,需要分成 4 个 float4 向量传输)

// MATRIX0
// SemanticName 语义名:							MATRIX (语义名后面不带数字)
// SemanticIndex 语义索引:						0
// InputSlot 输入槽:								第 1 号输入槽
// AlignedByteOffset 相对输入槽起始位置的偏移:		0
InputElementDesc[2].SemanticName = "MATRIX";
InputElementDesc[2].SemanticIndex = 0;
InputElementDesc[2].Format = DXGI_FORMAT_R32G32B32A32_FLOAT;
InputElementDesc[2].InputSlot = 1;
InputElementDesc[2].AlignedByteOffset = 0;
InputElementDesc[2].InputSlotClass = D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA;
InputElementDesc[2].InstanceDataStepRate = 0;

// MATRIX1
// SemanticName 语义名:							MATRIX (语义名后面不带数字)
// SemanticIndex 语义索引:						1
// InputSlot 输入槽:								第 1 号输入槽
// AlignedByteOffset 相对输入槽起始位置的偏移:		0 + 4*4 = 16
InputElementDesc[3].SemanticName = "MATRIX";
InputElementDesc[3].SemanticIndex = 1;
InputElementDesc[3].Format = DXGI_FORMAT_R32G32B32A32_FLOAT;
InputElementDesc[3].InputSlot = 1;
InputElementDesc[3].AlignedByteOffset = 16;
InputElementDesc[3].InputSlotClass = D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA;
InputElementDesc[3].InstanceDataStepRate = 0;

// MATRIX2
// SemanticName 语义名:							MATRIX (语义名后面不带数字)
// SemanticIndex 语义索引:						2
// InputSlot 输入槽:								第 1 号输入槽
// AlignedByteOffset 相对输入槽起始位置的偏移:		16 + 4*4 = 32
InputElementDesc[4].SemanticName = "MATRIX";
InputElementDesc[4].SemanticIndex = 2;
InputElementDesc[4].Format = DXGI_FORMAT_R32G32B32A32_FLOAT;
InputElementDesc[4].InputSlot = 1;
InputElementDesc[4].AlignedByteOffset = 32;
InputElementDesc[4].InputSlotClass = D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA;
InputElementDesc[4].InstanceDataStepRate = 0;

// MATRIX3
// SemanticName 语义名:							MATRIX (语义名后面不带数字)
// SemanticIndex 语义索引:						3
// InputSlot 输入槽:								第 1 号输入槽
// AlignedByteOffset 相对输入槽起始位置的偏移:		32 + 4*4 = 48
InputElementDesc[5].SemanticName = "MATRIX";
InputElementDesc[5].SemanticIndex = 3;
InputElementDesc[5].Format = DXGI_FORMAT_R32G32B32A32_FLOAT;
InputElementDesc[5].InputSlot = 1;
InputElementDesc[5].AlignedByteOffset = 48;
InputElementDesc[5].InputSlotClass = D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA;
InputElementDesc[5].InstanceDataStepRate = 0;



InputLayoutDesc.NumElements = 6;						// 输入元素个数
InputLayoutDesc.pInputElementDescs = InputElementDesc;	// 输入元素结构体数组指针
PSODesc.InputLayout = InputLayoutDesc;					// 设置渲染管线 IA 阶段的输入样式

大家有没有注意到,传递模型矩阵我们使用了第二个输入槽,每个模型也有两个 VertexBufferView,没错,我们用到了 Multi-Slot Input 多槽输入,现代 GPU 最多支持 16 个输入槽,多槽输入可以加快数据传递,加速渲染:


在这里插入图片描述
在这里插入图片描述

原图出处:DirectX12(D3D12)基础教程(十七)——让小姐姐翩翩起舞(3D骨骼动画渲染【6】)
鸣谢 GameBabyRockSun 大佬

修改渲染代码:Render


最后一步,我们修改渲染代码,添加这一句就大功告成了:

// 渲染全部模型
m_ModelManager.RenderAllModel(m_CommandList);

QQ2025414 14281 ezgif.com optimize


然而方块都没有渲染到正确的位置上,我们现在看到的画面是非常 “割裂” 的,这是因为我们没有考虑物体的遮挡关系,解决方法是:DepthStencilBuffer 深度模板缓冲。


遮挡算法


画家算法


画家算法就是先将物体按深度进行排序,然后从远到近依次渲染:


在这里插入图片描述


画家算法虽然简单,但有一个很大的缺陷:它无法解决下面相互遮挡的问题。


在这里插入图片描述


深度缓冲算法


为了解决画家算法带来的问题,Z-Buffer 深度缓冲算法应运而生。它的思路就是用一个等大小的缓冲存储每个像素的深度,如果有深度比它更大/更小的像素,就执行相应的操作:


在这里插入图片描述
在这里插入图片描述

在 DirectX 中,模板测试与深度测试 (Output Merger 输出合并阶段) 是相邻的,用于这两个测试的缓冲叫 Depth Stencil Buffer 深度模板缓冲,它也是一块资源。


在这里插入图片描述


创建深度模板描述符堆:CreateDSVHeap


和渲染目标一样,深度缓冲也有特定的描述符:Depth Stencil View 深度模板缓冲描述符:

// 创建 DSV 深度模板描述符堆 (Non-Shader Visible)
void CreateDSVHeap()
{
	D3D12_DESCRIPTOR_HEAP_DESC DSVHeapDesc = {};		// DSV 描述符堆结构体
	DSVHeapDesc.NumDescriptors = 1;						// 描述符只有 1 个,因为我们只有一个渲染目标
	DSVHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_DSV;	// 描述符堆类型
	
	// 创建 DSV 描述符堆 (Depth Stencil View,深度模板描述符),用于深度测试与模板测试
	m_D3D12Device->CreateDescriptorHeap(&DSVHeapDesc, IID_PPV_ARGS(&m_DSVHeap));

	// 获取 DSV 的 CPU 句柄
	DSVHandle = m_DSVHeap->GetCPUDescriptorHandleForHeapStart();
}

创建深度模板缓冲资源:CreateDepthStencilBuffer


注意深度模板缓冲是默认堆资源,它其实是一块特殊的纹理,有特定的 Format、Flag、ClearValue 和 ResourceState 需要指定,否则会创建失败:

// DSV 资源的格式
DXGI_FORMAT DSVFormat = DXGI_FORMAT_D24_UNORM_S8_UINT;	

// 创建深度与模板缓冲,用于开启深度测试,渲染物体正确的深度与遮挡关系
void CreateDepthStencilBuffer()
{
	D3D12_RESOURCE_DESC DSVResourceDesc = {};							// 深度模板缓冲资源信息结构体
	DSVResourceDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;		// 深度缓冲其实也是一块纹理
	DSVResourceDesc.Format = DSVFormat;									// 资源纹理格式
	DSVResourceDesc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN;				// 深度缓冲的布局也是 UNKNOWN
	DSVResourceDesc.Width = WindowWidth;								// 宽度和渲染目标一致
	DSVResourceDesc.Height = WindowHeight;								// 高度和渲染目标一致
	DSVResourceDesc.MipLevels = 1;										// Mipmap 层级,设置为 1 就行
	DSVResourceDesc.DepthOrArraySize = 1;								// 纹理数组大小 (3D 纹理深度),设置为 1 就行
	DSVResourceDesc.SampleDesc.Count = 1;								// 采样次数,设置为 1 就行
	DSVResourceDesc.SampleDesc.Quality = 0;								// 采样质量,设置为 0 就行
	// 资源标志,创建深度模板缓冲,必须要填 D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL,否则会创建失败
	DSVResourceDesc.Flags = D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL;	

	D3D12_CLEAR_VALUE DepthStencilBufferClearValue = {};				// 用于清空深度缓冲的信息结构体,DX12 能对这个进行优化
	DepthStencilBufferClearValue.DepthStencil.Depth = 1.0f;				// 要清空到的深度值,清空后会重置到该值
	DepthStencilBufferClearValue.DepthStencil.Stencil = 0;				// 要清空到的模板值,清空后会重置到该值
	DepthStencilBufferClearValue.Format = DSVFormat;					// 要清空缓冲的格式,要和上文一致

	// 默认堆属性,深度缓冲也是一块纹理,所以用默认堆
	D3D12_HEAP_PROPERTIES DefaultProperties = { D3D12_HEAP_TYPE_DEFAULT };

	// 创建资源,深度缓冲只会占用很少资源,所以直接 CreateCommittedResource 隐式堆创建即可,让操作系统帮我们管理
	m_D3D12Device->CreateCommittedResource(&DefaultProperties, D3D12_HEAP_FLAG_NONE, &DSVResourceDesc,
		D3D12_RESOURCE_STATE_DEPTH_WRITE, &DepthStencilBufferClearValue, IID_PPV_ARGS(&m_DepthStencilBuffer));
}

创建 DSV 描述符:CreateDSV


// 创建 DSV 描述符,DSV 描述符用于描述深度模板缓冲区,这个描述符才是渲染管线要设置的对象
void CreateDSV()
{
	D3D12_DEPTH_STENCIL_VIEW_DESC DSVViewDesc = {};
	DSVViewDesc.Format = DSVFormat;
	DSVViewDesc.ViewDimension = D3D12_DSV_DIMENSION_TEXTURE2D;
	DSVViewDesc.Flags = D3D12_DSV_FLAG_NONE;

	// 创建 DSV 描述符 (Depth Stencil View,深度模板描述符)
	m_D3D12Device->CreateDepthStencilView(m_DepthStencilBuffer.Get(), &DSVViewDesc, DSVHandle);
}

修改PSO:CreatePSO


在这里插入图片描述


光有深度缓冲是没用的,我们还需要修改渲染管线状态,让它开启深度测试:

// 设置深度测试状态
PSODesc.DSVFormat = DSVFormat;											// 设置深度缓冲的格式
PSODesc.DepthStencilState.DepthEnable = false;							// 开启深度缓冲
PSODesc.DepthStencilState.DepthFunc = D3D12_COMPARISON_FUNC_LESS;		// 深度缓冲的比较方式
PSODesc.DepthStencilState.DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ALL;	// 深度缓冲的读写权限

// D3D12_DEPTH_WRITE_MASK_ALL	允许通过深度测试的像素写入深度缓冲			(深度缓冲可读写)
// D3D12_DEPTH_WRITE_MASK_ZERO	禁止对深度缓冲进行写操作,但仍可进行深度测试	(深度缓冲只读)
// 两者只能选一个,不可共存。指定 DepthWriteMask 可以控制深度数据的读写,实现某些特效

/*
	深度测试比较像素深度的伪代码,符合条件就覆盖新像素,不符合就丢弃
	NewPixel:			要写入的新像素
	CurrentPixel:		当前在缓冲区的像素
	DepthFunc:			比较方式 (实际上就是 C/C++ 的二元操作运算符)

	if (NewPixel.Depth  DepthFunc  CurrentPixel.Depth)
	{
		Accept(NewPixel)			// 新像素通过深度测试,下一步可以进行混合
		WriteDepth(NewPixel.Depth)	// 将新像素深度写入深度缓冲中
	}
	else
	{
		Reject(NewPixel)			// 丢弃新像素
	}


	D3D12_COMPARISON_FUNC_LESS 相当于小于号 <

	if (NewPixel.Depth  <  CurrentPixel.Depth)
	{
		Accept(NewPixel)			// 如果新像素深度更小,说明距离摄像机更靠前,通过深度测试
		WriteDepth(NewPixel.Depth)	// 将新像素深度写入深度缓冲中
	}
	else
	{
		Reject(NewPixel)			// 否则,这个新像素更靠后,被当前像素遮住了,丢弃新像素
	}
*/

修改渲染代码:Render


最后一步就是修改渲染代码了,我们需要记录设置 DSV 句柄,清空深度缓冲的命令:

// 用 RTV 句柄设置渲染目标,同时用 DSV 句柄设置深度模板缓冲,开启深度测试
m_CommandList->OMSetRenderTargets(1, &RTVHandle, false, &DSVHandle);

// 清空后台的深度模板缓冲,将深度重置为初始值 1,记住上文创建深度缓冲资源的时候,要填 ClearValue
// 否则会报 D3D12 WARNING: The application did not pass any clear value to resource creation.
m_CommandList->ClearDepthStencilView(DSVHandle, D3D12_CLEAR_FLAG_DEPTH, 1, 0, 0, nullptr);

ClearDepthStencilView 清空深度模板缓冲区
第一个参数 DepthStencilView DSV 描述符句柄
第二个参数 ClearFlags 清理标志D3D12_CLEAR_FLAG_DEPTH 表示清理深度缓冲,D3D12_CLEAR_FLAG_STENCIL 表示清理模板缓冲,两个可以同时指定
第三个参数 Depth 要清理到的目标深度值
第四个参数 Stencil 要清理到的目标模板值
第五个参数 NumRects 需要清理的矩形区域数,0 表示对整个缓冲都进行清理
第六个参数 pRects 要清理的矩阵指针,nullptr 表示没有特定清理的矩形区域,默认对整个缓冲进行清理




第七章全代码


main.cpp



// (7) RenderMatchbox:渲染一个火柴盒,初步认识 Depth Stencil Buffer 深度模板缓冲,进一步认识顶点、模型与模型矩阵

#include<Windows.h>			// Windows 窗口编程核心头文件
#include<d3d12.h>			// DX12 核心头文件
#include<dxgi1_6.h>			// DXGI 头文件,用于管理与 DX12 相关联的其他必要设备,如 DXGI 工厂和 交换链
#include<DirectXColors.h>	// DirectX 颜色库
#include<DirectXMath.h>		// DirectX 数学库
#include<d3dcompiler.h>		// DirectX Shader 着色器编译库
#include<wincodec.h>		// WIC 图像处理框架,用于解码编码转换图片文件


#include<wrl.h>				// COM 组件模板库,方便写 DX12 和 DXGI 相关的接口
#include<string>			// C++ 标准 string 库
#include<sstream>			// C++ 字符串流处理库
#include<functional>		// C++ 标准函数对象库,用于下文的 std::function 函数包装器与 std::bind 绑定回调函数

#pragma comment(lib,"d3d12.lib")			// 链接 DX12 核心 DLL
#pragma comment(lib,"dxgi.lib")				// 链接 DXGI DLL
#pragma comment(lib,"dxguid.lib")			// 链接 DXGI 必要的设备 GUID
#pragma comment(lib,"d3dcompiler.lib")		// 链接 DX12 需要的着色器编译 DLL
#pragma comment(lib,"windowscodecs.lib")	// 链接 WIC DLL

using namespace Microsoft;
using namespace Microsoft::WRL;		// 使用 wrl.h 里面的命名空间,我们需要用到里面的 Microsoft::WRL::ComPtr COM智能指针
using namespace DirectX;			// DirectX 命名空间


// 命名空间 DX12TextureHelper 包含了帮助我们转换纹理图片格式的结构体与函数
namespace DX12TextureHelper
{
	// 纹理转换用,不是 DX12 所支持的格式,DX12 没法用

	// Standard GUID -> DXGI 格式转换结构体
	struct WICTranslate
	{
		GUID wic;
		DXGI_FORMAT format;
	};

	// WIC 格式与 DXGI 像素格式的对应表,该表中的格式为被支持的格式
	static WICTranslate g_WICFormats[] =
	{
		{ GUID_WICPixelFormat128bppRGBAFloat,       DXGI_FORMAT_R32G32B32A32_FLOAT },
		{ GUID_WICPixelFormat64bppRGBAHalf,         DXGI_FORMAT_R16G16B16A16_FLOAT },
		{ GUID_WICPixelFormat64bppRGBA,             DXGI_FORMAT_R16G16B16A16_UNORM },
		{ GUID_WICPixelFormat32bppRGBA,             DXGI_FORMAT_R8G8B8A8_UNORM },
		{ GUID_WICPixelFormat32bppBGRA,             DXGI_FORMAT_B8G8R8A8_UNORM },
		{ GUID_WICPixelFormat32bppBGR,              DXGI_FORMAT_B8G8R8X8_UNORM },
		{ GUID_WICPixelFormat32bppRGBA1010102XR,    DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM },
		{ GUID_WICPixelFormat32bppRGBA1010102,      DXGI_FORMAT_R10G10B10A2_UNORM },
		{ GUID_WICPixelFormat16bppBGRA5551,         DXGI_FORMAT_B5G5R5A1_UNORM },
		{ GUID_WICPixelFormat16bppBGR565,           DXGI_FORMAT_B5G6R5_UNORM },
		{ GUID_WICPixelFormat32bppGrayFloat,        DXGI_FORMAT_R32_FLOAT },
		{ GUID_WICPixelFormat16bppGrayHalf,         DXGI_FORMAT_R16_FLOAT },
		{ GUID_WICPixelFormat16bppGray,             DXGI_FORMAT_R16_UNORM },
		{ GUID_WICPixelFormat8bppGray,              DXGI_FORMAT_R8_UNORM },
		{ GUID_WICPixelFormat8bppAlpha,             DXGI_FORMAT_A8_UNORM }
	};

	// GUID -> Standard GUID 格式转换结构体
	struct WICConvert
	{
		GUID source;
		GUID target;
	};

	// WIC 像素格式转换表
	static WICConvert g_WICConvert[] =
	{
		// 目标格式一定是最接近的被支持的格式
		{ GUID_WICPixelFormatBlackWhite,            GUID_WICPixelFormat8bppGray },			// DXGI_FORMAT_R8_UNORM
		{ GUID_WICPixelFormat1bppIndexed,           GUID_WICPixelFormat32bppRGBA },			// DXGI_FORMAT_R8G8B8A8_UNORM
		{ GUID_WICPixelFormat2bppIndexed,           GUID_WICPixelFormat32bppRGBA },			// DXGI_FORMAT_R8G8B8A8_UNORM
		{ GUID_WICPixelFormat4bppIndexed,           GUID_WICPixelFormat32bppRGBA },			// DXGI_FORMAT_R8G8B8A8_UNORM
		{ GUID_WICPixelFormat8bppIndexed,           GUID_WICPixelFormat32bppRGBA },			// DXGI_FORMAT_R8G8B8A8_UNORM
		{ GUID_WICPixelFormat2bppGray,              GUID_WICPixelFormat8bppGray },			// DXGI_FORMAT_R8_UNORM
		{ GUID_WICPixelFormat4bppGray,              GUID_WICPixelFormat8bppGray },			// DXGI_FORMAT_R8_UNORM
		{ GUID_WICPixelFormat16bppGrayFixedPoint,   GUID_WICPixelFormat16bppGrayHalf },		// DXGI_FORMAT_R16_FLOAT
		{ GUID_WICPixelFormat32bppGrayFixedPoint,   GUID_WICPixelFormat32bppGrayFloat },	// DXGI_FORMAT_R32_FLOAT
		{ GUID_WICPixelFormat16bppBGR555,           GUID_WICPixelFormat16bppBGRA5551 },		// DXGI_FORMAT_B5G5R5A1_UNORM
		{ GUID_WICPixelFormat32bppBGR101010,        GUID_WICPixelFormat32bppRGBA1010102 },	// DXGI_FORMAT_R10G10B10A2_UNORM
		{ GUID_WICPixelFormat24bppBGR,              GUID_WICPixelFormat32bppRGBA },			// DXGI_FORMAT_R8G8B8A8_UNORM
		{ GUID_WICPixelFormat24bppRGB,              GUID_WICPixelFormat32bppRGBA },			// DXGI_FORMAT_R8G8B8A8_UNORM
		{ GUID_WICPixelFormat32bppPBGRA,            GUID_WICPixelFormat32bppRGBA },			// DXGI_FORMAT_R8G8B8A8_UNORM
		{ GUID_WICPixelFormat32bppPRGBA,            GUID_WICPixelFormat32bppRGBA },			// DXGI_FORMAT_R8G8B8A8_UNORM
		{ GUID_WICPixelFormat48bppRGB,              GUID_WICPixelFormat64bppRGBA },			// DXGI_FORMAT_R16G16B16A16_UNORM
		{ GUID_WICPixelFormat48bppBGR,              GUID_WICPixelFormat64bppRGBA },			// DXGI_FORMAT_R16G16B16A16_UNORM
		{ GUID_WICPixelFormat64bppBGRA,             GUID_WICPixelFormat64bppRGBA },			// DXGI_FORMAT_R16G16B16A16_UNORM
		{ GUID_WICPixelFormat64bppPRGBA,            GUID_WICPixelFormat64bppRGBA },			// DXGI_FORMAT_R16G16B16A16_UNORM
		{ GUID_WICPixelFormat64bppPBGRA,            GUID_WICPixelFormat64bppRGBA },			// DXGI_FORMAT_R16G16B16A16_UNORM
		{ GUID_WICPixelFormat48bppRGBFixedPoint,    GUID_WICPixelFormat64bppRGBAHalf },		// DXGI_FORMAT_R16G16B16A16_FLOAT
		{ GUID_WICPixelFormat48bppBGRFixedPoint,    GUID_WICPixelFormat64bppRGBAHalf },		// DXGI_FORMAT_R16G16B16A16_FLOAT
		{ GUID_WICPixelFormat64bppRGBAFixedPoint,   GUID_WICPixelFormat64bppRGBAHalf },		// DXGI_FORMAT_R16G16B16A16_FLOAT
		{ GUID_WICPixelFormat64bppBGRAFixedPoint,   GUID_WICPixelFormat64bppRGBAHalf },		// DXGI_FORMAT_R16G16B16A16_FLOAT
		{ GUID_WICPixelFormat64bppRGBFixedPoint,    GUID_WICPixelFormat64bppRGBAHalf },		// DXGI_FORMAT_R16G16B16A16_FLOAT
		{ GUID_WICPixelFormat48bppRGBHalf,          GUID_WICPixelFormat64bppRGBAHalf },		// DXGI_FORMAT_R16G16B16A16_FLOAT
		{ GUID_WICPixelFormat64bppRGBHalf,          GUID_WICPixelFormat64bppRGBAHalf },		// DXGI_FORMAT_R16G16B16A16_FLOAT
		{ GUID_WICPixelFormat128bppPRGBAFloat,      GUID_WICPixelFormat128bppRGBAFloat },	// DXGI_FORMAT_R32G32B32A32_FLOAT
		{ GUID_WICPixelFormat128bppRGBFloat,        GUID_WICPixelFormat128bppRGBAFloat },	// DXGI_FORMAT_R32G32B32A32_FLOAT
		{ GUID_WICPixelFormat128bppRGBAFixedPoint,  GUID_WICPixelFormat128bppRGBAFloat },	// DXGI_FORMAT_R32G32B32A32_FLOAT
		{ GUID_WICPixelFormat128bppRGBFixedPoint,   GUID_WICPixelFormat128bppRGBAFloat },	// DXGI_FORMAT_R32G32B32A32_FLOAT
		{ GUID_WICPixelFormat32bppRGBE,             GUID_WICPixelFormat128bppRGBAFloat },	// DXGI_FORMAT_R32G32B32A32_FLOAT
		{ GUID_WICPixelFormat32bppCMYK,             GUID_WICPixelFormat32bppRGBA },			// DXGI_FORMAT_R8G8B8A8_UNORM
		{ GUID_WICPixelFormat64bppCMYK,             GUID_WICPixelFormat64bppRGBA },			// DXGI_FORMAT_R16G16B16A16_UNORM
		{ GUID_WICPixelFormat40bppCMYKAlpha,        GUID_WICPixelFormat64bppRGBA },			// DXGI_FORMAT_R16G16B16A16_UNORM
		{ GUID_WICPixelFormat80bppCMYKAlpha,        GUID_WICPixelFormat64bppRGBA },			// DXGI_FORMAT_R16G16B16A16_UNORM
		{ GUID_WICPixelFormat32bppRGB,              GUID_WICPixelFormat32bppRGBA },			// DXGI_FORMAT_R8G8B8A8_UNORM
		{ GUID_WICPixelFormat64bppRGB,              GUID_WICPixelFormat64bppRGBA },			// DXGI_FORMAT_R16G16B16A16_UNORM
		{ GUID_WICPixelFormat64bppPRGBAHalf,        GUID_WICPixelFormat64bppRGBAHalf },		// DXGI_FORMAT_R16G16B16A16_FLOAT

		{ GUID_WICPixelFormat128bppRGBAFloat,       GUID_WICPixelFormat128bppRGBAFloat },	// DXGI_FORMAT_R32G32B32A32_FLOAT
		{ GUID_WICPixelFormat64bppRGBAHalf,         GUID_WICPixelFormat64bppRGBAHalf },		// DXGI_FORMAT_R16G16B16A16_FLOAT
		{ GUID_WICPixelFormat64bppRGBA,             GUID_WICPixelFormat64bppRGBA },			// DXGI_FORMAT_R16G16B16A16_UNORM
		{ GUID_WICPixelFormat32bppRGBA,             GUID_WICPixelFormat32bppRGBA },			// DXGI_FORMAT_R8G8B8A8_UNORM
		{ GUID_WICPixelFormat32bppBGRA,             GUID_WICPixelFormat32bppBGRA },			// DXGI_FORMAT_B8G8R8A8_UNORM
		{ GUID_WICPixelFormat32bppBGR,              GUID_WICPixelFormat32bppBGR },			// DXGI_FORMAT_B8G8R8X8_UNORM
		{ GUID_WICPixelFormat32bppRGBA1010102XR,    GUID_WICPixelFormat32bppRGBA1010102XR },// DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM
		{ GUID_WICPixelFormat32bppRGBA1010102,      GUID_WICPixelFormat32bppRGBA1010102 },	// DXGI_FORMAT_R10G10B10A2_UNORM
		{ GUID_WICPixelFormat16bppBGRA5551,         GUID_WICPixelFormat16bppBGRA5551 },		// DXGI_FORMAT_B5G5R5A1_UNORM
		{ GUID_WICPixelFormat16bppBGR565,           GUID_WICPixelFormat16bppBGR565 },		// DXGI_FORMAT_B5G6R5_UNORM
		{ GUID_WICPixelFormat32bppGrayFloat,        GUID_WICPixelFormat32bppGrayFloat },	// DXGI_FORMAT_R32_FLOAT
		{ GUID_WICPixelFormat16bppGrayHalf,         GUID_WICPixelFormat16bppGrayHalf },		// DXGI_FORMAT_R16_FLOAT
		{ GUID_WICPixelFormat16bppGray,             GUID_WICPixelFormat16bppGray },			// DXGI_FORMAT_R16_UNORM
		{ GUID_WICPixelFormat8bppGray,              GUID_WICPixelFormat8bppGray },			// DXGI_FORMAT_R8_UNORM
		{ GUID_WICPixelFormat8bppAlpha,             GUID_WICPixelFormat8bppAlpha }			// DXGI_FORMAT_A8_UNORM
	};


	// 查表确定兼容的最接近格式是哪个
	bool GetTargetPixelFormat(const GUID* pSourceFormat, GUID* pTargetFormat)
	{
		*pTargetFormat = *pSourceFormat;
		for (size_t i = 0; i < _countof(g_WICConvert); ++i)
		{
			if (InlineIsEqualGUID(g_WICConvert[i].source, *pSourceFormat))
			{
				*pTargetFormat = g_WICConvert[i].target;
				return true;
			}
		}
		return false;		// 找不到,就返回 false
	}

	// 查表确定最终对应的 DXGI 格式是哪一个
	DXGI_FORMAT GetDXGIFormatFromPixelFormat(const GUID* pPixelFormat)
	{
		for (size_t i = 0; i < _countof(g_WICFormats); ++i)
		{
			if (InlineIsEqualGUID(g_WICFormats[i].wic, *pPixelFormat))
			{
				return g_WICFormats[i].format;
			}
		}
		return DXGI_FORMAT_UNKNOWN;		// 找不到,就返回 UNKNOWN
	}
}


// 用于绑定回调函数的中间层
class CallBackWrapper
{
public:

	// 用于保存 DX12Engine 类的成员回调函数的包装器
	// 前 5 章我们一直将类里面的回调函数设置成 static 静态函数,是因为 WIN32 API 是用纯 C 风格写的
	// WNDCLASS 的 lpfnWndProc 是 C-Style 的函数指针,而 DX12Engine::CallBackFunc 是类成员函数,还需要传递一个 this 指针
	// 这个 this 指针还包含了类实例的额外信息 (类成员,虚函数表,类继承关系),但是 lpfnWndProc 传不了这个 this 指针
	// 函数声明不兼容,所以没法直接将 DX12Engine::CallBackFunc 赋值给 lpfnWndProc,常规的强制转换都不行 (reinterpret_cast 也不行)
	// 我们可以利用 C++11 的函数包装器 std::function ,利用它来保存 DX12Engine::CallBackFunc
	// [利用的是模板+仿函数 (函数对象) 闭包的特性,感兴趣可以查查资料,简单了解一下。如果想深究可以看看源码,这玩意内部实现非常神奇]
	// 然后再通过下文的静态函数 CallBackWrapper::CallBackFunc 间接调用,将这个 CallBackWrapper::CallBackFunc 传给 lpfnWndProc
	// 这样就实现了 类回调函数 -> C-Style 普通回调函数 的转化
	// 用 C++17 的 inline static 原因是 static 静态非常量成员要求类内声明,类外定义,不做类外定义的话就会报函数链接错误
	// 类内的静态成员变量仅仅是一个声明,要定义的时候才分配内存,在定义之前都是不可访问的,所以静态非常量成员不能在类内初始化
	// [详情可以看:https://www.cnblogs.com/lixuejian/p/13215271]
	// 如果去掉 inline,就要在后面加上定义。而且如果遇到多文件编译,每个文件都要定义这玩意的时候,就会发生链接错误,非常麻烦
	// inline static 允许静态成员变量可以直接类内初始化,可以完美规避上面这两种麻烦情况,所以我们才使用它
	inline static std::function<LRESULT(HWND, UINT, WPARAM, LPARAM)> Broker_Func;

	// 用于传递到 lpfnWndProc 的静态成员函数,内部调用保存 DX12Engine::CallBackFunc 的函数包装器
	// 静态成员函数属于类,不属于类实例对象,所以没有 this 指针,可以直接赋值给 C-Style 的函数指针
	static LRESULT CALLBACK CallBackFunc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
	{
		return Broker_Func(hwnd, msg, wParam, lParam);
	}

};


// 摄像机类
class Camera
{
private:

	XMVECTOR EyePosition = XMVectorSet(4, 5, -4, 1);		// 摄像机在世界空间下的位置
	XMVECTOR FocusPosition = XMVectorSet(4, 3, 4, 1);		// 摄像机在世界空间下观察的焦点位置
	XMVECTOR UpDirection = XMVectorSet(0, 1, 0, 0);			// 世界空间垂直向上的向量

	// 摄像机观察方向的单位向量,用于前后移动
	XMVECTOR ViewDirection = XMVector3Normalize(FocusPosition - EyePosition);

	// 焦距,摄像机原点与焦点的距离,XMVector3Length 表示对向量取模
	float FocalLength = XMVectorGetX(XMVector3Length(FocusPosition - EyePosition));

	// 摄像机向右方向的单位向量,用于左右移动,XMVector3Cross 求两向量叉乘
	XMVECTOR RightDirection = XMVector3Normalize(XMVector3Cross(ViewDirection, UpDirection));

	POINT LastCursorPoint = {};								// 上一次鼠标的位置

	float FovAngleY = XM_PIDIV4;							// 垂直视场角
	float AspectRatio = 4.0 / 3.0;							// 投影窗口宽高比
	float NearZ = 0.1;										// 近平面到原点的距离
	float FarZ = 1000;										// 远平面到原点的距离

	XMMATRIX ViewMatrix;									// 观察矩阵,世界空间 -> 观察空间
	XMMATRIX ProjectionMatrix;								// 投影矩阵,观察空间 -> 齐次裁剪空间

	XMMATRIX MVPMatrix;										// MVP 矩阵,类外需要用公有方法 GetMVPMatrix 获取

public:

	Camera()	// 摄像机的构造函数
	{
		// 注意!我们这里移除了模型矩阵!每个模型会指定具体的模型矩阵!

		// 观察矩阵,注意前两个参数是点,第三个参数才是向量
		ViewMatrix = XMMatrixLookAtLH(EyePosition, FocusPosition, UpDirection);
		// 投影矩阵 (注意近平面和远平面距离不能 <= 0!)
		ProjectionMatrix = XMMatrixPerspectiveFovLH(FovAngleY, AspectRatio, NearZ, FarZ);
	}

	// 摄像机前后移动,参数 Stride 是移动速度 (步长),正数向前移动,负数向后移动
	void Walk(float Stride)
	{
		EyePosition += Stride * ViewDirection;
		FocusPosition += Stride * ViewDirection;
	}

	// 摄像机左右移动,参数 Stride 是移动速度 (步长),正数向左移动,负数向右移动
	void Strafe(float Stride)
	{
		EyePosition += Stride * RightDirection;
		FocusPosition += Stride * RightDirection;
	}

	// 鼠标在屏幕空间 y 轴上移动,相当于摄像机以向右的向量 RightDirection 向上向下旋转,人眼往上下看
	void RotateByY(float angleY)
	{
		// 以向右向量为轴构建旋转矩阵,旋转 ViewDirection 和 UpDirection
		XMMATRIX R = XMMatrixRotationAxis(RightDirection, -angleY);

		UpDirection = XMVector3TransformNormal(UpDirection, R);
		ViewDirection = XMVector3TransformNormal(ViewDirection, R);

		// 利用 ViewDirection 观察向量、FocalLength 焦距,更新焦点位置
		FocusPosition = EyePosition + ViewDirection * FocalLength;
	}

	// 鼠标在屏幕空间 x 轴上移动,相当于摄像机绕世界空间的 y 轴向左向右旋转,人眼往左右看
	void RotateByX(float angleX)
	{
		// 以世界坐标系下的 y 轴 (0,1,0,0) 构建旋转矩阵,三个向量 ViewDirection, UpDirection, RightDirection 都要旋转
		XMMATRIX R = XMMatrixRotationY(angleX);

		UpDirection = XMVector3TransformNormal(UpDirection, R);
		ViewDirection = XMVector3TransformNormal(ViewDirection, R);
		RightDirection = XMVector3TransformNormal(RightDirection, R);

		// 利用 ViewDirection 观察向量、FocalLength 焦距,更新焦点位置
		FocusPosition = EyePosition + ViewDirection * FocalLength;
	}

	// 更新上一次的鼠标位置
	void UpdateLastCursorPos()
	{
		GetCursorPos(&LastCursorPoint);
	}

	// 当鼠标左键长按并移动时,旋转摄像机视角
	void CameraRotate()
	{
		POINT CurrentCursorPoint = {};
		GetCursorPos(&CurrentCursorPoint);	// 获取当前鼠标位置

		// 根据鼠标在屏幕坐标系的 x,y 轴的偏移量,计算摄像机旋转角
		float AngleX = XMConvertToRadians(0.25 * static_cast<float>(CurrentCursorPoint.x - LastCursorPoint.x));
		float AngleY = XMConvertToRadians(0.25 * static_cast<float>(CurrentCursorPoint.y - LastCursorPoint.y));

		// 旋转摄像机
		RotateByY(AngleY);
		RotateByX(AngleX);

		UpdateLastCursorPos();		// 旋转完毕,更新上一次的鼠标位置
	}

	// 更新 MVP 矩阵
	void UpdateMVPMatrix()
	{
		// 主要是更新观察矩阵
		ViewMatrix = XMMatrixLookAtLH(EyePosition, FocusPosition, UpDirection);
		MVPMatrix = ViewMatrix * ProjectionMatrix;
	}

	// 获取 MVP 矩阵
	inline XMMATRIX& GetMVPMatrix()
	{
		// 每次返回前,都更新一次
		UpdateMVPMatrix();
		return MVPMatrix;
	}
};


// 顶点
struct VERTEX
{
	XMFLOAT4 position;			// 顶点在模型坐标系的坐标
	XMFLOAT2 texcoordUV;		// 顶点纹理 UV 坐标
};


// 模型类,这是个抽象类,有两个纯虚函数,派生类需要实现下面两个纯虚函数才能创建实例
class Model
{
protected:	// 保护项,仅在自身和派生类可见

	XMMATRIX ModelMatrix = XMMatrixIdentity();			// 模型矩阵,模型空间 -> 世界空间

	ComPtr<ID3D12Resource> m_VertexResource;			// D3D12 顶点资源
	ComPtr<ID3D12Resource> m_ModelMatrixResource;		// D3D12 模型矩阵资源
	ComPtr<ID3D12Resource> m_IndexResource;				// D3D12 索引资源

	// 每个模型的 VBV 顶点信息描述符数组,数组每个元素占用一个输入槽,多槽输入可以加速 CPU-GPU 的传递
	// VertexBufferView[0] 描述每个顶点的顶点信息 (position 位置,texcoordUV 纹理 UV 坐标)
	// VertexBufferView[1] 描述每个顶点对应的模型矩阵,模型矩阵会在 IA 阶段拆分成四个行向量进行输入,之后在 VS 阶段重新组装成矩阵
	D3D12_VERTEX_BUFFER_VIEW VertexBufferView[2] = {};

	// 每个模型的 IBV 顶点索引描述符,一个模型只有一个索引描述符
	D3D12_INDEX_BUFFER_VIEW IndexBufferView = {};

	// 纹理名 - GPU 句柄映射表,用于索引纹理,设置根参数
	std::unordered_map<std::string, D3D12_GPU_DESCRIPTOR_HANDLE> Texture_GPUHandle_Map;

	// 添加新纹理 (key) 到映射表,对应的 GPUHandle 先设置为 nullptr,仅限派生类可用,对外不公开
	void AppendTextureKey(std::string&& TextureName)
	{
		Texture_GPUHandle_Map[TextureName] = {};
	}

public:		// 公共项,全局均可见

	// 类外获取模型需要的纹理,返回映射表的只读引用
	const std::unordered_map<std::string, D3D12_GPU_DESCRIPTOR_HANDLE>& RequestForTextureMap()
	{
		return Texture_GPUHandle_Map;
	}

	// 模型获取类外已经创建纹理 SRV 描述符的 SRVHandle
	void SetTextureGPUHandle(std::string TextureName, D3D12_GPU_DESCRIPTOR_HANDLE GPUHandle)
	{
		Texture_GPUHandle_Map[TextureName] = GPUHandle;
	}

	// 获取模型矩阵
	XMMATRIX GetModelMatrix()
	{
		return ModelMatrix;
	}

	// 设置模型矩阵
	void SetModelMatrix(XMMATRIX Matrix)
	{
		ModelMatrix = Matrix;
	}

	// 创建资源与描述符,这个是纯虚函数,实例类需要实现
	virtual void CreateResourceAndDescriptor(ComPtr<ID3D12Device4>& pD3D12Device) = 0;

	// 绘制模型,这个也是纯虚函数,实例类需要实现
	virtual void DrawModel(ComPtr<ID3D12GraphicsCommandList>& pCommandList) = 0;

};


// 全遮挡固体方块类 (抽象类),继承自模型类,只定义 CreateResourceAndDescriptor 这个函数,DrawModel 仍然需要派生类实现
class SoildBlock : public Model
{
protected:	// 保护项,仅在自身和派生类可见

	// CPU 高速缓存上的顶点信息数组 (静态成员,属于类,不属于类实例对象,只初始化一次)
	// 注意 DirectX 使用的是左手坐标系,写顶点信息时,请比一比你的左手!
	inline static VERTEX VertexArray[24] =
	{
		// 正面
		{{0,1,0,1},{0,0}},
		{{1,1,0,1},{1,0}},
		{{1,0,0,1},{1,1}},
		{{0,0,0,1},{0,1}},

		// 背面
		{{1,1,1,1},{0,0}},
		{{0,1,1,1},{1,0}},
		{{0,0,1,1},{1,1}},
		{{1,0,1,1},{0,1}},

		// 左面
		{{0,1,1,1},{0,0}},
		{{0,1,0,1},{1,0}},
		{{0,0,0,1},{1,1}},
		{{0,0,1,1},{0,1}},

		// 右面
		{{1,1,0,1},{0,0}},
		{{1,1,1,1},{1,0}},
		{{1,0,1,1},{1,1}},
		{{1,0,0,1},{0,1}},

		// 上面
		{{0,1,1,1},{0,0}},
		{{1,1,1,1},{1,0}},
		{{1,1,0,1},{1,1}},
		{{0,1,0,1},{0,1}},

		// 下面
		{{0,0,0,1},{0,0}},
		{{1,0,0,1},{1,0}},
		{{1,0,1,1},{1,1}},
		{{0,0,1,1},{0,1}}
	};

	// 顶点索引数组 (静态成员,属于类,不属于类实例对象,只初始化一次)
	// 注意这里的 UINT == UINT32,后面填的格式 (步长) 必须是 DXGI_FORMAT_R32_UINT,否则会出错
	inline static UINT IndexArray[36] =
	{
		// 正面
		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
	};


public:		// 公共项,全局均可见

	// 创建资源与描述符,函数声明的 override 在编译期能检查虚函数是否被重写,重写/实现函数常写 override 是一种好习惯
	virtual void CreateResourceAndDescriptor(ComPtr<ID3D12Device4>& pD3D12Device) override
	{
		// 临时设置 XMFLOAT4X4 类型的模型矩阵,XMFLOAT4X4 擅长存储与传递,XMMATRIX 擅长并行运算
		XMFLOAT4X4 _temp_ModelMatrix = {};
		XMStoreFloat4x4(&_temp_ModelMatrix, ModelMatrix);

		// 用于批量复制模型矩阵的 vector,vector 的底层是一块连续内存,memcpy 复制连续内存有 CPU 优化,能快很多
		std::vector<XMFLOAT4X4> _temp_ModelMatrixGroup;
		// 批量填充 ModelMatrix 到 ModelMatrixGroup
		_temp_ModelMatrixGroup.assign(24, _temp_ModelMatrix);


		// 用于创建上传堆资源的 D3D12Resource 信息结构体,这个结构体可复用
		D3D12_RESOURCE_DESC UploadResourceDesc = {};
		UploadResourceDesc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER;		// 上传堆资源类型都是 BUFFER 缓冲
		UploadResourceDesc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR;			// row_major 行主序,上传堆资源都是按行存储的
		UploadResourceDesc.Height = 1;										// 上传堆资源高度都是 1 (线性存储)
		UploadResourceDesc.Format = DXGI_FORMAT_UNKNOWN;					// 上传堆资源都是 DXGI_FORMAT_UNKNOWN
		UploadResourceDesc.MipLevels = 1;									// 上传堆资源没有 Mipmap,所以都设 1
		UploadResourceDesc.DepthOrArraySize = 1;							// 上传堆资源都设 1
		UploadResourceDesc.SampleDesc.Count = 1;							// 采样次数,上传堆资源都设 1
		UploadResourceDesc.SampleDesc.Quality = 0;							// 采样质量,上传堆资源都设 0

		// 上传堆属性
		D3D12_HEAP_PROPERTIES HeapProperties = { D3D12_HEAP_TYPE_UPLOAD };

		// 创建 VertexResource
		UploadResourceDesc.Width = 24 * sizeof(VERTEX);		// 宽度就是 VertexGroup 总元素大小

		// 以隐式堆方式创建资源,好处是简单方便,坏处是隐式堆由操作系统全权管理,开发者无法手动管理隐式堆的属性和生命周期
		pD3D12Device->CreateCommittedResource(&HeapProperties, D3D12_HEAP_FLAG_NONE,
			&UploadResourceDesc, D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&m_VertexResource));

		// 创建 ModelMatrix
		UploadResourceDesc.Width = 24 * sizeof(XMFLOAT4X4);

		pD3D12Device->CreateCommittedResource(&HeapProperties, D3D12_HEAP_FLAG_NONE,
			&UploadResourceDesc, D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&m_ModelMatrixResource));

		// 创建 IndexResource
		UploadResourceDesc.Width = 36 * sizeof(XMFLOAT4X4);

		pD3D12Device->CreateCommittedResource(&HeapProperties, D3D12_HEAP_FLAG_NONE,
			&UploadResourceDesc, D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&m_IndexResource));


		// 将数据进行转移: CPU 高速缓存 -> CPU 共享内存

		BYTE* TransmitPointer = nullptr;	// 用于传递数据的指针

		// 映射资源,获取 D3D12 资源的地址,同时 D3D12 资源开放写权限
		m_VertexResource->Map(0, nullptr, reinterpret_cast<void**>(&TransmitPointer));
		// 用 memcpy 将数据复制到 D3D12 资源
		memcpy(TransmitPointer, VertexArray, 24 * sizeof(VERTEX));
		// 关闭映射,资源只写,这样静态资源读取效率会快很多,动态资源无需关闭映射
		m_VertexResource->Unmap(0, nullptr);


		m_ModelMatrixResource->Map(0, nullptr, reinterpret_cast<void**>(&TransmitPointer));
		memcpy(TransmitPointer, &_temp_ModelMatrixGroup[0], 24 * sizeof(XMFLOAT4X4));
		m_ModelMatrixResource->Unmap(0, nullptr);


		m_IndexResource->Map(0, nullptr, reinterpret_cast<void**>(&TransmitPointer));
		memcpy(TransmitPointer, IndexArray, 36 * sizeof(UINT));
		m_IndexResource->Unmap(0, nullptr);


		// 创建完所有资源,就可以填写 VBV 和 IBV 描述符了

		// VBV[0]: 描述顶点位置与纹理 UV 坐标,占据第 0 号输入槽
		VertexBufferView[0].BufferLocation = m_VertexResource->GetGPUVirtualAddress();		// D3D12 资源地址
		VertexBufferView[0].SizeInBytes = 24 * sizeof(VERTEX);								// D3D12 资源总大小
		VertexBufferView[0].StrideInBytes = sizeof(VERTEX);									// D3D12 资源单个元素的大小 (步长)

		// VBV[1]: 描述顶点的模型矩阵,占据第 1 号输入槽
		VertexBufferView[1].BufferLocation = m_ModelMatrixResource->GetGPUVirtualAddress();	// D3D12 资源地址
		VertexBufferView[1].SizeInBytes = 24 * sizeof(XMFLOAT4X4);							// D3D12 资源总大小
		VertexBufferView[1].StrideInBytes = sizeof(XMFLOAT4X4);								// D3D12 资源单个元素的大小 (步长)

		// IBV: 描述顶点的索引
		IndexBufferView.BufferLocation = m_IndexResource->GetGPUVirtualAddress();			// D3D12 资源地址
		IndexBufferView.SizeInBytes = 36 * sizeof(UINT);									// D3D12 资源总大小
		IndexBufferView.Format = DXGI_FORMAT_R32_UINT;										// D3D12 资源单个元素的大小 (步长)
	}
};


// 台阶方块类 (抽象类),继承自模型类,只定义 CreateResourceAndDescriptor 这个函数,DrawModel 仍然需要派生类实现
class SoildStair : public Model
{
protected:	// 保护项,仅在自身和派生类可见

	// CPU 高速缓存上的顶点信息数组 (静态成员,属于类,不属于类实例对象,只初始化一次)
	// 注意 DirectX 使用的是左手坐标系,写顶点信息时,请比一比你的左手!
	inline static VERTEX VertexArray[40] =
	{
		// 台阶底面
		{{0,0,0,1},{0,0}},
		{{1,0,0,1},{1,0}},
		{{1,0,1,1},{1,1}},
		{{0,0,1,1},{0,1}},

		// 台阶背面
		{{1,1,1,1},{0,0}},
		{{0,1,1,1},{1,0}},
		{{0,0,1,1},{1,1}},
		{{1,0,1,1},{0,1}},

		// 台阶正面
		{{0,0.5,0,1},{0,0.5}},
		{{1,0.5,0,1},{1,0.5}},
		{{1,0,0,1},{1,1}},
		{{0,0,0,1},{0,1}},

		{{0,1,0.5,1},{0,0}},
		{{1,1,0.5,1},{1,0}},
		{{1,0.5,0.5,1},{1,0.5}},
		{{0,0.5,0.5,1},{0,0.5}},

		// 台阶顶面
		{{0,0.5,0.5,1},{0,0.5}},
		{{1,0.5,0.5,1},{1,0.5}},
		{{1,0.5,0,1},{1,1}},
		{{0,0.5,0,1},{0,1}},

		{{0,1,1,1},{0,0}},
		{{1,1,1,1},{1,0}},
		{{1,1,0.5,1},{1,0.5}},
		{{0,1,0.5,1},{0,0.5}},

		// 台阶左面
		{{0,1,1,1},{0,0}},
		{{0,1,0.5,1},{0.5,0}},
		{{0,0,0.5,1},{0.5,1}},
		{{0,0,1,1},{0,1}},

		{{0,0.5,0.5,1},{0.5,0.5}},
		{{0,0.5,0,1},{1,0.5}},
		{{0,0,0,1},{1,1}},
		{{0,0,0.5,1},{0.5,1}},

		// 台阶右面
		{{1,1,0.5,1},{0.5,0}},
		{{1,1,1,1},{1,0}},
		{{1,0,1,1},{1,1}},
		{{1,0,0.5,1},{0.5,1}},

		{{1,0.5,0,1},{0,0.5}},
		{{1,0.5,0.5,1},{0.5,0.5}},
		{{1,0,0.5,1},{0.5,1}},
		{{1,0,0,1},{0,1}}
	};

	// 顶点索引数组 (静态成员,属于类,不属于类实例对象,只初始化一次)
	// 注意这里的 UINT == UINT32,后面填的格式 (步长) 必须是 DXGI_FORMAT_R32_UINT,否则会出错
	inline static UINT IndexArray[60] =
	{
		// 台阶底面
		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,
		// 台阶左面
		24,25,26,24,26,27,
		28,29,30,28,30,31,
		// 台阶右面
		32,33,34,32,34,35,
		36,37,38,36,38,39
	};


public:		// 公共项,全局均可见

	// 创建资源与描述符,函数声明的 override 在编译期能检查虚函数是否被重写,重写/实现函数常写 override 是一种好习惯
	virtual void CreateResourceAndDescriptor(ComPtr<ID3D12Device4>& pD3D12Device) override
	{
		// 临时设置 XMFLOAT4X4 类型的模型矩阵,XMFLOAT4X4 擅长存储与传递,XMMATRIX 擅长并行运算
		XMFLOAT4X4 _temp_ModelMatrix = {};
		XMStoreFloat4x4(&_temp_ModelMatrix, ModelMatrix);

		// 用于批量复制模型矩阵的 vector,vector 的底层是一块连续内存,memcpy 复制连续内存有 CPU 优化,能快很多
		std::vector<XMFLOAT4X4> _temp_ModelMatrixGroup;
		// 批量填充 ModelMatrix 到 ModelMatrixGroup
		_temp_ModelMatrixGroup.assign(40, _temp_ModelMatrix);


		// 用于创建上传堆资源的 D3D12Resource 信息结构体,这个结构体可复用
		D3D12_RESOURCE_DESC UploadResourceDesc = {};
		UploadResourceDesc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER;		// 上传堆资源类型都是 BUFFER 缓冲
		UploadResourceDesc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR;			// row_major 行主序,上传堆资源都是按行存储的
		UploadResourceDesc.Height = 1;										// 上传堆资源高度都是 1 (线性存储)
		UploadResourceDesc.Format = DXGI_FORMAT_UNKNOWN;					// 上传堆资源都是 DXGI_FORMAT_UNKNOWN
		UploadResourceDesc.MipLevels = 1;									// 上传堆资源没有 Mipmap,所以都设 1
		UploadResourceDesc.DepthOrArraySize = 1;							// 上传堆资源都设 1
		UploadResourceDesc.SampleDesc.Count = 1;							// 采样次数,上传堆资源都设 1
		UploadResourceDesc.SampleDesc.Quality = 0;							// 采样质量,上传堆资源都设 0

		// 上传堆属性
		D3D12_HEAP_PROPERTIES HeapProperties = { D3D12_HEAP_TYPE_UPLOAD };

		// 创建 VertexResource
		UploadResourceDesc.Width = 40 * sizeof(VERTEX);		// 宽度就是 VertexGroup 总元素大小

		// 以隐式堆方式创建资源,好处是简单方便,坏处是隐式堆由操作系统全权管理,开发者无法手动管理隐式堆的属性和生命周期
		pD3D12Device->CreateCommittedResource(&HeapProperties, D3D12_HEAP_FLAG_NONE,
			&UploadResourceDesc, D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&m_VertexResource));

		// 创建 ModelMatrix
		UploadResourceDesc.Width = 40 * sizeof(XMFLOAT4X4);

		pD3D12Device->CreateCommittedResource(&HeapProperties, D3D12_HEAP_FLAG_NONE,
			&UploadResourceDesc, D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&m_ModelMatrixResource));

		// 创建 IndexResource
		UploadResourceDesc.Width = 60 * sizeof(XMFLOAT4X4);

		pD3D12Device->CreateCommittedResource(&HeapProperties, D3D12_HEAP_FLAG_NONE,
			&UploadResourceDesc, D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&m_IndexResource));


		// 将数据进行转移: CPU 高速缓存 -> CPU 共享内存

		BYTE* TransmitPointer = nullptr;	// 用于传递数据的指针

		// 映射资源,获取 D3D12 资源的地址,同时 D3D12 资源开放写权限
		m_VertexResource->Map(0, nullptr, reinterpret_cast<void**>(&TransmitPointer));
		// 用 memcpy 将数据复制到 D3D12 资源
		memcpy(TransmitPointer, VertexArray, 40 * sizeof(VERTEX));
		// 关闭映射,资源只写,这样静态资源读取效率会快很多,动态资源无需关闭映射
		m_VertexResource->Unmap(0, nullptr);


		m_ModelMatrixResource->Map(0, nullptr, reinterpret_cast<void**>(&TransmitPointer));
		memcpy(TransmitPointer, &_temp_ModelMatrixGroup[0], 40 * sizeof(XMFLOAT4X4));
		m_ModelMatrixResource->Unmap(0, nullptr);


		m_IndexResource->Map(0, nullptr, reinterpret_cast<void**>(&TransmitPointer));
		memcpy(TransmitPointer, IndexArray, 60 * sizeof(UINT));
		m_IndexResource->Unmap(0, nullptr);


		// 创建完所有资源,就可以填写 VBV 和 IBV 描述符了

		// VBV[0]: 描述顶点位置与纹理 UV 坐标,占据第 0 号输入槽
		VertexBufferView[0].BufferLocation = m_VertexResource->GetGPUVirtualAddress();		// D3D12 资源地址
		VertexBufferView[0].SizeInBytes = 40 * sizeof(VERTEX);								// D3D12 资源总大小
		VertexBufferView[0].StrideInBytes = sizeof(VERTEX);									// D3D12 资源单个元素的大小 (步长)

		// VBV[1]: 描述顶点的模型矩阵,占据第 1 号输入槽
		VertexBufferView[1].BufferLocation = m_ModelMatrixResource->GetGPUVirtualAddress();	// D3D12 资源地址
		VertexBufferView[1].SizeInBytes = 40 * sizeof(XMFLOAT4X4);							// D3D12 资源总大小
		VertexBufferView[1].StrideInBytes = sizeof(XMFLOAT4X4);								// D3D12 资源单个元素的大小 (步长)

		// IBV: 描述顶点的索引
		IndexBufferView.BufferLocation = m_IndexResource->GetGPUVirtualAddress();			// D3D12 资源地址
		IndexBufferView.SizeInBytes = 60 * sizeof(UINT);									// D3D12 资源总大小
		IndexBufferView.Format = DXGI_FORMAT_R32_UINT;										// D3D12 资源单个元素的大小 (步长)
	}
};


// 泥土 (实例类),继承自全遮挡固体方块
class Dirt : public SoildBlock
{
public:

	// 构造函数,调用 AppendTextureKey 添加需要的纹理
	Dirt()
	{
		this->AppendTextureKey("dirt");
	}

	// 绘制模型,函数声明的 override 在编译期能检查虚函数是否被重写,重写/实现函数常写 override 是一种好习惯
	virtual void DrawModel(ComPtr<ID3D12GraphicsCommandList>& pCommandList) override
	{
		// 设置 IBV 索引缓冲描述符
		pCommandList->IASetIndexBuffer(&IndexBufferView);
		// 设置 VBV 顶点缓冲描述符,注意我们这里使用了多槽输入!
		pCommandList->IASetVertexBuffers(0, 2, VertexBufferView);

		// 设置根参数,将纹理 SRV 描述符设置到 GPU 的寄存器上,这样着色器就可以找到纹理了
		pCommandList->SetGraphicsRootDescriptorTable(1, Texture_GPUHandle_Map["dirt"]);

		// Draw Call 渲染!
		pCommandList->DrawIndexedInstanced(36, 1, 0, 0, 0);
	}
};


// 橡木木板 (实例类),继承自全遮挡固体方块
class Planks_Oak : public SoildBlock
{
public:

	// 构造函数,调用 AppendTextureKey 添加需要的纹理
	Planks_Oak()
	{
		this->AppendTextureKey("planks_oak");
	}

	// 绘制模型,函数声明的 override 在编译期能检查虚函数是否被重写,重写/实现函数常写 override 是一种好习惯
	virtual void DrawModel(ComPtr<ID3D12GraphicsCommandList>& pCommandList) override
	{
		// 设置 IBV 索引缓冲描述符
		pCommandList->IASetIndexBuffer(&IndexBufferView);
		// 设置 VBV 顶点缓冲描述符,注意我们这里使用了多槽输入!
		pCommandList->IASetVertexBuffers(0, 2, VertexBufferView);

		// 设置根参数,将纹理 SRV 描述符设置到 GPU 的寄存器上,这样着色器就可以找到纹理了
		pCommandList->SetGraphicsRootDescriptorTable(1, Texture_GPUHandle_Map["planks_oak"]);

		// Draw Call 渲染!
		pCommandList->DrawIndexedInstanced(36, 1, 0, 0, 0);
	}
};


// 熔炉 (实例类),继承自全遮挡固体方块
class Furnace : public SoildBlock
{
public:

	// 构造函数,调用 AppendTextureKey 添加需要的纹理
	Furnace()
	{
		this->AppendTextureKey("furnace_front_off");
		this->AppendTextureKey("furnace_side");
		this->AppendTextureKey("furnace_top");
	}

	// 绘制模型,函数声明的 override 在编译期能检查虚函数是否被重写,重写/实现函数常写 override 是一种好习惯
	virtual void DrawModel(ComPtr<ID3D12GraphicsCommandList>& pCommandList) override
	{
		// 设置 IBV 索引缓冲描述符
		pCommandList->IASetIndexBuffer(&IndexBufferView);
		// 设置 VBV 顶点缓冲描述符,注意我们这里使用了多槽输入!
		pCommandList->IASetVertexBuffers(0, 2, VertexBufferView);

		// 设置根参数,将纹理 SRV 描述符设置到 GPU 的寄存器上,这样着色器就可以找到纹理了
		// 要更换纹理,可以通过 SetGraphicsRootDescriptorTable 改变根参数绑定的 GPUHandle

		// 渲染上下面
		pCommandList->SetGraphicsRootDescriptorTable(1, Texture_GPUHandle_Map["furnace_top"]);
		pCommandList->DrawIndexedInstanced(12, 1, 24, 0, 0);

		// 渲染左右背面
		pCommandList->SetGraphicsRootDescriptorTable(1, Texture_GPUHandle_Map["furnace_side"]);
		pCommandList->DrawIndexedInstanced(18, 1, 6, 0, 0);

		// 渲染正面
		pCommandList->SetGraphicsRootDescriptorTable(1, Texture_GPUHandle_Map["furnace_front_off"]);
		pCommandList->DrawIndexedInstanced(6, 1, 0, 0, 0);
	}
};


// 工作台 (实例类),继承自全遮挡固体方块
class Crafting_Table : public SoildBlock
{
public:

	// 构造函数,调用 AppendTextureKey 添加需要的纹理
	Crafting_Table()
	{
		this->AppendTextureKey("crafting_table_front");
		this->AppendTextureKey("crafting_table_side");
		this->AppendTextureKey("crafting_table_top");
	}

	// 绘制模型,函数声明的 override 在编译期能检查虚函数是否被重写,重写/实现函数常写 override 是一种好习惯
	virtual void DrawModel(ComPtr<ID3D12GraphicsCommandList>& pCommandList) override
	{
		// 设置 IBV 索引缓冲描述符
		pCommandList->IASetIndexBuffer(&IndexBufferView);
		// 设置 VBV 顶点缓冲描述符,注意我们这里使用了多槽输入!
		pCommandList->IASetVertexBuffers(0, 2, VertexBufferView);

		// 设置根参数,将纹理 SRV 描述符设置到 GPU 的寄存器上,这样着色器就可以找到纹理了
		// 要更换纹理,可以通过 SetGraphicsRootDescriptorTable 改变根参数绑定的 GPUHandle

		// 渲染上下面
		pCommandList->SetGraphicsRootDescriptorTable(1, Texture_GPUHandle_Map["crafting_table_top"]);
		pCommandList->DrawIndexedInstanced(12, 1, 24, 0, 0);

		// 渲染左右背面
		pCommandList->SetGraphicsRootDescriptorTable(1, Texture_GPUHandle_Map["crafting_table_side"]);
		pCommandList->DrawIndexedInstanced(18, 1, 6, 0, 0);

		// 渲染正面
		pCommandList->SetGraphicsRootDescriptorTable(1, Texture_GPUHandle_Map["crafting_table_front"]);
		pCommandList->DrawIndexedInstanced(6, 1, 0, 0, 0);
	}
};


// 橡树原木 (实例类),继承自全遮挡固体方块
class Log_Oak : public SoildBlock
{
public:

	// 构造函数,调用 AppendTextureKey 添加需要的纹理
	Log_Oak()
	{
		this->AppendTextureKey("log_oak");
		this->AppendTextureKey("log_oak_top");
	}

	// 绘制模型,函数声明的 override 在编译期能检查虚函数是否被重写,重写/实现函数常写 override 是一种好习惯
	virtual void DrawModel(ComPtr<ID3D12GraphicsCommandList>& pCommandList) override
	{
		// 设置 IBV 索引缓冲描述符
		pCommandList->IASetIndexBuffer(&IndexBufferView);
		// 设置 VBV 顶点缓冲描述符,注意我们这里使用了多槽输入!
		pCommandList->IASetVertexBuffers(0, 2, VertexBufferView);

		// 设置根参数,将纹理 SRV 描述符设置到 GPU 的寄存器上,这样着色器就可以找到纹理了
		// 要更换纹理,可以通过 SetGraphicsRootDescriptorTable 改变根参数绑定的 GPUHandle

		// 渲染上下面
		pCommandList->SetGraphicsRootDescriptorTable(1, Texture_GPUHandle_Map["log_oak_top"]);
		pCommandList->DrawIndexedInstanced(12, 1, 24, 0, 0);

		// 渲染左右正背面
		pCommandList->SetGraphicsRootDescriptorTable(1, Texture_GPUHandle_Map["log_oak"]);
		pCommandList->DrawIndexedInstanced(24, 1, 0, 0, 0);
	}
};


// 草方块 (实例类),继承自全遮挡固体方块
class Grass : public SoildBlock
{
public:

	// 构造函数,调用 AppendTextureKey 添加需要的纹理
	Grass()
	{
		this->AppendTextureKey("grass_side");
		this->AppendTextureKey("grass_top");
		this->AppendTextureKey("dirt");
	}

	// 绘制模型,函数声明的 override 在编译期能检查虚函数是否被重写,重写/实现函数常写 override 是一种好习惯
	virtual void DrawModel(ComPtr<ID3D12GraphicsCommandList>& pCommandList) override
	{
		// 设置 IBV 索引缓冲描述符
		pCommandList->IASetIndexBuffer(&IndexBufferView);
		// 设置 VBV 顶点缓冲描述符,注意我们这里使用了多槽输入!
		pCommandList->IASetVertexBuffers(0, 2, VertexBufferView);

		// 设置根参数,将纹理 SRV 描述符设置到 GPU 的寄存器上,这样着色器就可以找到纹理了
		// 要更换纹理,可以通过 SetGraphicsRootDescriptorTable 改变根参数绑定的 GPUHandle

		// 渲染上面
		pCommandList->SetGraphicsRootDescriptorTable(1, Texture_GPUHandle_Map["grass_top"]);
		pCommandList->DrawIndexedInstanced(6, 1, 24, 0, 0);

		// 渲染下面
		pCommandList->SetGraphicsRootDescriptorTable(1, Texture_GPUHandle_Map["dirt"]);
		pCommandList->DrawIndexedInstanced(6, 1, 30, 0, 0);

		// 渲染左右正背面
		pCommandList->SetGraphicsRootDescriptorTable(1, Texture_GPUHandle_Map["grass_side"]);
		pCommandList->DrawIndexedInstanced(24, 1, 0, 0, 0);
	}
};


// 橡木完整台阶 (实例类),继承自完整台阶方块
class Planks_Oak_SoildStair : public SoildStair
{
public:

	// 构造函数,调用 AppendTextureKey 添加需要的纹理
	Planks_Oak_SoildStair()
	{
		this->AppendTextureKey("planks_oak");
	}

	// 绘制模型,函数声明的 override 在编译期能检查虚函数是否被重写,重写/实现函数常写 override 是一种好习惯
	virtual void DrawModel(ComPtr<ID3D12GraphicsCommandList>& pCommandList) override
	{
		// 设置 IBV 索引缓冲描述符
		pCommandList->IASetIndexBuffer(&IndexBufferView);
		// 设置 VBV 顶点缓冲描述符,注意我们这里使用了多槽输入!
		pCommandList->IASetVertexBuffers(0, 2, VertexBufferView);

		// 渲染
		pCommandList->SetGraphicsRootDescriptorTable(1, Texture_GPUHandle_Map["planks_oak"]);
		pCommandList->DrawIndexedInstanced(60, 1, 0, 0, 0);
	}
};



// 模型管理器
class ModelManager
{
public:

	// 纹理映射表元素结构体
	struct TEXTURE_MAP_INFO
	{
		std::wstring TextureFilePath;			// 文件路径
		
		// 位于默认堆上纹理资源
		ComPtr<ID3D12Resource> DefaultHeapTextureResource;
		// 位于上传堆的纹理资源
		ComPtr<ID3D12Resource> UploadHeapTextureResource;

		D3D12_CPU_DESCRIPTOR_HANDLE CPUHandle;	// SRV 描述符堆的 CPU 句柄,用于创建纹理 SRV 描述符,将纹理与描述符绑定
		D3D12_GPU_DESCRIPTOR_HANDLE GPUHandle;	// SRV 描述符堆的 GPU 句柄,用于设置根参数,索引到对应纹理

		// 不用担心,创建 SRV 堆描述符堆,就会确定 CPU 句柄和 GPU 句柄的地址,后续只要描述符堆不销毁重新构造,句柄地址就是固定的
		// CPU 句柄和 GPU 句柄只有位置和用法不同的区别 (CPU 句柄在 CPU 端,GPU 句柄在 GPU 端)
		// 同一索引的 CPU 和 GPU 句柄能共享数据,所以只需创建 CPU 句柄,GPU 句柄无需创建
		// SetDescriptorHeap 会把描述符堆映射到 GPU 端的渲染管线,对应的 GPU 句柄就会有数据 (和同索引的 CPU 句柄一致)
	};

	// 纹理映射表
	std::unordered_map<std::string, TEXTURE_MAP_INFO> Texture_SRV_Map;


	// 模型组,存储 Model 类指针的 vector,注意存储的是指针,指针可以指向不同类的对象
	std::vector<Model*> ModelGroup;


public:

	// 构造函数,我们在构造函数上创建纹理映射表
	ModelManager()
	{
		Texture_SRV_Map["dirt"].TextureFilePath = L"resource/dirt.png";
		Texture_SRV_Map["grass_top"].TextureFilePath = L"resource/grass_top.png";
		Texture_SRV_Map["grass_side"].TextureFilePath = L"resource/grass_side.png";
		Texture_SRV_Map["log_oak"].TextureFilePath = L"resource/log_oak.png";
		Texture_SRV_Map["log_oak_top"].TextureFilePath = L"resource/log_oak_top.png";
		Texture_SRV_Map["furnace_front_off"].TextureFilePath = L"resource/furnace_front_off.png";
		Texture_SRV_Map["furnace_side"].TextureFilePath = L"resource/furnace_side.png";
		Texture_SRV_Map["furnace_top"].TextureFilePath = L"resource/furnace_top.png";
		Texture_SRV_Map["crafting_table_front"].TextureFilePath = L"resource/crafting_table_front.png";
		Texture_SRV_Map["crafting_table_side"].TextureFilePath = L"resource/crafting_table_side.png";
		Texture_SRV_Map["crafting_table_top"].TextureFilePath = L"resource/crafting_table_top.png";
		Texture_SRV_Map["planks_oak"].TextureFilePath = L"resource/planks_oak.png";
	}

	// 创建方块,我们在这里写上创建方块的代码
	void CreateBlock()
	{
		// 两层泥土地基,y 是高度
		for (int x = 0; x < 10; x++)
		{
			for (int z = -4; z < 10; z++)
			{
				for (int y = -2; y < 0; y++)
				{
					Model* dirt = new Dirt();							// 创建对象指针,调用时会根据虚函数表调用不同的函数
					dirt->SetModelMatrix(XMMatrixTranslation(x, y, z));	// 设置不同的模型矩阵,XMMatrixTranslation 平移模型
					ModelGroup.push_back(dirt);							// 将新模型添加到模型组
				}
			}
		}

		// 一层草方块地基
		for (int x = 0; x < 10; x++)
		{
			for (int z = -4; z < 10; z++)
			{
				Model* grass = new Grass();
				grass->SetModelMatrix(XMMatrixTranslation(x, 0, z));
				ModelGroup.push_back(grass);
			}
		}

		// 4x4 木板房基

		for (int x = 3; x < 7; x++)
		{
			for (int z = 3; z < 7; z++)
			{
				Model* plank = new Planks_Oak();
				plank->SetModelMatrix(XMMatrixTranslation(x, 2, z));
				ModelGroup.push_back(plank);
			}
		}


		// 8 柱原木 

		for (int y = 1; y < 7; y++)
		{
			Model* log_oak = new Log_Oak();
			log_oak->SetModelMatrix(XMMatrixTranslation(3, y, 2));
			ModelGroup.push_back(log_oak);
		}

		for (int y = 1; y < 7; y++)
		{
			Model* log_oak = new Log_Oak();
			log_oak->SetModelMatrix(XMMatrixTranslation(2, y, 3));
			ModelGroup.push_back(log_oak);
		}

		for (int y = 1; y < 7; y++)
		{
			Model* log_oak = new Log_Oak();
			log_oak->SetModelMatrix(XMMatrixTranslation(6, y, 2));
			ModelGroup.push_back(log_oak);
		}

		for (int y = 1; y < 7; y++)
		{
			Model* log_oak = new Log_Oak();
			log_oak->SetModelMatrix(XMMatrixTranslation(7, y, 3));
			ModelGroup.push_back(log_oak);
		}

		for (int y = 1; y < 7; y++)
		{
			Model* log_oak = new Log_Oak();
			log_oak->SetModelMatrix(XMMatrixTranslation(7, y, 6));
			ModelGroup.push_back(log_oak);
		}

		for (int y = 1; y < 7; y++)
		{
			Model* log_oak = new Log_Oak();
			log_oak->SetModelMatrix(XMMatrixTranslation(6, y, 7));
			ModelGroup.push_back(log_oak);
		}

		for (int y = 1; y < 7; y++)
		{
			Model* log_oak = new Log_Oak();
			log_oak->SetModelMatrix(XMMatrixTranslation(2, y, 6));
			ModelGroup.push_back(log_oak);
		}

		for (int y = 1; y < 7; y++)
		{
			Model* log_oak = new Log_Oak();
			log_oak->SetModelMatrix(XMMatrixTranslation(3, y, 7));
			ModelGroup.push_back(log_oak);
		}


		// 其他木板与门前台阶
		{
			Model* plank = new Planks_Oak();
			plank->SetModelMatrix(XMMatrixTranslation(4, 2, 2));
			ModelGroup.push_back(plank);

			plank = new Planks_Oak();
			plank->SetModelMatrix(XMMatrixTranslation(5, 2, 2));
			ModelGroup.push_back(plank);

			for (int y = 5; y < 7; y++)
			{
				for (int x = 4; x < 6; x++)
				{
					plank = new Planks_Oak();
					plank->SetModelMatrix(XMMatrixTranslation(x, y, 2));
					ModelGroup.push_back(plank);
				}
			}

			for (int y = 2; y < 4; y++)
			{
				for (int z = 4; z < 6; z++)
				{
					plank = new Planks_Oak();
					plank->SetModelMatrix(XMMatrixTranslation(2, y, z));
					ModelGroup.push_back(plank);
				}
			}

			for (int y = 2; y < 4; y++)
			{
				for (int x = 4; x < 6; x++)
				{
					plank = new Planks_Oak();
					plank->SetModelMatrix(XMMatrixTranslation(x, y, 7));
					ModelGroup.push_back(plank);
				}
			}

			for (int y = 2; y < 4; y++)
			{
				for (int z = 4; z < 6; z++)
				{
					plank = new Planks_Oak();
					plank->SetModelMatrix(XMMatrixTranslation(7, y, z));
					ModelGroup.push_back(plank);
				}
			}

			plank = new Planks_Oak();
			plank->SetModelMatrix(XMMatrixTranslation(2, 6, 4));
			ModelGroup.push_back(plank);

			plank = new Planks_Oak();
			plank->SetModelMatrix(XMMatrixTranslation(2, 6, 5));
			ModelGroup.push_back(plank);

			plank = new Planks_Oak();
			plank->SetModelMatrix(XMMatrixTranslation(4, 6, 7));
			ModelGroup.push_back(plank);

			plank = new Planks_Oak();
			plank->SetModelMatrix(XMMatrixTranslation(5, 6, 7));
			ModelGroup.push_back(plank);

			plank = new Planks_Oak();
			plank->SetModelMatrix(XMMatrixTranslation(7, 6, 4));
			ModelGroup.push_back(plank);

			plank = new Planks_Oak();
			plank->SetModelMatrix(XMMatrixTranslation(7, 6, 5));
			ModelGroup.push_back(plank);

			Model* stair = new Planks_Oak_SoildStair();
			stair->SetModelMatrix(XMMatrixTranslation(4, 2, 1));
			ModelGroup.push_back(stair);

			stair = new Planks_Oak_SoildStair();
			stair->SetModelMatrix(XMMatrixTranslation(5, 2, 1));
			ModelGroup.push_back(stair);

			stair = new Planks_Oak_SoildStair();
			stair->SetModelMatrix(XMMatrixTranslation(4, 1, 0));
			ModelGroup.push_back(stair);

			stair = new Planks_Oak_SoildStair();
			stair->SetModelMatrix(XMMatrixTranslation(5, 1, 0));
			ModelGroup.push_back(stair);
		}

		// 4x4 木板房顶

		for (int x = 3; x < 7; x++)
		{
			for (int z = 3; z < 7; z++)
			{
				Model* plank = new Planks_Oak();
				plank->SetModelMatrix(XMMatrixTranslation(x, 6, z));
				ModelGroup.push_back(plank);
			}
		}

		// 屋顶

		{
			// 第一层

			for (int x = 3; x < 7; x++)
			{
				Model* stair = new Planks_Oak_SoildStair();
				stair->SetModelMatrix(XMMatrixTranslation(x, 6, 1));
				ModelGroup.push_back(stair);
			}

			for (int x = 3; x < 7; x++)
			{
				// 旋转橡木台阶用的模型矩阵
				// 这里本来是可以不用 XMMatrixTranslation(-0.5, -0.5, -0.5) 平移到模型中心的
				// 因为作者本人 (我) 的设计失误,把模型坐标系原点建立在模型左下角了 (见上文的 VertexArray)
				// 导致还要先把原点平移到模型中心,旋转完再还原,增大计算量,这个是完全可以规避的
				// 读者可以自行修改 VertexArray,使方块以自身中心为原点建系,这样就可以直接 XMMatrixRotationY() 进行旋转了
				XMMATRIX transform = XMMatrixTranslation(-0.5, -0.5, -0.5);
				transform *= XMMatrixRotationY(XM_PI);						// 平移中心后,再旋转,否则会出错 (旋转角度是弧度)
				transform *= XMMatrixTranslation(0.5, 0.5, 0.5);			// 旋转完再还原
				transform *= XMMatrixTranslation(x, 6, 8);					// 再平移到对应的坐标
				Model* stair = new Planks_Oak_SoildStair();
				stair->SetModelMatrix(transform);
				ModelGroup.push_back(stair);
			}

			for (int z = 3; z < 7; z++)
			{
				XMMATRIX transform = XMMatrixTranslation(-0.5, -0.5, -0.5);
				transform *= XMMatrixRotationY(XM_PIDIV2);					// 旋转 90°
				transform *= XMMatrixTranslation(0.5, 0.5, 0.5);
				transform *= XMMatrixTranslation(1, 6, z);
				Model* stair = new Planks_Oak_SoildStair();
				stair->SetModelMatrix(transform);
				ModelGroup.push_back(stair);
			}

			for (int z = 3; z < 7; z++)
			{
				XMMATRIX transform = XMMatrixTranslation(-0.5, -0.5, -0.5);
				transform *= XMMatrixRotationY(XM_PI + XM_PIDIV2);			// 旋转 270°
				transform *= XMMatrixTranslation(0.5, 0.5, 0.5);
				transform *= XMMatrixTranslation(8, 6, z);
				Model* stair = new Planks_Oak_SoildStair();
				stair->SetModelMatrix(transform);
				ModelGroup.push_back(stair);
			}

			// 第二层

			for (int x = 3; x < 7; x++)
			{
				Model* stair = new Planks_Oak_SoildStair();
				stair->SetModelMatrix(XMMatrixTranslation(x, 7, 2));
				ModelGroup.push_back(stair);
			}

			for (int x = 3; x < 7; x++)
			{
				XMMATRIX transform = XMMatrixTranslation(-0.5, -0.5, -0.5);
				transform *= XMMatrixRotationY(XM_PI);
				transform *= XMMatrixTranslation(0.5, 0.5, 0.5);
				transform *= XMMatrixTranslation(x, 7, 7);
				Model* stair = new Planks_Oak_SoildStair();
				stair->SetModelMatrix(transform);
				ModelGroup.push_back(stair);
			}

			for (int z = 3; z < 7; z++)
			{
				XMMATRIX transform = XMMatrixTranslation(-0.5, -0.5, -0.5);
				transform *= XMMatrixRotationY(XM_PIDIV2);
				transform *= XMMatrixTranslation(0.5, 0.5, 0.5);
				transform *= XMMatrixTranslation(2, 7, z);
				Model* stair = new Planks_Oak_SoildStair();
				stair->SetModelMatrix(transform);
				ModelGroup.push_back(stair);
			}

			for (int z = 3; z < 7; z++)
			{
				XMMATRIX transform = XMMatrixTranslation(-0.5, -0.5, -0.5);
				transform *= XMMatrixRotationY(XM_PI + XM_PIDIV2);
				transform *= XMMatrixTranslation(0.5, 0.5, 0.5);
				transform *= XMMatrixTranslation(7, 7, z);
				Model* stair = new Planks_Oak_SoildStair();
				stair->SetModelMatrix(transform);
				ModelGroup.push_back(stair);
			}

			// 补上屋顶空位

			for (int x = 3; x < 7; x++)
			{
				for (int z = 3; z < 7; z++)
				{
					Model* plank = new Planks_Oak();
					plank->SetModelMatrix(XMMatrixTranslation(x, 7, z));
					ModelGroup.push_back(plank);
				}
			}
		}

		// 工作台和熔炉
		{
			Model* craft_table = new Crafting_Table();
			craft_table->SetModelMatrix(XMMatrixTranslation(3, 3, 6));
			ModelGroup.push_back(craft_table);

			Model* furnace = new Furnace();
			furnace->SetModelMatrix(XMMatrixTranslation(4, 3, 6));
			ModelGroup.push_back(furnace);

			furnace = new Furnace();
			furnace->SetModelMatrix(XMMatrixTranslation(5, 3, 6));
			ModelGroup.push_back(furnace);
		}

	}

	// 当一切准备就绪后,就可以正式创建模型资源,准备渲染了
	// 调用该函数的前提是: 依次完成 DX12Engine::CreateModelTextureResource (读取并创建纹理资源),CreateBlock (创建方块,设置模型矩阵)
	void CreateModelResource(ComPtr<ID3D12Device4>& pD3D12Device)
	{
		// 遍历模型组
		for (auto& model : ModelGroup)
		{
			// 创建模型资源
			model->CreateResourceAndDescriptor(pD3D12Device);
			// 遍历模型自身的映射表,设置模型需要用到的纹理
			for (const auto& texture : model->RequestForTextureMap())
			{
				// 设置模型的 SRV 描述符
				model->SetTextureGPUHandle(texture.first, Texture_SRV_Map[texture.first].GPUHandle);
			}
		}
	}

	// 渲染全部模型!
	// 调用该函数的前提是: 完成上面的 CreateModelResource
	void RenderAllModel(ComPtr<ID3D12GraphicsCommandList>& pCommandList)
	{
		// 遍历模型组
		for (const auto& model : ModelGroup)
		{
			model->DrawModel(pCommandList);
		}
	}

};



// DX12 引擎
class DX12Engine
{
private:

	int WindowWidth = 640;		// 窗口宽度
	int WindowHeight = 480;		// 窗口高度
	HWND m_hwnd;				// 窗口句柄

	ComPtr<ID3D12Debug> m_D3D12DebugDevice;					// D3D12 调试层设备
	UINT m_DXGICreateFactoryFlag = NULL;					// 创建 DXGI 工厂时需要用到的标志

	ComPtr<IDXGIFactory5> m_DXGIFactory;					// DXGI 工厂
	ComPtr<IDXGIAdapter1> m_DXGIAdapter;					// 显示适配器 (显卡)
	ComPtr<ID3D12Device4> m_D3D12Device;					// D3D12 核心设备

	ComPtr<ID3D12CommandQueue> m_CommandQueue;				// 命令队列
	ComPtr<ID3D12CommandAllocator> m_CommandAllocator;		// 命令分配器
	ComPtr<ID3D12GraphicsCommandList> m_CommandList;		// 命令列表

	ComPtr<IDXGISwapChain3> m_DXGISwapChain;				// DXGI 交换链
	ComPtr<ID3D12DescriptorHeap> m_RTVHeap;					// RTV 描述符堆
	ComPtr<ID3D12Resource> m_RenderTarget[3];				// 渲染目标缓冲区数组,每一副渲染缓冲对应一个窗口缓冲区
	D3D12_CPU_DESCRIPTOR_HANDLE RTVHandle;					// RTV 描述符句柄
	UINT RTVDescriptorSize = 0;								// RTV 描述符的大小
	UINT FrameIndex = 0;									// 帧索引,表示当前渲染的第 i 帧 (第 i 个渲染目标)

	ComPtr<ID3D12Fence> m_Fence;							// 围栏
	UINT64 FenceValue = 0;									// 用于围栏等待的围栏值
	HANDLE RenderEvent = NULL;								// GPU 渲染事件
	D3D12_RESOURCE_BARRIER beg_barrier = {};				// 渲染开始的资源屏障,呈现 -> 渲染目标
	D3D12_RESOURCE_BARRIER end_barrier = {};				// 渲染结束的资源屏障,渲染目标 -> 呈现

	ComPtr<ID3D12DescriptorHeap> m_DSVHeap;					// DSV 描述符堆
	D3D12_CPU_DESCRIPTOR_HANDLE DSVHandle;					// DSV 描述符句柄
	ComPtr<ID3D12Resource> m_DepthStencilBuffer;			// DSV 深度模板缓冲资源

	// DSV 资源的格式
	// 深度模板缓冲只支持四种格式:
	// DXGI_FORMAT_D24_UNORM_S8_UINT	(每个像素占用四个字节 32 位,24 位无符号归一化浮点数留作深度值,8 位整数留作模板值)
	// DXGI_FORMAT_D32_FLOAT_S8X24_UINT	(每个像素占用八个字节 64 位,32 位浮点数留作深度值,8 位整数留作模板值,其余 24 位保留不使用)
	// DXGI_FORMAT_D16_UNORM			(每个像素占用两个字节 16 位,16 位无符号归一化浮点数留作深度值,范围 [0,1],不使用模板)
	// DXGI_FORMAT_D32_FLOAT			(每个像素占用四个字节 32 位,32 位浮点数留作深度值,不使用模板)
	// 这里我们选择最常用的格式 DXGI_FORMAT_D24_UNORM_S8_UINT
	DXGI_FORMAT DSVFormat = DXGI_FORMAT_D24_UNORM_S8_UINT;	


	ModelManager m_ModelManager;							// 模型管理器,帮助管理并渲染模型

	ComPtr<IWICImagingFactory> m_WICFactory;				// WIC 工厂
	ComPtr<IWICBitmapDecoder> m_WICBitmapDecoder;			// 位图解码器
	ComPtr<IWICBitmapFrameDecode> m_WICBitmapDecodeFrame;	// 由解码器得到的单个位图帧
	ComPtr<IWICFormatConverter> m_WICFormatConverter;		// 位图转换器
	ComPtr<IWICBitmapSource> m_WICBitmapSource;				// WIC 位图资源,用于获取位图数据
	UINT TextureWidth = 0;									// 纹理宽度
	UINT TextureHeight = 0;									// 纹理高度
	UINT BitsPerPixel = 0;									// 图像深度,图片每个像素占用的比特数
	UINT BytePerRowSize = 0;								// 纹理每行数据的真实字节大小,用于读取纹理数据、上传纹理资源
	DXGI_FORMAT TextureFormat = DXGI_FORMAT_UNKNOWN;		// 纹理格式

	ComPtr<ID3D12DescriptorHeap> m_SRVHeap;					// SRV 描述符堆

	UINT TextureSize = 0;									// 纹理的真实大小 (单位:字节)
	UINT UploadResourceRowSize = 0;							// 上传堆资源每行的大小 (单位:字节)
	UINT UploadResourceSize = 0;							// 上传堆资源的总大小 (单位:字节)



	ComPtr<ID3D12Resource> m_CBVResource;		// 常量缓冲资源,用于存放 MVP 矩阵,MVP 矩阵每帧都要更新,所以需要存储在常量缓冲区中
	struct CBuffer								// 常量缓冲结构体
	{
		XMFLOAT4X4 MVPMatrix;		// MVP 矩阵,用于将顶点数据从顶点空间变换到齐次裁剪空间
	};
	CBuffer* MVPBuffer = nullptr;	// 常量缓冲结构体指针,里面存储的是 MVP 矩阵信息,下文 Map 后指针会指向 CBVResource 的地址

	Camera m_FirstCamera;			// 第一人称摄像机

	ComPtr<ID3D12RootSignature> m_RootSignature;			// 根签名
	ComPtr<ID3D12PipelineState> m_PipelineStateObject;		// 渲染管线状态


	// 视口
	D3D12_VIEWPORT viewPort = D3D12_VIEWPORT{ 0, 0, float(WindowWidth), float(WindowHeight), D3D12_MIN_DEPTH, D3D12_MAX_DEPTH };
	// 裁剪矩形
	D3D12_RECT ScissorRect = D3D12_RECT{ 0, 0, WindowWidth, WindowHeight };

public:

	// 初始化窗口
	void InitWindow(HINSTANCE hins)
	{
		WNDCLASS wc = {};					// 用于记录窗口类信息的结构体
		wc.hInstance = hins;				// 窗口类需要一个应用程序的实例句柄 hinstance

		// 绑定回调函数,利用 std::bind,将 DX12Engine::CallBackFunc 绑定到 CallBackWrapper 的函数包装器上
		// std::bind 的作用是将 CallBackFunc 带参数绑定到一个对象上,并生成对应的函数包装器
		// 第一个参数表示对象的成员函数的指针,因为编译器不会将对象的成员函数隐式转换成函数指针,所以必须在前面添加 &
		// 第二个参数表示对象的地址,这个参数就表示类成员函数需要传递的 this 指针
		// 之后的表示将要绑定的参数,std::placeholders 表示占位符,用户调用函数,传递实参时,这个占位符会将实参一一对应
		CallBackWrapper::Broker_Func = std::bind(&DX12Engine::CallBackFunc, this,
			std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4);

		wc.lpfnWndProc = CallBackWrapper::CallBackFunc;		// 窗口类需要一个回调函数,用于处理窗口产生的消息,注意这里传递的是中间层的回调函数
		wc.lpszClassName = L"DX12 Game";					// 窗口类的名称

		RegisterClass(&wc);					// 注册窗口类,将窗口类录入到操作系统中

		// 使用上文的窗口类创建窗口
		m_hwnd = CreateWindow(wc.lpszClassName, L"Minecraft", WS_SYSMENU | WS_OVERLAPPED,
			10, 10, WindowWidth, WindowHeight,
			NULL, NULL, hins, NULL);

		// 因为指定了窗口大小不可变的 WS_SYSMENU 和 WS_OVERLAPPED,应用不会自动显示窗口,需要使用 ShowWindow 强制显示窗口
		ShowWindow(m_hwnd, SW_SHOW);
	}

	// 创建调试层
	void CreateDebugDevice()
	{
		::CoInitialize(nullptr);	// 注意这里!DX12 的所有设备接口都是基于 COM 接口的,我们需要先全部初始化为 nullptr

#if defined(_DEBUG)		// 如果是 Debug 模式下编译,就执行下面的代码

		// 获取调试层设备接口
		D3D12GetDebugInterface(IID_PPV_ARGS(&m_D3D12DebugDevice));
		// 开启调试层
		m_D3D12DebugDevice->EnableDebugLayer();
		// 开启调试层后,创建 DXGI 工厂也需要 Debug Flag
		m_DXGICreateFactoryFlag = DXGI_CREATE_FACTORY_DEBUG;
		
#endif
	}


	// 创建设备
	bool CreateDevice()
	{
		// 创建 DXGI 工厂
		CreateDXGIFactory2(m_DXGICreateFactoryFlag, IID_PPV_ARGS(&m_DXGIFactory));

		// DX12 支持的所有功能版本,你的显卡最低需要支持 11.0
		const D3D_FEATURE_LEVEL dx12SupportLevel[] =
		{
			D3D_FEATURE_LEVEL_12_2,		// 12.2
			D3D_FEATURE_LEVEL_12_1,		// 12.1
			D3D_FEATURE_LEVEL_12_0,		// 12.0
			D3D_FEATURE_LEVEL_11_1,		// 11.1
			D3D_FEATURE_LEVEL_11_0		// 11.0
		};


		// 用 EnumAdapters1 先遍历电脑上的每一块显卡
		// 每次调用 EnumAdapters1 找到显卡会自动创建 DXGIAdapter 接口,并返回 S_OK
		// 找不到显卡会返回 ERROR_NOT_FOUND

		for (UINT i = 0; m_DXGIFactory->EnumAdapters1(i, &m_DXGIAdapter) != ERROR_NOT_FOUND; i++)
		{
			// 找到显卡,就创建 D3D12 设备,从高到低遍历所有功能版本,创建成功就跳出
			for (const auto& level : dx12SupportLevel)
			{
				// 创建 D3D12 核心层设备,创建成功就返回 true
				if (SUCCEEDED(D3D12CreateDevice(m_DXGIAdapter.Get(), level, IID_PPV_ARGS(&m_D3D12Device))))
				{
					DXGI_ADAPTER_DESC1 adap = {};
					m_DXGIAdapter->GetDesc1(&adap);
					OutputDebugStringW(adap.Description);		// 在输出窗口上输出创建 D3D12 设备所用的显卡名称
					return true;
				}
			}
		}

		// 如果找不到任何能支持 DX12 的显卡,就退出程序
		if (m_D3D12Device == nullptr)
		{
			MessageBox(NULL, L"找不到任何能支持 DX12 的显卡,请升级电脑上的硬件!", L"错误", MB_OK | MB_ICONERROR);
			return false;
		}
	}

	// 创建命令三件套
	void CreateCommandComponents()
	{
		// 队列信息结构体,这里只需要填队列的类型 type 就行了
		D3D12_COMMAND_QUEUE_DESC queueDesc = {};
		// D3D12_COMMAND_LIST_TYPE_DIRECT 表示将命令都直接放进队列里,不做其他处理
		queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
		// 创建命令队列
		m_D3D12Device->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&m_CommandQueue));

		// 创建命令分配器,它的作用是开辟内存,存储命令列表上的命令,注意命令类型要一致
		m_D3D12Device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&m_CommandAllocator));

		// 创建图形命令列表,注意命令类型要一致
		m_D3D12Device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, m_CommandAllocator.Get(),
			nullptr, IID_PPV_ARGS(&m_CommandList));

		// 命令列表创建时处于 Record 录制状态,我们需要关闭它,这样下文的 Reset 才能成功
		m_CommandList->Close();
	}

	// 创建 RTV 渲染目标描述符堆 (Non-Shader Visible),创建渲染目标,将渲染目标设置为窗口
	void CreateRenderTarget()
	{
		// 创建 RTV 描述符堆 (Render Target View,渲染目标描述符)
		D3D12_DESCRIPTOR_HEAP_DESC RTVHeapDesc = {};
		RTVHeapDesc.NumDescriptors = 3;							// 渲染目标的数量
		RTVHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;		// 描述符堆的类型:RTV
		// 创建一个 RTV 描述符堆,创建成功后,会自动开辟三个描述符的内存
		m_D3D12Device->CreateDescriptorHeap(&RTVHeapDesc, IID_PPV_ARGS(&m_RTVHeap));


		// 创建 DXGI 交换链,用于将窗口缓冲区和渲染目标绑定
		DXGI_SWAP_CHAIN_DESC1 swapchainDesc = {};
		swapchainDesc.BufferCount = 3;								// 缓冲区数量
		swapchainDesc.Width = WindowWidth;							// 缓冲区 (窗口) 宽度
		swapchainDesc.Height = WindowHeight;						// 缓冲区 (窗口) 高度
		swapchainDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;			// 缓冲区格式,指定缓冲区每个像素的大小
		swapchainDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;	// 交换链类型,有 FILP 和 BITBLT 两种类型
		swapchainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;// 缓冲区的用途,这里表示把缓冲区用作渲染目标的输出
		swapchainDesc.SampleDesc.Count = 1;							// 缓冲区像素采样次数

		// 临时低版本交换链接口,用于创建高版本交换链,因为下文的 CreateSwapChainForHwnd 不能直接用于创建高版本接口
		ComPtr<IDXGISwapChain1> _temp_swapchain;

		// 创建交换链,将窗口与渲染目标绑定
		// 注意:交换链需要绑定到命令队列来刷新,所以第一个参数要填命令队列,不填会创建失败!
		m_DXGIFactory->CreateSwapChainForHwnd(m_CommandQueue.Get(), m_hwnd,
			&swapchainDesc, nullptr, nullptr, &_temp_swapchain);

		// 通过 As 方法,将低版本接口的信息传递给高版本接口
		_temp_swapchain.As(&m_DXGISwapChain);


		// 创建完交换链后,我们还需要令 RTV 描述符 指向 渲染目标
		// 因为 ID3D12Resource 本质上只是一块数据,它本身没有对数据用法的说明
		// 我们要让程序知道这块数据是一个渲染目标,就得创建并使用 RTV 描述符

		// 获取 RTV 堆指向首描述符的句柄
		RTVHandle = m_RTVHeap->GetCPUDescriptorHandleForHeapStart();
		// 获取 RTV 描述符的大小
		RTVDescriptorSize = m_D3D12Device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);

		for (UINT i = 0; i < 3; i++)
		{
			// 从交换链中获取第 i 个窗口缓冲,创建第 i 个 RenderTarget 渲染目标
			m_DXGISwapChain->GetBuffer(i, IID_PPV_ARGS(&m_RenderTarget[i]));

			// 创建 RTV 描述符,将渲染目标绑定到描述符上
			m_D3D12Device->CreateRenderTargetView(m_RenderTarget[i].Get(), nullptr, RTVHandle);

			// 偏移到下一个 RTV 句柄
			RTVHandle.ptr += RTVDescriptorSize;
		}
	}

	// 创建围栏和资源屏障,用于 CPU-GPU 的同步
	void CreateFenceAndBarrier()
	{
		// 创建 CPU 上的等待事件
		RenderEvent = CreateEvent(nullptr, false, true, nullptr);

		// 创建围栏,设定初始值为 0
		m_D3D12Device->CreateFence(FenceValue, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&m_Fence));


		// 设置资源屏障
		// beg_barrier 起始屏障:Present 呈现状态 -> Render Target 渲染目标状态
		beg_barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;					// 指定类型为转换屏障		
		beg_barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_PRESENT;
		beg_barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_RENDER_TARGET;

		// end_barrier 终止屏障:Render Target 渲染目标状态 -> Present 呈现状态
		end_barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
		end_barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_RENDER_TARGET;
		end_barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_PRESENT;
	}

	// 创建 DSV 深度模板描述符堆 (Non-Shader Visible)
	void CreateDSVHeap()
	{
		D3D12_DESCRIPTOR_HEAP_DESC DSVHeapDesc = {};		// DSV 描述符堆结构体
		DSVHeapDesc.NumDescriptors = 1;						// 描述符只有 1 个,因为我们只有一个渲染目标
		DSVHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_DSV;	// 描述符堆类型
		
		// 创建 DSV 描述符堆 (Depth Stencil View,深度模板描述符),用于深度测试与模板测试
		m_D3D12Device->CreateDescriptorHeap(&DSVHeapDesc, IID_PPV_ARGS(&m_DSVHeap));

		// 获取 DSV 的 CPU 句柄
		DSVHandle = m_DSVHeap->GetCPUDescriptorHandleForHeapStart();
	}

	// 创建深度与模板缓冲,用于开启深度测试,渲染物体正确的深度与遮挡关系
	void CreateDepthStencilBuffer()
	{
		D3D12_RESOURCE_DESC DSVResourceDesc = {};							// 深度模板缓冲资源信息结构体
		DSVResourceDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;		// 深度缓冲其实也是一块纹理
		DSVResourceDesc.Format = DSVFormat;									// 资源纹理格式
		DSVResourceDesc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN;				// 深度缓冲的布局也是 UNKNOWN
		DSVResourceDesc.Width = WindowWidth;								// 宽度和渲染目标一致
		DSVResourceDesc.Height = WindowHeight;								// 高度和渲染目标一致
		DSVResourceDesc.MipLevels = 1;										// Mipmap 层级,设置为 1 就行
		DSVResourceDesc.DepthOrArraySize = 1;								// 纹理数组大小 (3D 纹理深度),设置为 1 就行
		DSVResourceDesc.SampleDesc.Count = 1;								// 采样次数,设置为 1 就行
		DSVResourceDesc.SampleDesc.Quality = 0;								// 采样质量,设置为 0 就行
		// 资源标志,创建深度模板缓冲,必须要填 D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL,否则会创建失败
		DSVResourceDesc.Flags = D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL;	

		D3D12_CLEAR_VALUE DepthStencilBufferClearValue = {};				// 用于清空深度缓冲的信息结构体,DX12 能对这个进行优化
		DepthStencilBufferClearValue.DepthStencil.Depth = 1.0f;				// 要清空到的深度值,清空后会重置到该值
		DepthStencilBufferClearValue.DepthStencil.Stencil = 0;				// 要清空到的模板值,清空后会重置到该值
		DepthStencilBufferClearValue.Format = DSVFormat;					// 要清空缓冲的格式,要和上文一致

		// 默认堆属性,深度缓冲也是一块纹理,所以用默认堆
		D3D12_HEAP_PROPERTIES DefaultProperties = { D3D12_HEAP_TYPE_DEFAULT };

		// 创建资源,深度缓冲只会占用很少资源,所以直接 CreateCommittedResource 隐式堆创建即可,让操作系统帮我们管理
		m_D3D12Device->CreateCommittedResource(&DefaultProperties, D3D12_HEAP_FLAG_NONE, &DSVResourceDesc,
			D3D12_RESOURCE_STATE_DEPTH_WRITE, &DepthStencilBufferClearValue, IID_PPV_ARGS(&m_DepthStencilBuffer));
	}

	// 创建 DSV 描述符,DSV 描述符用于描述深度模板缓冲区,这个描述符才是渲染管线要设置的对象
	void CreateDSV()
	{
		D3D12_DEPTH_STENCIL_VIEW_DESC DSVViewDesc = {};
		DSVViewDesc.Format = DSVFormat;
		DSVViewDesc.ViewDimension = D3D12_DSV_DIMENSION_TEXTURE2D;
		DSVViewDesc.Flags = D3D12_DSV_FLAG_NONE;

		// 创建 DSV 描述符 (Depth Stencil View,深度模板描述符)
		m_D3D12Device->CreateDepthStencilView(m_DepthStencilBuffer.Get(), &DSVViewDesc, DSVHandle);
	}


	// 创建 SRV Descriptor Heap 着色器资源描述符堆 (Shader Visible)
	void CreateSRVHeap()
	{
		// 创建 SRV 描述符堆 (Shader Resource View,着色器资源描述符)
		D3D12_DESCRIPTOR_HEAP_DESC SRVHeapDesc = {};
		SRVHeapDesc.NumDescriptors = m_ModelManager.Texture_SRV_Map.size();	// 描述符堆的容量
		SRVHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;			// 描述符堆类型,CBV、SRV、UAV 这三种描述符可以放在同一种描述符堆上
		SRVHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;		// 描述符堆标志,Shader-Visible 表示对着色器可见

		// 创建 SRV 描述符堆
		m_D3D12Device->CreateDescriptorHeap(&SRVHeapDesc, IID_PPV_ARGS(&m_SRVHeap));
	}

	// 启动命令列表,准备录制复制命令
	void StartCommandRecord()
	{
		// 复制资源需要使用 GPU 的 CopyEngine 复制引擎,所以需要向命令队列发出复制命令

		m_CommandAllocator->Reset();								// 先重置命令分配器
		m_CommandList->Reset(m_CommandAllocator.Get(), nullptr);	// 再重置命令列表,复制命令不需要 PSO 状态,所以第二个参数填 nullptr
	}

	// 加载纹理到内存中
	bool LoadTextureFromFile(std::wstring TextureFilename)
	{
		// 如果还没创建 WIC 工厂,就新建一个 WIC 工厂实例。注意!WIC 工厂不可以重复释放与创建!
		if (m_WICFactory == nullptr) CoCreateInstance(CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&m_WICFactory));

		// 创建图片解码器,并将图片读入到解码器中
		HRESULT hr = m_WICFactory->CreateDecoderFromFilename(TextureFilename.c_str(), nullptr, GENERIC_READ, WICDecodeMetadataCacheOnDemand, &m_WICBitmapDecoder);

		std::wostringstream output_str;		// 用于格式化字符串
		switch (hr)
		{
		case S_OK: break;	// 解码成功,直接 break 进入下一步即可

		case HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND):	// 文件找不到
			output_str << L"找不到文件 " << TextureFilename << L" !请检查文件路径是否有误!";
			MessageBox(NULL, output_str.str().c_str(), L"错误", MB_OK | MB_ICONERROR);
			return false;

		case HRESULT_FROM_WIN32(ERROR_FILE_CORRUPT):	// 文件句柄正在被另一个应用进程占用
			output_str << L"文件 " << TextureFilename << L" 已经被另一个应用进程打开并占用了!请先关闭那个应用进程!";
			MessageBox(NULL, output_str.str().c_str(), L"错误", MB_OK | MB_ICONERROR);
			return false;

		case WINCODEC_ERR_COMPONENTNOTFOUND:			// 找不到可解码的组件,说明这不是有效的图像文件
			output_str << L"文件 " << TextureFilename << L" 不是有效的图像文件,无法解码!请检查文件是否为图像文件!";
			MessageBox(NULL, output_str.str().c_str(), L"错误", MB_OK | MB_ICONERROR);
			return false;

		default:			// 发生其他未知错误
			output_str << L"文件 " << TextureFilename << L" 解码失败!发生了其他错误,错误码:" << hr << L" ,请查阅微软官方文档。";
			MessageBox(NULL, output_str.str().c_str(), L"错误", MB_OK | MB_ICONERROR);
			return false;
		}

		// 获取图片数据的第一帧,这个 GetFrame 可以用于 gif 这种多帧动图
		m_WICBitmapDecoder->GetFrame(0, &m_WICBitmapDecodeFrame);


		// 获取图片格式,并将它转化为 DX12 能接受的纹理格式
		// 如果碰到格式无法支持的错误,可以用微软提供的 画图3D 来转换,强力推荐!
		WICPixelFormatGUID SourceFormat = {};				// 源图格式
		GUID TargetFormat = {};								// 目标格式

		m_WICBitmapDecodeFrame->GetPixelFormat(&SourceFormat);						// 获取源图格式

		if (DX12TextureHelper::GetTargetPixelFormat(&SourceFormat, &TargetFormat))	// 获取目标格式
		{
			TextureFormat = DX12TextureHelper::GetDXGIFormatFromPixelFormat(&TargetFormat);	// 获取 DX12 支持的格式
		}
		else	// 如果没有可支持的目标格式
		{
			::MessageBox(NULL, L"此纹理不受支持!", L"提示", MB_OK);
			return false;
		}


		// 获取目标格式后,将纹理转换为目标格式,使其能被 DX12 使用
		m_WICFactory->CreateFormatConverter(&m_WICFormatConverter);		// 创建图片转换器
		// 初始化转换器,实际上是把位图进行了转换
		m_WICFormatConverter->Initialize(m_WICBitmapDecodeFrame.Get(), TargetFormat, WICBitmapDitherTypeNone,
			nullptr, 0.0f, WICBitmapPaletteTypeCustom);
		// 将位图数据继承到 WIC 位图资源,我们要在这个 WIC 位图资源上获取信息
		m_WICFormatConverter.As(&m_WICBitmapSource);



		m_WICBitmapSource->GetSize(&TextureWidth, &TextureHeight);		// 获取纹理宽高

		ComPtr<IWICComponentInfo> _temp_WICComponentInfo = {};			// 用于获取 BitsPerPixel 纹理图像深度
		ComPtr<IWICPixelFormatInfo> _temp_WICPixelInfo = {};			// 用于获取 BitsPerPixel 纹理图像深度
		m_WICFactory->CreateComponentInfo(TargetFormat, &_temp_WICComponentInfo);
		_temp_WICComponentInfo.As(&_temp_WICPixelInfo);
		_temp_WICPixelInfo->GetBitsPerPixel(&BitsPerPixel);				// 获取 BitsPerPixel 图像深度

		return true;
	}

	// 上取整算法,对 A 向上取整,判断至少要多少个长度为 B 的空间才能容纳 A,用于内存对齐
	inline UINT Ceil(UINT A, UINT B)
	{
		return (A + B - 1) / B;
	}

	// 创建用于上传的 UploadResource 与用于放纹理的 DefaultResource
	void CreateUploadAndDefaultResource(ModelManager::TEXTURE_MAP_INFO& Info)
	{
		// 计算纹理每行数据的真实数据大小 (单位:Byte 字节),因为纹理图片在内存中是线性存储的
		// 想获取纹理的真实大小、正确读取纹理数据、上传到 GPU,必须先获取纹理的 BitsPerPixel 图像深度,因为不同位图深度可能不同
		// 然后再计算每行像素占用的字节,除以 8 是因为 1 Byte = 8 bits
		BytePerRowSize = TextureWidth * BitsPerPixel / 8;

		// 纹理的真实大小 (单位:字节)
		TextureSize = BytePerRowSize * TextureHeight;

		// 上传堆资源每行的大小 (单位:字节),注意这里要进行 256 字节对齐!
		// 因为 GPU 与 CPU 架构不同,GPU 注重并行计算,注重结构化数据的快速读取,读取数据都是以 256 字节为一组来读的
		// 因此要先要对 BytePerRowSize 进行对齐,判断需要有多少组才能容纳纹理每行像素,不对齐的话数据会读错的。
		UploadResourceRowSize = Ceil(BytePerRowSize, 256) * 256;

		// 上传堆资源的总大小 (单位:字节),分配空间必须只多不少,否则会报 D3D12 MinimumAlloc Error 资源内存创建错误
		// 注意最后一行不用内存对齐 (因为后面没其他行了,不用内存对齐也能正确读取),所以要 (TextureHeight - 1) 再加 BytePerRowSize
		UploadResourceSize = UploadResourceRowSize * (TextureHeight - 1) + BytePerRowSize;


		// 用于中转纹理的上传堆资源结构体
		D3D12_RESOURCE_DESC UploadResourceDesc = {};
		UploadResourceDesc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER;		// 资源类型,上传堆的资源类型都是 buffer 缓冲
		UploadResourceDesc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR;			// 资源布局,指定资源的存储方式,上传堆的资源都是 row major 按行线性存储
		UploadResourceDesc.Width = UploadResourceSize;						// 资源宽度,上传堆的资源宽度是资源的总大小,注意资源大小必须只多不少
		UploadResourceDesc.Height = 1;										// 资源高度,上传堆仅仅是传递线性资源的,所以高度必须为 1
		UploadResourceDesc.Format = DXGI_FORMAT_UNKNOWN;					// 资源格式,上传堆资源的格式必须为 UNKNOWN
		UploadResourceDesc.DepthOrArraySize = 1;							// 资源深度,这个是用于纹理数组和 3D 纹理的,上传堆资源必须为 1
		UploadResourceDesc.MipLevels = 1;									// Mipmap 等级,这个是用于纹理的,上传堆资源必须为 1
		UploadResourceDesc.SampleDesc.Count = 1;							// 资源采样次数,上传堆资源都是填 1

		// 上传堆属性的结构体,上传堆位于 CPU 和 GPU 的共享内存
		D3D12_HEAP_PROPERTIES UploadHeapDesc = { D3D12_HEAP_TYPE_UPLOAD };

		// 创建上传堆资源
		m_D3D12Device->CreateCommittedResource(&UploadHeapDesc, D3D12_HEAP_FLAG_NONE, &UploadResourceDesc,
			D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&Info.UploadHeapTextureResource));


		// 用于放纹理的默认堆资源结构体
		D3D12_RESOURCE_DESC DefaultResourceDesc = {};
		DefaultResourceDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;	// 资源类型,这里指定为 Texture2D 2D纹理
		DefaultResourceDesc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN;			// 纹理资源的布局都是 UNKNOWN
		DefaultResourceDesc.Width = TextureWidth;							// 资源宽度,这里填纹理宽度
		DefaultResourceDesc.Height = TextureHeight;							// 资源高度,这里填纹理高度
		DefaultResourceDesc.Format = TextureFormat;							// 资源格式,这里填纹理格式,要和纹理一样
		DefaultResourceDesc.DepthOrArraySize = 1;							// 资源深度,我们只有一副纹理,所以填 1
		DefaultResourceDesc.MipLevels = 1;									// Mipmap 等级,我们暂时不使用 Mipmap,所以填 1
		DefaultResourceDesc.SampleDesc.Count = 1;							// 资源采样次数,这里我们填 1 就行

		// 默认堆属性的结构体,默认堆位于显存
		D3D12_HEAP_PROPERTIES DefaultHeapDesc = { D3D12_HEAP_TYPE_DEFAULT };

		// 创建默认堆资源
		m_D3D12Device->CreateCommittedResource(&DefaultHeapDesc, D3D12_HEAP_FLAG_NONE, &DefaultResourceDesc,
			D3D12_RESOURCE_STATE_COPY_DEST, nullptr, IID_PPV_ARGS(&Info.DefaultHeapTextureResource));
	}

	// CommandList 录制命令,录制将纹理数据复制到默认堆资源的命令
	void CopyTextureDataToDefaultResource(ModelManager::TEXTURE_MAP_INFO& Info)
	{
		// 用于暂时存储纹理数据的指针,这里要用 malloc 分配空间
		BYTE* TextureData = (BYTE*)malloc(TextureSize);

		// 将整块纹理数据读到 TextureData 中,方便后文的 memcpy 复制操作
		m_WICBitmapSource->CopyPixels(nullptr, BytePerRowSize, TextureSize, TextureData);

		// 用于传递资源的指针
		BYTE* TransferPointer = nullptr;

		// Map 开始映射,Map 方法会得到上传堆资源的地址 (在共享内存上),传递给指针,这样我们就能通过 memcpy 操作复制数据了
		Info.UploadHeapTextureResource->Map(0, nullptr, reinterpret_cast<void**>(&TransferPointer));

		// 这里我们要逐行复制数据!注意两个指针偏移的长度不同!
		for (UINT i = 0; i < TextureHeight; i++)
		{
			// 向上传堆资源逐行复制纹理数据 (CPU 高速缓存 -> 共享内存)
			memcpy(TransferPointer, TextureData, BytePerRowSize);
			// 纹理指针偏移到下一行
			TextureData += BytePerRowSize;
			// 上传堆资源指针偏移到下一行,注意偏移长度不同!
			TransferPointer += UploadResourceRowSize;
		}

		// Unmap 结束映射,因为我们无法直接读写默认堆资源,需要上传堆复制到那里,在复制之前,我们需要先结束映射,让上传堆处于只读状态
		Info.UploadHeapTextureResource->Unmap(0, nullptr);

		TextureData -= TextureSize;		// 纹理资源指针偏移回初始位置
		free(TextureData);				// 释放上文 malloc 分配的空间,后面我们用不到它,不要让它占内存

		D3D12_PLACED_SUBRESOURCE_FOOTPRINT PlacedFootprint = {};								// 资源脚本,用来描述要复制的资源
		D3D12_RESOURCE_DESC DefaultResourceDesc = Info.DefaultHeapTextureResource->GetDesc();	// 默认堆资源结构体

		// 获取纹理复制脚本,用于下文的纹理复制
		m_D3D12Device->GetCopyableFootprints(&DefaultResourceDesc, 0, 1, 0, &PlacedFootprint, nullptr, nullptr, nullptr);

		D3D12_TEXTURE_COPY_LOCATION DstLocation = {};						// 复制目标位置 (默认堆资源) 结构体
		DstLocation.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX;		// 纹理复制类型,这里必须指向纹理
		DstLocation.SubresourceIndex = 0;									// 指定要复制的子资源索引
		DstLocation.pResource = Info.DefaultHeapTextureResource.Get();		// 要复制到的资源

		D3D12_TEXTURE_COPY_LOCATION SrcLocation = {};						// 复制源位置 (上传堆资源) 结构体
		SrcLocation.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT;		// 纹理复制类型,这里必须指向缓冲区
		SrcLocation.PlacedFootprint = PlacedFootprint;						// 指定要复制的资源脚本信息
		SrcLocation.pResource = Info.UploadHeapTextureResource.Get();		// 被复制数据的缓冲



		// 记录复制资源到默认堆的命令 (共享内存 -> 显存) 
		m_CommandList->CopyTextureRegion(&DstLocation, 0, 0, 0, &SrcLocation, nullptr);
	}

	// 关闭命令列表,启动命令队列,正式开始将纹理复制到默认堆资源中
	void StartCommandExecute()
	{
		// 关闭命令列表
		m_CommandList->Close();

		// 用于传递命令用的临时 ID3D12CommandList 数组
		ID3D12CommandList* _temp_cmdlists[] = { m_CommandList.Get() };

		// 提交复制命令!GPU 开始复制!
		m_CommandQueue->ExecuteCommandLists(1, _temp_cmdlists);


		// 将围栏预定值设定为下一帧,注意复制资源也需要围栏等待,否则会发生资源冲突
		FenceValue++;
		// 在命令队列 (命令队列在 GPU 端) 设置围栏预定值,此命令会加入到命令队列中
		// 命令队列执行到这里会修改围栏值,表示复制已完成,"击中"围栏
		m_CommandQueue->Signal(m_Fence.Get(), FenceValue);
		// 设置围栏的预定事件,当复制完成时,围栏被"击中",激发预定事件,将事件由无信号状态转换成有信号状态
		m_Fence->SetEventOnCompletion(FenceValue, RenderEvent);
	}

	// 最终创建 SRV 着色器资源描述符,用于描述默认堆资源为一块纹理,创建完 SRV 描述符,会将描述符句柄存储到纹理映射表中
	void CreateSRV(ModelManager::TEXTURE_MAP_INFO& Info, 
		D3D12_CPU_DESCRIPTOR_HANDLE CPUHandle, D3D12_GPU_DESCRIPTOR_HANDLE GPUHandle)
	{
		// SRV 描述符信息结构体
		D3D12_SHADER_RESOURCE_VIEW_DESC SRVDescriptorDesc = {};
		// SRV 描述符类型,这里我们指定 Texture2D 2D纹理
		SRVDescriptorDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
		// SRV 描述符的格式也要填纹理格式
		SRVDescriptorDesc.Format = TextureFormat;
		// 纹理采样后每个纹理像素 RGBA 分量的顺序,D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING 表示纹理采样后分量顺序不改变
		SRVDescriptorDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
		// 这里我们不使用 Mipmap,所以填 1
		SRVDescriptorDesc.Texture2D.MipLevels = 1;

		// 创建 SRV 描述符,注意这里要用参数中的 CPUHandle
		m_D3D12Device->CreateShaderResourceView(Info.DefaultHeapTextureResource.Get(), &SRVDescriptorDesc, CPUHandle);

		// 将当前 SRV 描述符句柄存储到 ModelManager 的纹理映射表中,注意我们传的是引用参数,可以直接对参数进行修改
		Info.CPUHandle = CPUHandle;
		Info.GPUHandle = GPUHandle;
	}


	// 读取并创建纹理资源
	void CreateModelTextureResource()
	{
		CreateSRVHeap();	// 创建 SRV 描述符堆,创建时就会确定描述符的 CPU 和 GPU 地址,无需担心

		// 当前元素的 CPU 句柄
		D3D12_CPU_DESCRIPTOR_HANDLE CurrentCPUHandle = m_SRVHeap->GetCPUDescriptorHandleForHeapStart();
		// 当前元素的 GPU 句柄
		D3D12_GPU_DESCRIPTOR_HANDLE CurrentGPUHandle = m_SRVHeap->GetGPUDescriptorHandleForHeapStart();
		// SRV 描述符的大小
		UINT SRVDescriptorSize = m_D3D12Device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);

		StartCommandRecord();	// 启动命令列表,开始录制命令

		// 对纹理映射表进行遍历
		for (auto& CurrentElem : m_ModelManager.Texture_SRV_Map)
		{
			// 从纹理文件中加载数据
			LoadTextureFromFile(CurrentElem.second.TextureFilePath);
			// 创建上传堆和默认堆资源
			CreateUploadAndDefaultResource(CurrentElem.second);
			// 将纹理数据复制到上传堆,并记录一条上传堆复制到默认堆的命令
			CopyTextureDataToDefaultResource(CurrentElem.second);
			// 最终创建 SRV 描述符
			CreateSRV(CurrentElem.second, CurrentCPUHandle, CurrentGPUHandle);

			// CPU 和 GPU 句柄偏移,准备下一个纹理
			CurrentCPUHandle.ptr += SRVDescriptorSize;
			CurrentGPUHandle.ptr += SRVDescriptorSize;
		}

		StartCommandExecute();	// 关闭命令列表,交给命令队列执行
	}


	// 创建模型顶点与索引资源
	void CreateModelVertexAndIndexResource()
	{
		m_ModelManager.CreateBlock();
		m_ModelManager.CreateModelResource(m_D3D12Device);
	}


	// 创建 Constant Buffer Resource 常量缓冲资源,常量缓冲是一块预先分配的高速显存,用于存储每一帧都要变换的资源,这里我们要存储 MVP 矩阵
	void CreateCBVResource()
	{
		// 常量资源宽度,这里填整个结构体的大小。注意!硬件要求,常量缓冲需要 256 字节对齐!所以这里要进行 Ceil 向上取整,进行内存对齐!
		UINT CBufferWidth = Ceil(sizeof(CBuffer), 256) * 256;

		D3D12_RESOURCE_DESC CBVResourceDesc = {};						// 常量缓冲资源信息结构体
		CBVResourceDesc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER;	// 上传堆资源都是缓冲
		CBVResourceDesc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR;		// 上传堆资源都是按行存储数据的 (一维线性存储)
		CBVResourceDesc.Width = CBufferWidth;							// 常量缓冲区资源宽度 (要分配显存的总大小)
		CBVResourceDesc.Height = 1;										// 上传堆资源都是存储一维线性资源,所以高度必须为 1
		CBVResourceDesc.Format = DXGI_FORMAT_UNKNOWN;					// 上传堆资源的格式必须为 DXGI_FORMAT_UNKNOWN
		CBVResourceDesc.DepthOrArraySize = 1;							// 资源深度,这个是用于纹理数组和 3D 纹理的,上传堆资源必须为 1
		CBVResourceDesc.MipLevels = 1;									// Mipmap 等级,这个是用于纹理的,上传堆资源必须为 1
		CBVResourceDesc.SampleDesc.Count = 1;							// 资源采样次数,上传堆资源都是填 1

		// 上传堆属性的结构体,上传堆位于 CPU 和 GPU 的共享内存
		D3D12_HEAP_PROPERTIES UploadHeapDesc = { D3D12_HEAP_TYPE_UPLOAD };

		// 创建常量缓冲资源
		m_D3D12Device->CreateCommittedResource(&UploadHeapDesc, D3D12_HEAP_FLAG_NONE, &CBVResourceDesc,
			D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&m_CBVResource));



		// 常量缓冲直接 Map 映射到结构体指针就行即可,无需 Unmap 关闭映射,Map-Unmap 是耗时操作,对于动态数据我们都只需要 Map 一次就行,然后直接对指针修改数据,这样就实现了常量缓冲数据的修改
		// 因为我们每帧都要变换 MVP 矩阵,每帧都要对 MVPBuffer 进行修改,所以我们直接将上传堆资源地址映射到结构体指针
		// 每帧直接对指针进行修改操作,不用再进行 Unmap 了,这样着色器每帧都能读取到修改后的数据了
		m_CBVResource->Map(0, nullptr, reinterpret_cast<void**>(&MVPBuffer));

	}


	// 创建根签名
	void CreateRootSignature()
	{
		ComPtr<ID3DBlob> SignatureBlob;			// 根签名字节码
		ComPtr<ID3DBlob> ErrorBlob;				// 错误字节码,根签名创建失败时用 OutputDebugStringA((const char*)ErrorBlob->GetBufferPointer()); 可以获取报错信息

		D3D12_ROOT_PARAMETER RootParameters[2] = {};							// 根参数数组

		// 把更新频率高的根参数放前面,低的放后面,可以优化性能 (微软官方文档建议)
		// 因为 DirectX API 能对根签名进行 Version Control 版本控制,在根签名越前面的根参数,访问速度更快

		// 第一个根参数:CBV 根描述符,根描述符是内联描述符,所以下文绑定根参数时,只需要传递常量缓冲资源的地址即可

		D3D12_ROOT_DESCRIPTOR CBVRootDescriptorDesc = {};					// 常量缓冲根描述符信息结构体
		CBVRootDescriptorDesc.ShaderRegister = 0;							// 要绑定的寄存器编号,这里对应 HLSL 的 b0 寄存器
		CBVRootDescriptorDesc.RegisterSpace = 0;							// 要绑定的命名空间,这里对应 HLSL 的 space0

		RootParameters[0].ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL;	// 常量缓冲对整个渲染管线都可见
		RootParameters[0].ParameterType = D3D12_ROOT_PARAMETER_TYPE_CBV;	// 第二个根参数的类型:CBV 根描述符
		RootParameters[0].Descriptor = CBVRootDescriptorDesc;				// 填上文的结构体


		// 第二个根参数:根描述表 (Range: SRV)

		D3D12_DESCRIPTOR_RANGE SRVDescriptorRangeDesc = {};						// Range 描述符范围结构体,一块 Range 表示一堆连续的同类型描述符
		SRVDescriptorRangeDesc.RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_SRV;		// Range 类型,这里指定 SRV 类型,CBV_SRV_UAV 在这里分流
		SRVDescriptorRangeDesc.NumDescriptors = 1;								// Range 里面的描述符数量 N,一次可以绑定多个描述符到多个寄存器槽上
		SRVDescriptorRangeDesc.BaseShaderRegister = 0;							// Range 要绑定的起始寄存器槽编号 i,绑定范围是 [s(i),s(i+N)],我们绑定 s0
		SRVDescriptorRangeDesc.RegisterSpace = 0;								// Range 要绑定的寄存器空间,整个 Range 都会绑定到同一寄存器空间上,我们绑定 space0
		SRVDescriptorRangeDesc.OffsetInDescriptorsFromTableStart = 0;			// Range 到根描述表开头的偏移量 (单位:描述符),根签名需要用它来寻找 Range 的地址,我们这填 0 就行

		D3D12_ROOT_DESCRIPTOR_TABLE RootDescriptorTableDesc = {};				// RootDescriptorTable 根描述表信息结构体,一个 Table 可以有多个 Range
		RootDescriptorTableDesc.pDescriptorRanges = &SRVDescriptorRangeDesc;	// Range 描述符范围指针
		RootDescriptorTableDesc.NumDescriptorRanges = 1;						// 根描述表中 Range 的数量

		RootParameters[1].ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL;				// 根参数在着色器中的可见性,这里指定仅在像素着色器可见 (只有像素着色器用到了纹理)
		RootParameters[1].ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE;	// 根参数类型,这里我们选 Table 根描述表,一个根描述表占用 1 DWORD
		RootParameters[1].DescriptorTable = RootDescriptorTableDesc;					// 根参数指针



		D3D12_STATIC_SAMPLER_DESC StaticSamplerDesc = {};						// 静态采样器结构体,静态采样器不会占用根签名
		StaticSamplerDesc.ShaderRegister = 0;									// 要绑定的寄存器槽,对应 s0
		StaticSamplerDesc.RegisterSpace = 0;									// 要绑定的寄存器空间,对应 space0
		StaticSamplerDesc.ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL;		// 静态采样器在着色器中的可见性,这里指定仅在像素着色器可见 (只有像素着色器用到了纹理采样)
		StaticSamplerDesc.Filter = D3D12_FILTER_COMPARISON_MIN_MAG_MIP_POINT;	// 纹理过滤类型,这里我们直接选 邻近点采样 就行
		StaticSamplerDesc.AddressU = D3D12_TEXTURE_ADDRESS_MODE_BORDER;			// 在 U 方向上的纹理寻址方式
		StaticSamplerDesc.AddressV = D3D12_TEXTURE_ADDRESS_MODE_BORDER;			// 在 V 方向上的纹理寻址方式
		StaticSamplerDesc.AddressW = D3D12_TEXTURE_ADDRESS_MODE_BORDER;			// 在 W 方向上的纹理寻址方式 (3D 纹理会用到)
		StaticSamplerDesc.MinLOD = 0;											// 最小 LOD 细节层次,这里我们默认填 0 就行
		StaticSamplerDesc.MaxLOD = D3D12_FLOAT32_MAX;							// 最大 LOD 细节层次,这里我们默认填 D3D12_FLOAT32_MAX (没有 LOD 上限)
		StaticSamplerDesc.MipLODBias = 0;										// 基础 Mipmap 采样偏移量,我们这里我们直接填 0 就行
		StaticSamplerDesc.MaxAnisotropy = 1;									// 各向异性过滤等级,我们不使用各向异性过滤,需要默认填 1
		StaticSamplerDesc.ComparisonFunc = D3D12_COMPARISON_FUNC_NEVER;			// 这个是用于阴影贴图的,我们不需要用它,所以填 D3D12_COMPARISON_FUNC_NEVER


		D3D12_ROOT_SIGNATURE_DESC rootsignatureDesc = {};			// 根签名信息结构体,上限 64 DWORD,静态采样器不占用根签名
		rootsignatureDesc.NumParameters = 2;						// 根参数数量
		rootsignatureDesc.pParameters = RootParameters;				// 根参数指针
		rootsignatureDesc.NumStaticSamplers = 1;					// 静态采样器数量
		rootsignatureDesc.pStaticSamplers = &StaticSamplerDesc;		// 静态采样器指针
		// 根签名标志,可以设置渲染管线不同阶段下的输入参数状态。注意这里!我们要从 IA 阶段输入顶点数据,所以要通过根签名,设置渲染管线允许从 IA 阶段读入数据
		rootsignatureDesc.Flags = D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT;

		// 编译根签名,让根签名先编译成 GPU 可读的二进制字节码
		D3D12SerializeRootSignature(&rootsignatureDesc, D3D_ROOT_SIGNATURE_VERSION_1_0, &SignatureBlob, &ErrorBlob);
		if (ErrorBlob)		// 如果根签名编译出错,ErrorBlob 可以提供报错信息
		{
			OutputDebugStringA((const char*)ErrorBlob->GetBufferPointer());
			OutputDebugStringA("\n");
		}


		// 用这个二进制字节码创建根签名对象
		m_D3D12Device->CreateRootSignature(0, SignatureBlob->GetBufferPointer(), SignatureBlob->GetBufferSize(), IID_PPV_ARGS(&m_RootSignature));
	}

	// 创建渲染管线状态对象 (Pipeline State Object, PSO)
	void CreatePSO()
	{
		// PSO 信息结构体
		D3D12_GRAPHICS_PIPELINE_STATE_DESC PSODesc = {};

		// Input Assembler 输入装配阶段
		D3D12_INPUT_LAYOUT_DESC InputLayoutDesc = {};			// 输入样式信息结构体
		D3D12_INPUT_ELEMENT_DESC InputElementDesc[6] = {};		// 输入元素信息结构体数组

		// 第 0 号输入槽: 输入顶点位置与纹理 UV 坐标

		InputElementDesc[0].SemanticName = "POSITION";					// 要锚定的语义
		InputElementDesc[0].SemanticIndex = 0;							// 语义索引,目前我们填 0 就行
		InputElementDesc[0].Format = DXGI_FORMAT_R32G32B32A32_FLOAT;	// 输入格式
		InputElementDesc[0].InputSlot = 0;								// 输入槽编号,目前我们填 0 就行
		InputElementDesc[0].AlignedByteOffset = 0;						// 在输入槽中的偏移
		// 输入流类型,一种是我们现在用的 D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA 逐顶点输入流,还有一种叫逐实例输入流,后面再学
		InputElementDesc[0].InputSlotClass = D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA;
		InputElementDesc[0].InstanceDataStepRate = 0;					// 实例数据步进率,目前我们没有用到实例化,填 0


		InputElementDesc[1].SemanticName = "TEXCOORD";										// 要锚定的语义
		InputElementDesc[1].SemanticIndex = 0;												// 语义索引
		InputElementDesc[1].Format = DXGI_FORMAT_R32G32_FLOAT;								// 输入格式
		InputElementDesc[1].InputSlot = 0;													// 输入槽编号
		// 在输入槽中的偏移,因为 position 与 texcoord 在同一输入槽(0号输入槽)
		// position 是 float4,有 4 个 float ,每个 float 占 4 个字节,所以要偏移 4*4=16 个字节,这样才能确定 texcoord 参数的位置,不然装配的时候会覆盖原先 position 的数据
		InputElementDesc[1].AlignedByteOffset = 16;											// 在输入槽中的偏移
		InputElementDesc[1].InputSlotClass = D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA;	// 输入流类型
		InputElementDesc[1].InstanceDataStepRate = 0;										// 实例数据步进率


		// 第 1 号输入槽: 输入模型矩阵 (因为 4x4 矩阵太大,需要分成 4 个 float4 向量传输)

		// MATRIX0
		// SemanticName 语义名:							MATRIX (语义名后面不带数字)
		// SemanticIndex 语义索引:						0
		// InputSlot 输入槽:								第 1 号输入槽
		// AlignedByteOffset 相对输入槽起始位置的偏移:		0
		InputElementDesc[2].SemanticName = "MATRIX";
		InputElementDesc[2].SemanticIndex = 0;
		InputElementDesc[2].Format = DXGI_FORMAT_R32G32B32A32_FLOAT;
		InputElementDesc[2].InputSlot = 1;
		InputElementDesc[2].AlignedByteOffset = 0;
		InputElementDesc[2].InputSlotClass = D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA;
		InputElementDesc[2].InstanceDataStepRate = 0;

		// MATRIX1
		// SemanticName 语义名:							MATRIX (语义名后面不带数字)
		// SemanticIndex 语义索引:						1
		// InputSlot 输入槽:								第 1 号输入槽
		// AlignedByteOffset 相对输入槽起始位置的偏移:		0 + 4*4 = 16
		InputElementDesc[3].SemanticName = "MATRIX";
		InputElementDesc[3].SemanticIndex = 1;
		InputElementDesc[3].Format = DXGI_FORMAT_R32G32B32A32_FLOAT;
		InputElementDesc[3].InputSlot = 1;
		InputElementDesc[3].AlignedByteOffset = 16;
		InputElementDesc[3].InputSlotClass = D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA;
		InputElementDesc[3].InstanceDataStepRate = 0;

		// MATRIX2
		// SemanticName 语义名:							MATRIX (语义名后面不带数字)
		// SemanticIndex 语义索引:						2
		// InputSlot 输入槽:								第 1 号输入槽
		// AlignedByteOffset 相对输入槽起始位置的偏移:		16 + 4*4 = 32
		InputElementDesc[4].SemanticName = "MATRIX";
		InputElementDesc[4].SemanticIndex = 2;
		InputElementDesc[4].Format = DXGI_FORMAT_R32G32B32A32_FLOAT;
		InputElementDesc[4].InputSlot = 1;
		InputElementDesc[4].AlignedByteOffset = 32;
		InputElementDesc[4].InputSlotClass = D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA;
		InputElementDesc[4].InstanceDataStepRate = 0;

		// MATRIX3
		// SemanticName 语义名:							MATRIX (语义名后面不带数字)
		// SemanticIndex 语义索引:						3
		// InputSlot 输入槽:								第 1 号输入槽
		// AlignedByteOffset 相对输入槽起始位置的偏移:		32 + 4*4 = 48
		InputElementDesc[5].SemanticName = "MATRIX";
		InputElementDesc[5].SemanticIndex = 3;
		InputElementDesc[5].Format = DXGI_FORMAT_R32G32B32A32_FLOAT;
		InputElementDesc[5].InputSlot = 1;
		InputElementDesc[5].AlignedByteOffset = 48;
		InputElementDesc[5].InputSlotClass = D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA;
		InputElementDesc[5].InstanceDataStepRate = 0;



		InputLayoutDesc.NumElements = 6;						// 输入元素个数
		InputLayoutDesc.pInputElementDescs = InputElementDesc;	// 输入元素结构体数组指针
		PSODesc.InputLayout = InputLayoutDesc;					// 设置渲染管线 IA 阶段的输入样式




		ComPtr<ID3DBlob> VertexShaderBlob;		// 顶点着色器二进制字节码
		ComPtr<ID3DBlob> PixelShaderBlob;		// 像素着色器二进制字节码
		ComPtr<ID3DBlob> ErrorBlob;				// 错误字节码,根签名创建失败时用 OutputDebugStringA((const char*)ErrorBlob->GetBufferPointer()); 可以获取报错信息

		// 编译顶点着色器 Vertex Shader
		D3DCompileFromFile(L"shader.hlsl", nullptr, nullptr, "VSMain", "vs_5_1", NULL, NULL, &VertexShaderBlob, &ErrorBlob);
		if (ErrorBlob)		// 如果着色器编译出错,ErrorBlob 可以提供报错信息
		{
			OutputDebugStringA((const char*)ErrorBlob->GetBufferPointer());
			OutputDebugStringA("\n");
		}

		// 编译像素着色器 Pixel Shader
		D3DCompileFromFile(L"shader.hlsl", nullptr, nullptr, "PSMain", "ps_5_1", NULL, NULL, &PixelShaderBlob, &ErrorBlob);
		if (ErrorBlob)		// 如果着色器编译出错,ErrorBlob 可以提供报错信息
		{
			OutputDebugStringA((const char*)ErrorBlob->GetBufferPointer());
			OutputDebugStringA("\n");
		}

		PSODesc.VS.pShaderBytecode = VertexShaderBlob->GetBufferPointer();		// VS 字节码数据指针
		PSODesc.VS.BytecodeLength = VertexShaderBlob->GetBufferSize();			// VS 字节码数据长度
		PSODesc.PS.pShaderBytecode = PixelShaderBlob->GetBufferPointer();		// PS 字节码数据指针
		PSODesc.PS.BytecodeLength = PixelShaderBlob->GetBufferSize();			// PS 字节码数据长度

		// Rasterizer 光栅化
		PSODesc.RasterizerState.CullMode = D3D12_CULL_MODE_BACK;		// 剔除模式,指定是否开启背面/正面/不剔除,这里选背面剔除
		PSODesc.RasterizerState.FillMode = D3D12_FILL_MODE_SOLID;		// 填充模式,指定是否开启纯色/线框填充,这里选纯色填充

		// 第一次设置根签名!本次设置是将根签名与 PSO 绑定,设置渲染管线的输入参数状态
		PSODesc.pRootSignature = m_RootSignature.Get();

		// 设置深度测试状态
		PSODesc.DSVFormat = DSVFormat;											// 设置深度缓冲的格式
		PSODesc.DepthStencilState.DepthEnable = false;							// 开启深度缓冲
		PSODesc.DepthStencilState.DepthFunc = D3D12_COMPARISON_FUNC_LESS;		// 深度缓冲的比较方式
		PSODesc.DepthStencilState.DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ALL;	// 深度缓冲的读写权限

		// D3D12_DEPTH_WRITE_MASK_ALL	允许通过深度测试的像素写入深度缓冲			(深度缓冲可读写)
		// D3D12_DEPTH_WRITE_MASK_ZERO	禁止对深度缓冲进行写操作,但仍可进行深度测试	(深度缓冲只读)
		// 两者只能选一个,不可共存。指定 DepthWriteMask 可以控制深度数据的读写,实现某些特效

		/*
			深度测试比较像素深度的伪代码,符合条件就覆盖新像素,不符合就丢弃
			NewPixel:			要写入的新像素
			CurrentPixel:		当前在缓冲区的像素
			DepthFunc:			比较方式 (实际上就是 C/C++ 的二元操作运算符)

			if (NewPixel.Depth  DepthFunc  CurrentPixel.Depth)
			{
				Accept(NewPixel)			// 新像素通过深度测试,下一步可以进行混合
				WriteDepth(NewPixel.Depth)	// 将新像素深度写入深度缓冲中
			}
			else
			{
				Reject(NewPixel)			// 丢弃新像素
			}


			D3D12_COMPARISON_FUNC_LESS 相当于小于号 <

			if (NewPixel.Depth  <  CurrentPixel.Depth)
			{
				Accept(NewPixel)			// 如果新像素深度更小,说明距离摄像机更靠前,通过深度测试
				WriteDepth(NewPixel.Depth)	// 将新像素深度写入深度缓冲中
			}
			else
			{
				Reject(NewPixel)			// 否则,这个新像素更靠后,被当前像素遮住了,丢弃新像素
			}
		*/


		// 设置基本图元,这里我们设置三角形面
		PSODesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
		// 设置渲染目标数量,我们只有一副渲染目标 (颜色缓冲) 需要进行渲染,所以填 1
		PSODesc.NumRenderTargets = 1;
		// 设置渲染目标的格式,这里要和交换链指定窗口缓冲的格式一致,这里的 0 指的是渲染目标的索引
		PSODesc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM;
		// 设置混合阶段 (输出合并阶段) 下 RGBA 颜色通道的开启和关闭,D3D12_COLOR_WRITE_ENABLE_ALL 表示 RGBA 四色通道全部开启
		PSODesc.BlendState.RenderTarget[0].RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_ALL;
		// 设置采样次数,我们这里填 1 就行
		PSODesc.SampleDesc.Count = 1;
		// 设置采样掩码,这个是用于多重采样的,我们直接填全采样 (UINT_MAX,就是将 UINT 所有的比特位全部填充为 1) 就行
		PSODesc.SampleMask = UINT_MAX;

		// 最终创建 PSO 对象
		m_D3D12Device->CreateGraphicsPipelineState(&PSODesc, IID_PPV_ARGS(&m_PipelineStateObject));
	}


	// 更新常量缓冲区,将每帧新的 MVP 矩阵传递到常量缓冲区中,这样就能看到动态的 3D 画面了
	void UpdateConstantBuffer()
	{
		// 将更新后的矩阵,存储到共享内存上的常量缓冲,这样 GPU 就可以访问到 MVP 矩阵了
		XMStoreFloat4x4(&MVPBuffer->MVPMatrix, m_FirstCamera.GetMVPMatrix());
	}

	// 渲染
	void Render()
	{
		// 每帧渲染开始前,调用 UpdateConstantBuffer() 更新常量缓冲区
		UpdateConstantBuffer();

		// 获取 RTV 堆首句柄
		RTVHandle = m_RTVHeap->GetCPUDescriptorHandleForHeapStart();
		// 获取当前渲染的后台缓冲序号
		FrameIndex = m_DXGISwapChain->GetCurrentBackBufferIndex();
		// 偏移 RTV 句柄,找到对应的 RTV 描述符
		RTVHandle.ptr += FrameIndex * RTVDescriptorSize;


		// 先重置命令分配器
		m_CommandAllocator->Reset();
		// 再重置命令列表,Close 关闭状态 -> Record 录制状态
		m_CommandList->Reset(m_CommandAllocator.Get(), nullptr);

		// 将起始转换屏障的资源指定为当前渲染目标
		beg_barrier.Transition.pResource = m_RenderTarget[FrameIndex].Get();
		// 调用资源屏障,将渲染目标由 Present 呈现(只读) 转换到 RenderTarget 渲染目标(只写)
		m_CommandList->ResourceBarrier(1, &beg_barrier);

		// 第二次设置根签名!本次设置将会检查 渲染管线绑定的根签名 与 这里的根签名 是否匹配
		// 以及根签名指定的资源是否被正确绑定,检查完毕后会进行简单的映射
		m_CommandList->SetGraphicsRootSignature(m_RootSignature.Get());
		// 设置渲染管线状态,可以在上面 m_CommandList->Reset() 的时候直接在第二个参数设置 PSO
		m_CommandList->SetPipelineState(m_PipelineStateObject.Get());

		// 设置视口 (光栅化阶段),用于光栅化里的屏幕映射
		m_CommandList->RSSetViewports(1, &viewPort);
		// 设置裁剪矩形 (光栅化阶段)
		m_CommandList->RSSetScissorRects(1, &ScissorRect);

		// 用 RTV 句柄设置渲染目标,同时用 DSV 句柄设置深度模板缓冲,开启深度测试
		m_CommandList->OMSetRenderTargets(1, &RTVHandle, false, &DSVHandle);

		// 清空后台的深度模板缓冲,将深度重置为初始值 1,记住上文创建深度缓冲资源的时候,要填 ClearValue
		// 否则会报 D3D12 WARNING: The application did not pass any clear value to resource creation.
		m_CommandList->ClearDepthStencilView(DSVHandle, D3D12_CLEAR_FLAG_DEPTH, 1, 0, 0, nullptr);

		// 清空当前渲染目标的背景为天蓝色
		m_CommandList->ClearRenderTargetView(RTVHandle, DirectX::Colors::SkyBlue, 0, nullptr);

		// 用于设置描述符堆用的临时 ID3D12DescriptorHeap 数组
		ID3D12DescriptorHeap* _temp_DescriptorHeaps[] = { m_SRVHeap.Get() };
		// 设置描述符堆
		m_CommandList->SetDescriptorHeaps(1, _temp_DescriptorHeaps);

		// 设置常量缓冲 (第一个根参数),我们复制完数据到 CBVResource 后,就可以让着色器读取、对顶点进行 MVP 变换了
		m_CommandList->SetGraphicsRootConstantBufferView(0, m_CBVResource->GetGPUVirtualAddress());

		// 设置图元拓扑 (输入装配阶段),我们这里设置三角形列表
		m_CommandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);


		// 渲染全部模型
		m_ModelManager.RenderAllModel(m_CommandList);


		// 将终止转换屏障的资源指定为当前渲染目标
		end_barrier.Transition.pResource = m_RenderTarget[FrameIndex].Get();
		// 再通过一次资源屏障,将渲染目标由 RenderTarget 渲染目标(只写) 转换到 Present 呈现(只读)
		m_CommandList->ResourceBarrier(1, &end_barrier);

		// 关闭命令列表,Record 录制状态 -> Close 关闭状态,命令列表只有关闭才可以提交
		m_CommandList->Close();

		// 用于传递命令用的临时 ID3D12CommandList 数组
		ID3D12CommandList* _temp_cmdlists[] = { m_CommandList.Get() };

		// 执行上文的渲染命令!
		m_CommandQueue->ExecuteCommandLists(1, _temp_cmdlists);

		// 向命令队列发出交换缓冲的命令,此命令会加入到命令队列中,命令队列执行到该命令时,会通知交换链交换缓冲
		m_DXGISwapChain->Present(1, NULL);




		// 将围栏预定值设定为下一帧
		FenceValue++;
		// 在命令队列 (命令队列在 GPU 端) 设置围栏预定值,此命令会加入到命令队列中
		// 命令队列执行到这里会修改围栏值,表示渲染已完成,"击中"围栏
		m_CommandQueue->Signal(m_Fence.Get(), FenceValue);
		// 设置围栏的预定事件,当渲染完成时,围栏被"击中",激发预定事件,将事件由无信号状态转换成有信号状态
		m_Fence->SetEventOnCompletion(FenceValue, RenderEvent);
	}

	// 渲染循环
	void RenderLoop()
	{
		bool isExit = false;	// 是否退出
		MSG msg = {};			// 消息结构体

		while (isExit != true)
		{
			// MsgWaitForMultipleObjects 用于多个线程的无阻塞等待,返回值是激发事件 (线程) 的 ID
			// 经过该函数后 RenderEvent 也会自动重置为无信号状态,因为我们创建事件的时候指定了第二个参数为 false
			DWORD ActiveEvent = ::MsgWaitForMultipleObjects(1, &RenderEvent, false, INFINITE, QS_ALLINPUT);

			switch (ActiveEvent - WAIT_OBJECT_0)
			{
			case 0:				// ActiveEvent 是 0,说明渲染事件已经完成了,进行下一次渲染
			{
				Render();
			}
			break;


			case 1:				// ActiveEvent 是 1,说明渲染事件未完成,CPU 主线程同时处理窗口消息,防止界面假死
			{
				// 查看消息队列是否有消息,如果有就获取。 PM_REMOVE 表示获取完消息,就立刻将该消息从消息队列中移除
				while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
				{
					// 如果程序没有收到退出消息,就向操作系统发出派发消息的命令
					if (msg.message != WM_QUIT)
					{
						TranslateMessage(&msg);		// 翻译消息,当键盘按键发出信号 (WM_KEYDOWN),将虚拟按键值转换为对应的 ASCII 码,同时产生 WM_CHAR 消息
						DispatchMessage(&msg);		// 派发消息,通知操作系统调用回调函数处理消息
					}
					else
					{
						isExit = true;							// 收到退出消息,就退出消息循环
					}
				}
			}
			break;


			case WAIT_TIMEOUT:	// 渲染超时
			{

			}
			break;

			}
		}
	}

	// 回调函数,处理窗口产生的消息
	// WASD 键 —— WM_CHAR 字符消息 —— 摄像机前后左右移动
	// 鼠标长按左键移动 —— WM_MOUSEMOVE 鼠标移动消息 —— 摄像机视角旋转
	// 关闭窗口 —— WM_DESTROY 窗口销毁消息 —— 窗口关闭,程序进程退出
	LRESULT CALLBACK CallBackFunc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
	{
		// 用 switch 将第二个参数分流,每个 case 分别对应一个窗口消息
		switch (msg)
		{
		case WM_DESTROY:			// 窗口被销毁 (当按下右上角 X 关闭窗口时)
		{
			PostQuitMessage(0);		// 向操作系统发出退出请求 (WM_QUIT),结束消息循环
		}
		break;


		case WM_CHAR:	// 获取键盘产生的字符消息,TranslateMessage 会将虚拟键码翻译成字符码,同时产生 WM_CHAR 消息
		{
			switch (wParam)		// wParam 是按键对应的字符 ASCII 码
			{
			case 'w':
			case 'W':	// 向前移动
				m_FirstCamera.Walk(0.2);
				break;

			case 's':
			case 'S':	// 向后移动
				m_FirstCamera.Walk(-0.2);
				break;

			case 'a':
			case 'A':	// 向左移动
				m_FirstCamera.Strafe(0.2);
				break;

			case 'd':
			case 'D':	// 向右移动
				m_FirstCamera.Strafe(-0.2);
				break;
			}
		}
		break;


		case WM_MOUSEMOVE:	// 获取鼠标移动消息
		{
			switch (wParam)	// wParam 是鼠标按键的状态
			{
			case MK_LBUTTON:	// 当用户长按鼠标左键的同时移动鼠标,摄像机旋转
				m_FirstCamera.CameraRotate();
				break;

				// 按键没按,鼠标只是移动也要更新,否则就会发生摄像机视角瞬移
			default: m_FirstCamera.UpdateLastCursorPos();
			}
		}
		break;


		// 如果接收到其他消息,直接默认返回整个窗口
		default: return DefWindowProc(hwnd, msg, wParam, lParam);

		}

		return 0;	// 注意这里!default 除外的分支都会运行到这里,因此需要 return 0,否则就会返回系统随机值,导致窗口无法正常显示
	}


	// 运行窗口
	static void Run(HINSTANCE hins)
	{
		DX12Engine engine;
		engine.InitWindow(hins);
		engine.CreateDebugDevice();
		engine.CreateDevice();
		engine.CreateCommandComponents();
		engine.CreateRenderTarget();
		engine.CreateFenceAndBarrier();

		engine.CreateDSVHeap();
		engine.CreateDepthStencilBuffer();
		engine.CreateDSV();

		engine.CreateModelTextureResource();

		engine.CreateModelVertexAndIndexResource();

		engine.CreateCBVResource();

		engine.CreateRootSignature();
		engine.CreatePSO();


		engine.RenderLoop();
	}
};


// 主函数
int WINAPI WinMain(HINSTANCE hins, HINSTANCE hPrev, LPSTR cmdLine, int cmdShow)
{
	DX12Engine::Run(hins);
}



shader.hlsl



// (7) RenderMatchbox:渲染一个火柴盒,初步认识 Depth Stencil Buffer 深度模板缓冲,进一步认识顶点、模型与模型矩阵

struct VSInput      // VS 阶段输入顶点数据
{
    float4 position : POSITION;         // 输入顶点的位置,POSITION 语义对应 C++ 端输入布局中的 POSITION
    float2 texcoordUV : TEXCOORD;       // 输入顶点的纹理坐标,TEXCOORD 语义对应 C++ 端输入布局中的 TEXCOORD
    
    // 如果我们需要向 IA 阶段传递矩阵,矩阵太大没法直接传,我们可以把矩阵分割成一个一个行向量,再到 VS 阶段重新组装
    // MATRIX 是自定义语义,语义后面的数字表示同一个输入槽下,同语义名 (MATRIX) 的第 i 号数据
    float4 Matrix_Row0 : MATRIX0;
    float4 Matrix_Row1 : MATRIX1;
    float4 Matrix_Row2 : MATRIX2;
    float4 Matrix_Row3 : MATRIX3;
    
    // 其实语义只是个标识东西的字符串...
};

struct VSOutput     // VS 阶段输出顶点数据
{
    float4 position : SV_Position;      // 输出顶点的位置,SV_POSITION 是系统语义,指定顶点坐标已经位于齐次裁剪空间,通知光栅化阶段对顶点进行透视除法和屏幕映射
    float2 texcoordUV : TEXCOORD;       // 输出顶点纹理坐标时,仍然需要 TEXCOORD 语义
};

// Constant Buffer 常量缓冲,常量缓冲是预先分配的一段高速显存,存放每一帧都要变换的数据,例如我们这里的 MVP 变换矩阵
// 常量缓冲对所有着色器都是只读的,着色器不可以修改常量缓冲里面的内容
cbuffer GlobalData : register(b0, space0) // 常量缓冲,b 表示 buffer 缓冲,b0 表示 0 号 CBV 寄存器,space0 表示使用 b0 的 0 号空间
{
    row_major float4x4 MVP; // MVP 矩阵,用于将顶点坐标从模型空间变换到齐次裁剪空间,HLSL 默认按列存储,row_major 表示数据按行存储
}


// Vertex Shader 顶点着色器入口函数 (逐顶点输入),接收来自 IA 阶段输入的顶点数据,处理并返回齐次裁剪空间下的顶点坐标
// 上一阶段:Input Assembler 输入装配阶段
// 下一阶段:Rasterization 光栅化阶段
VSOutput VSMain(VSInput input)
{
    float4x4 ModelMatrix;   // VS 阶段要用到的模型矩阵
    VSOutput output;        // 输出给光栅化阶段的结构体变量
    
    // 将 IA 阶段得到的行数据组装成矩阵
    ModelMatrix[0] = input.Matrix_Row0;
    ModelMatrix[1] = input.Matrix_Row1;
    ModelMatrix[2] = input.Matrix_Row2;
    ModelMatrix[3] = input.Matrix_Row3;
    
    // 注意 cbuffer 常量缓冲对着色器是只读的!所以我们不能在这里对常量缓冲进行修改!
    output.position = mul(input.position, ModelMatrix);     // 先乘 模型矩阵
    output.position = mul(output.position, MVP);            // 再乘 观察矩阵 和 投影矩阵,注意 mul 左操作数是 output.position
    output.texcoordUV = input.texcoordUV;                   // 纹理 UV 不用变化,照常输出即可
    
    return output;
}

// register(*#,spaceN) *表示资源类型,#表示所用的寄存器编号,spaceN 表示使用的 N 号寄存器空间

Texture2D m_texure : register(t0, space0);      // 纹理,t 表示 SRV 着色器资源,t0 表示 0 号 SRV 寄存器,space0 表示使用 t0 的 0 号空间
SamplerState m_sampler : register(s0, space0);  // 纹理采样器,s 表示采样器,s0 表示 0 号 sampler 寄存器,space0 表示使用 s0 的 0 号空间

// Pixel Shader 像素着色器入口函数 (逐像素输入),接收来自光栅化阶段经过插值后的每个片元,返回像素颜色
// 上一阶段:Rasterization 光栅化阶段
// 下一阶段:Output Merger 输出合并阶段
float4 PSMain(VSOutput input) : SV_Target // SV_Target 也是系统语义,通知输出合并阶段将 PS 阶段返回的颜色写入到渲染目标(颜色缓冲)上
{
    return m_texure.Sample(m_sampler, input.texcoordUV); // 在像素着色器根据光栅化插值得到的 UV 坐标对纹理进行采样
}


在这里插入图片描述



如果读者需要找项目源代码 (文件夹下项目源代码是 “DX12 项目源代码 (xx.xx.xx 更新).zip”),也可以点击下方的链接,我们之后更新代码,都会发布到下面这个链接中:
项目源代码地址:https://wwek.lanzoue.com/b002uxwd3a 密码:dgaf

下一节教程,我们将补上门和玻璃,接触 AlphaTest 透明测试 和 AlphaBlend 透明混合 这两个新的概念。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

dgaf

谢谢大佬打赏

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值