Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第六章:在Direct3D中绘制

本文介绍了使用Direct3D 12进行游戏开发的基础知识,包括顶点和输入布局的定义、顶点缓冲与索引缓冲的创建、顶点着色器和像素着色器的编写。此外,还讲解了常量缓冲的创建与更新,以及渲染管线状态对象(PSO)的重要性。通过示例代码展示了如何将几何体数据上传到GPU,以及如何通过着色器进行渲染。最后,讨论了光栅化阶段、PSO的创建和使用,以及如何通过根签名和描述表绑定资源。
摘要由CSDN通过智能技术生成

代码工程地址:

https://github.com/jiabaodan/Direct12BookReadingNotes



学习目标

  1. 熟悉Direct3D接口的定义,保存和绘制几何数据 ;
  2. 学习编写基本的顶点和像素着色器;
  3. 学习使用渲染流水线状态对象来配置渲染流水线;
  4. 理解如何创建常数缓存数据(constant buffer data),并且熟悉根签名?(root signature);


1 顶点和输入布局

下面的代码定义了2类顶点:

struct Vertex1
{
	XMFLOAT3 Pos;
	XMFLOAT4 Color;
};

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

当我们定义完一个顶点结构以后,我们需要提供一个描述来让Direct3D知道每个组件是什么作用,这个描述由Direct3D结构体D3D12_INPUT_LAYOUT_DESC通过输入布局描述(input layout description)的形式提供:

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

一个输入布局描述就是D3D12_INPUT_ELEMENT_DESC的数组,和数组的个数。
数组中每个元素用来描述顶点结构中对应的组件,D3D12_INPUT_ELEMENT_DESC结构体定义如下:

typedef struct D3D12_INPUT_ELEMENT_DESC
{
	LPCSTR SemanticName;
	UINT SemanticIndex;
	DXGI_FORMAT Format;
	UINT InputSlot;
	UINT AlignedByteOffset;
	D3D12_INPUT_CLASSIFICATION InputSlotClass;
	UINT InstanceDataStepRate;
} D3D12_INPUT_ELEMENT_DESC;
  1. SemanticName:关联到顶点结构中每个元素,它主要用以将顶点结构中的元素映射到顶点着色器输入签名中使用;
    在这里插入图片描述
  2. SemanticIndex:关联到语义上的索引,使相同的语义可以多次使用,以索引区分,如上图;
  3. Format:由DXGI_FORMAT枚举类型定义的类型,下面是一些常用的值:
		DXGI_FORMAT_R32_FLOAT // 1D 32-bit float scalar
		DXGI_FORMAT_R32G32_FLOAT // 2D 32-bit float vector
		DXGI_FORMAT_R32G32B32_FLOAT // 3D 32-bit float vector
		DXGI_FORMAT_R32G32B32A32_FLOAT // 4D 32-bit float vector
		DXGI_FORMAT_R8_UINT // 1D 8-bit unsigned integer scalar
		DXGI_FORMAT_R16G16_SINT // 2D 16-bit signed integer vector
		DXGI_FORMAT_R32G32B32_UINT // 3D 32-bit unsigned integer vector
		DXGI_FORMAT_R8G8B8A8_SINT // 4D 8-bit signed integer vector
		DXGI_FORMAT_R8G8B8A8_UINT // 4D 8-bit unsigned integer vector
  1. InputSlot:定义元素传进来的输入槽,Direct3D支持16个输入槽(0~15);
  2. AlignedByteOffset:每个元素的偏移量,单位是字节:
		struct Vertex2
		{
			XMFLOAT3 Pos; // 0-byte offset
			XMFLOAT3 Normal; // 12-byte offset
			XMFLOAT2 Tex0; // 24-byte offset
			XMFLOAT2 Tex1; // 32-byte offset
		};
  1. InputSlotClass:目前暂时都设置为D3D12_INPUT_PER_VERTEX_DATA,其他值用以实例化技术;
  2. InstanceDataStepRate:目前暂时都设置为0,其他值用以实例化技术;

对于之前描述的两个顶点结构,其对应的输入布局描述如下:

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

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


2 顶点缓冲(VERTEX BUFFERS)

为了让GPU访问到顶点数组,我们需要将它们保存在一个叫缓冲(buffer)的GPU资源中(ID3D12Resource),用来保存顶点数据的缓冲叫做顶点缓冲。
如之前4.3.8,我们通过填写一个D3D12_RESOURCE_DESC结构体,创建一个ID3D12Resource对象来描述缓冲资源,然后调用ID3D12Device::CreateCommittedResource方法。Direct3D 12提供了一个C++封装的类CD3DX12_RESOURCE_DESC(继承自D3D12_RESOURCE_DESC),它提供了一个更加方便的构造方法:

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

with代表了缓冲中的字节数。

对于静态的几何体,我们将顶点缓冲放到默认堆中(default heap)((D3D12_HEAP_TYPE_DEFAULT)用以优化性能;为了创建实际的顶点缓冲资源,我们需要创建一个类型为D3D12_HEAP_TYPE_UPLOAD的上传缓冲(upload buffer)资源。
因为中间的上传缓冲需要在默认缓冲(default buffer)中初始化,所以我们在d3dUtil.h/.cpp中编写下面函数,用以避免重复代码:

Microsoft::WRL::ComPtr<ID3D12Resource> d3dUtil::CreateDefaultBuffer(
	ID3D12Device* device,
	ID3D12GraphicsCommandList* cmdList,
	const void* initData,
	UINT64 byteSize,
	Microsoft::WRL::ComPtr<ID3D12Resource>& uploadBuffer)
{
	ComPtr<ID3D12Resource> defaultBuffer;
	
	// Create the actual default buffer resource.
	ThrowIfFailed(device->CreateCommittedResource(
		&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT),
		D3D12_HEAP_FLAG_NONE,
		&CD3DX12_RESOURCE_DESC::Buffer(byteSize),
		D3D12_RESOURCE_STATE_COMMON,
		nullptr,
		IID_PPV_ARGS(defaultBuffer.GetAddressOf())));
		
	// In order to copy CPU memory data into our default buffer, we need
	// to create an intermediate upload heap.
	ThrowIfFailed(device->CreateCommittedResource(
		&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
		D3D12_HEAP_FLAG_NONE,
		&CD3DX12_RESOURCE_DESC::Buffer(byteSize),
		D3D12_RESOURCE_STATE_GENERIC_READ,
		nullptr,
		IID_PPV_ARGS(uploadBuffer.GetAddressOf())));
		
	// Describe the data we want to copy into the default buffer.
	D3D12_SUBRESOURCE_DATA subResourceData = {};
	subResourceData.pData = initData;
	subResourceData.RowPitch = byteSize;
	subResourceData.SlicePitch = subResourceData.RowPitch;
	
	// Schedule to copy the data to the default buffer resource.
	// At a high level, the helper function UpdateSubresources
	// will copy the CPU memory into the intermediate upload heap.
	// Then, using ID3D12CommandList::CopySubresourceRegion,
	// the intermediate upload heap data will be copied to mBuffer.
	cmdList->ResourceBarrier(1,
		&CD3DX12_RESOURCE_BARRIER::Transition(defaultBuffer.D3D12_RESOURCE_STATE_COMMON,
		D3D12_RESOURCE_STATE_COPY_DEST));
		
	UpdateSubresources<1>(cmdList,
		defaultBuffer.Get(), uploadBuffer.Get(),
		0, 0, 1, &subResourceData);
		
	cmdList->ResourceBarrier(1,
		&CD3DX12_RESOURCE_BARRIER::Transition(defaultBuffer.D3D12_RESOURCE_STATE_COPY_DEST,
		D3D12_RESOURCE_STATE_GENERIC_READ));
		
	// Note: uploadBuffer has to be kept alive after the above function
	// calls because the command list has not been executed yet that
	// performs the actual copy.
	// The caller can Release the uploadBuffer after it knows the copy
	// has been executed.
	
	return defaultBuffer;
}

D3D12_SUBRESOURCE_DATA结构体定义如下:

typedef struct D3D12_SUBRESOURCE_DATA
{
	const void *pData;
	LONG_PTR RowPitch;
	LONG_PTR SlicePitch;
} D3D12_SUBRESOURCE_DATA;
  1. pData:指向包含缓冲中需要初始化数据的内存的指针,如果该缓冲可以保存n个顶点,那么内存数组至少也要有n个顶点的内存;
  2. RowPitch:对于缓冲来说,是我们要复制的数据的大小;
  3. SlicePitch:对于缓冲来说,是我们要复制的数据的大小;

下面的代码展示了该类使用的一个例子:

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

const UINT64 vbByteSize = 8 * sizeof(Vertex);
ComPtr<ID3D12Resource> VertexBufferGPU = nullptr;
ComPtr<ID3D12Resource> VertexBufferUploader = nullptr;

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

顶点的定义如下:

struct Vertex
{
	XMFLOAT3 Pos;
	XMFLOAT4 Color;
};

为了绑定顶点缓冲到渲染管线,我们需要创建一个顶点缓冲描述(vertex buffer view)。和RTV(render target view)不同,我们不需要为顶点缓冲描述创建描述堆(descriptor heap),它可以通过D3D12_VERTEX_BUFFER_VIEW_DESC结构来表示:

typedef struct D3D12_VERTEX_BUFFER_VIEW
{
	D3D12_GPU_VIRTUAL_ADDRESS BufferLocation;
	UINT SizeInBytes;
	UINT StrideInBytes;
} D3D12_VERTEX_BUFFER_VIEW;
  1. BufferLocation:需要创建的描述的虚拟地址,可以使用ID3D12Resource::GetGPUVirtualAddress方法来获取;
  2. SizeInBytes:从BufferLocation开始,描述需要的字符数;
  3. StrideInBytes:每个顶点元素的大小,单位是字节;

当我们创建好一个顶点缓冲,并为它创建好描述后,我们可以把它绑定到渲染管线的一个输入槽,用以将顶点数据输入到输入阶段;这个过程可以使用下面函数完成:

void ID3D12GraphicsCommandList::IASetVertexBuffers(
	UINT StartSlot,
	UINT NumBuffers,
	const D3D12_VERTEX_BUFFER_VIEW *pViews);
  1. StartSlot:输入槽的序号(0~15);
  2. NumBuffers:需要绑定的顶点缓冲的数量,如果开始序号是k,要绑定n个,那么绑定的序号为k,k+1…;
  3. pViews:指向第一个顶点缓冲描述的指针;

下面是一个调用的例子:

D3D12_VERTEX_BUFFER_VIEW vbv;

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

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

一个顶点缓冲将会保持在输入的槽,知道它被改变,所以你的代码应该类似下面:

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

D3D12_VERTEX_BUFFER_VIEW_DESC mVBView1; // view to mVB1
D3D12_VERTEX_BUFFER_VIEW_DESC mVBView2; // view to mVB2
/*…Create the vertex buffers and views…*/

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

设置顶点缓冲到输入槽并没有开始绘制,它只是让顶点做好输入到渲染管线的准备,最终实际的渲染步骤是由ID3D12GraphicsCommandList::DrawInstanced方法完成:

void ID3D12CommandList::DrawInstanced(
	UINT VertexCountPerInstance,
	UINT InstanceCount,
	UINT StartVertexLocation,
	UINT StartInstanceLocation);
  1. VertexCountPerInstance:需要绘制的顶点的数量;
  2. InstanceCount:应用于实例化技术,这里先设置为1;
  3. StartVertexLocation:指明开始的第一个顶点;
  4. StartInstanceLocation:应用于实例化技术,这里先设置为0;

VertexCountPerInstance和StartVertexLocation参数定义了绘制那些顶点:
在这里插入图片描述

DrawInstanced方法中并没有指明拓扑结构,它由下面的方法中指定:

cmdList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);


3 索引和索引缓冲(INDEX BUFFERS)

和顶点一样,为了让GPU能访问到索引数据,我们需要创建一个索引缓冲和上传缓冲(Upload Buffer)。因为d3dUtil::CreateDefaultBuffer函数的数据是void*类型,所以可以直接使用它;为了绑定索引缓冲到渲染流水线,我们需要创建一个缓冲描述,和顶点一样,不需要描述堆;一个索引缓冲描述可以使用D3D12_INDEX_BUFFER_VIEW结构来表示:

typedef struct D3D12_INDEX_BUFFER_VIEW
{
	D3D12_GPU_VIRTUAL_ADDRESS BufferLocation;
	UINT SizeInBytes;
	DXGI_FORMAT Format;
} D3D12_INDEX_BUFFER_VIEW;
  1. BufferLocation:索引缓冲资源的虚拟地址,我们使用ID3D12Resource::GetGPUVirtualAddress来获取;
  2. SizeInBytes:从BufferLocation开始占用的字节数;
  3. Format:DXGI_FORMAT_R16_UINT或者DXGI_FORMAT_R32_UINT,正常情况下使用16位来减少内存和带宽,除非真的有需要使用32位;

和顶点一样,使用前需要绑定到流水线,可以使用ID3D12CommandList::SetIndexBuffer方法绑定到输入阶段:

std::uint16_t indices[] = {
	// front face
	0, 1, 2,
	0, 2, 3,
	// back face
	4, 6, 5,
	4, 7, 6,
	// left face
	4, 5, 1,
	4, 1, 0,
	// right face
	3, 2, 6,
	3, 6, 7,
	// top face
	1, 5, 6,
	1, 6, 2,
	// bottom face
	4, 0, 3,
	4, 3, 7
};

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

ComPtr<ID3D12Resource> IndexBufferGPU = nullptr;
ComPtr<ID3D12Resource> IndexBufferUploader = nullptr;

IndexBufferGPU = d3dUtil::CreateDefaultBuffer(md3dDevice.Get(),
	mCommandList.Get(), indices), ibByteSize,
	Index
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值