Bootstrap

DirectX12(D3D12)基础教程一 “你好三角形”

准备工作

检测系统显卡信息使用命令行运行: dxdiag

显卡-功能级别:12_x 表明硬件支持 d3d12功能。

推荐使用VS2019开发工具,依赖10.0.19041.0 sdk ,如使用vs2017则单独下载10.0.19041.0 sdk并安装。10.0.19041.0 sdk下载Windows SDK和模拟器存档 | Microsoft Developer

工程sdk设置如下:

创建win32程序并配置DirectX12

本程序使用vs2017+10.0.19041.0 sdk实现,新建win32工程:

2.设置sdk:

3. 运行:

接下来配置DirectX12,下载DirectX-Headers :GitHub - microsoft/DirectX-Headers: Official DirectX headers available under an open source license

把DirectX-Headers-main\include\directx目录加到工程

引入DirectX12头文件和lib文件

到这 配置d3d12环境工作都完成。

 调用D3D12接口创建设备对象 

这个一样复杂的过程,因此对d3d12接口操作做封装成三角形类对外有三个接口:

1.OnInit()放在win32窗口初始化时调用

2. 把OnRender()放在win32窗口绘画时调用,把OnDestroy()放在win32窗口关闭时调用:

接下来我们使用d3d12接口绘画一个三角形最终效果如下:

程序结构流程图如下:

我们将流程分为两个部份1.初始化和渲染

初始化:

      1. 创建D3D设备对象.

 查找系统显示适配器(显卡)设备创建D3D设备接口(gpu device), 使用CreateDXGIFactory2创建IDXGIFactory对象,然后IDXGIFactory对象枚举系统显卡设备,最后使用D3D12CreateDevice创建我们D3D设备对象.对应流程图1、2步,代码如下:

void CD3D12Triangle::CreateDevice(UINT dxgiFactoryFlags, ComPtr<IDXGIFactory6>& factory, ComPtr<ID3D12Device>& device)
{
	ComPtr<IDXGIAdapter1> adapter;
	HRESULT hr = S_OK;
	ThrowIfFailed(CreateDXGIFactory2(dxgiFactoryFlags, IID_PPV_ARGS(&factory)));
	
	for (int i = 0; SUCCEEDED(factory->EnumAdapterByGpuPreference(i, DXGI_GPU_PREFERENCE_HIGH_PERFORMANCE, IID_PPV_ARGS(&adapter))); i++)
	{
		DXGI_ADAPTER_DESC1 desc;
		adapter->GetDesc1(&desc);
		if (desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE)
		{
			continue;
		}

		if (SUCCEEDED(D3D12CreateDevice(adapter.Get(), D3D_FEATURE_LEVEL_12_0, _uuidof(ID3D12Device), nullptr)))
		{
			SetWindowText(m_hWnd, desc.Description);
			ThrowIfFailed(D3D12CreateDevice(adapter.Get(), D3D_FEATURE_LEVEL_12_0, IID_PPV_ARGS(&device)));
			break;
		}
	}

	///ThrowIfFailed(factory->MakeWindowAssociation(m_hWnd, DXGI_MWA_NO_ALT_ENTER));
}

     2.  创建D3D12命令队列接口:

使用D3D设备对象创建,加上ThrowIfFailed()作用是:如出错直接退出程序

void CD3D12Triangle::CreateCommandQueue(ComPtr<ID3D12CommandQueue>& commandQueue)
{
	D3D12_COMMAND_QUEUE_DESC queueDesc = {};
	queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
	queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;

	ThrowIfFailed(m_device->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&commandQueue)));
}

D3D12_COMMAND_QUEUE_DESC 是一个结构体 在D3D12被称为描述符,在D3D12以结尾xxx_DESC的结构体被称为xxx描述符,作用简化函数参数调(严格说是不对的,暂时先这样理解描述符)。再来看看什么是描述符堆是以xxx_HEAP_DESC以结尾的结构体. 位于gpu显存上。

3. 创建交换链(Swap Chain)

   交换链的概念:指的是一系列的表面组成的一个合集,这些表面中有一个是前台表面(显示在屏幕上)只要两个就可以了一个后台缓冲表面,一个前台表面。交换即前台表面变成后台缓冲表面,后台缓冲表面变成前台表面。在后台缓冲表面先绘图操作,当下一次需要显示画面的时候,这两个表面再次交换,这也叫做双缓冲。使用IDXGIFactory6对象创建:

void CD3D12Triangle::CreateSwapChain(ID3D12CommandQueue* pQueue, int w, int h, ComPtr<IDXGISwapChain3>& swapChain3)
{

	m_viewport = CD3DX12_VIEWPORT(0.0f, 0.0f, static_cast<float>(w), static_cast<float>(h));
	m_scissorRect = CD3DX12_RECT(0, 0, static_cast<LONG>(w), static_cast<LONG>(h));

	DXGI_SWAP_CHAIN_DESC1 swapChainDesc = {};
	swapChainDesc.BufferCount = m_nFrameCount;
	swapChainDesc.Width = w;
	swapChainDesc.Height = h;
	swapChainDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
	swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
	swapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
	swapChainDesc.SampleDesc.Count = 1;
	ComPtr<IDXGISwapChain1> swapChain;
	
	ThrowIfFailed(m_factory->CreateSwapChainForHwnd(
		pQueue,       
		m_hWnd,
		&swapChainDesc,
		nullptr,
		nullptr,
		&swapChain
	));
	ThrowIfFailed(swapChain.As(&swapChain3));
	m_frameIndex = swapChain3->GetCurrentBackBufferIndex();
}

交换链的创建依赖1.窗口句柄 2.命令队列 3.交换链描述符(交换链结构体)填值如下:

	DXGI_SWAP_CHAIN_DESC1 swapChainDesc = {};  ///交换链描述符
	swapChainDesc.BufferCount = m_nFrameCount;  //数值为2 双缓冲
	swapChainDesc.Width = w;    //窗口的宽
	swapChainDesc.Height = h;   //窗口的高
	swapChainDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; 
	swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
	swapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
	swapChainDesc.SampleDesc.Count = 1;

ThrowIfFailed(swapChain.As(&swapChain3)); 目的是将IDXGISwapChain1转换成IDXGISwapChain3 为使用 IDXGISwapChain3的GetCurrentBackBufferIndex()接口。

3. 创建RTV(Render Target View)

    Render Target View(简称RTV)可以作为渲染目标.它的创建依赖于1 RTV描述符堆 2.交换链表面关连 可见程序结构流程图编号5。

1.RTV描述符堆的创建 填充D3D12_DESCRIPTOR_HEAP_DESC 结构体,然后调用CreateDescriptorHeap创建ID3D12DescriptorHeap(RTV描述符堆)代码:

void CD3D12Triangle::CreateRenderTargetViewHeap(ComPtr<ID3D12DescriptorHeap>& heap)
{
	D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc = {};
	rtvHeapDesc.NumDescriptors = m_nFrameCount;
	rtvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;
	rtvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
	ThrowIfFailed(m_device->CreateDescriptorHeap(&rtvHeapDesc, IID_PPV_ARGS(&heap)));
}

2.创建RTV描述符:

void CD3D12Triangle::CreateRenderTargetView(ComPtr<ID3D12Resource> rtv[])
{
	
	CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(m_rtvHeap->GetCPUDescriptorHandleForHeapStart());
	
	m_rtvDescriptorSize = m_device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);

	// Create a RTV for each frame.
	for (int i = 0; i < m_nFrameCount; i++)
	{
		ThrowIfFailed(m_swapChain->GetBuffer(i, IID_PPV_ARGS(&rtv[i])));
		m_device->CreateRenderTargetView(rtv[i].Get(), nullptr, rtvHandle);
		rtvHandle.Offset(1, m_rtvDescriptorSize);
	}
	 
}

通过 RTV描述符堆对象取到RTV句柄,和RTV描述符的大小,可以这样理解 数组: A a[2]={0} a数组是全局的,我们要知道a[0]的大小通过GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV)得到(像sizeof(a[0])),访问数组a变量通过GetCPUDescriptorHandleForHeapStart()得到。

ThrowIfFailed(m_swapChain->GetBuffer(i, IID_PPV_ARGS(&rtv[i]))) RTV描述符它其实是交换链表面,m_device->CreateRenderTargetView(rtv[i].Get(), nullptr, rtvHandle)创建RTV描述符与交换链表面关连起来。rtvHandle.Offset(1, m_rtvDescriptorSize)维护RTV描述符堆数组。

4.创建根签名对象接口

根签名在d3d是一个重要的概念,目前我们只要了解如何使用就好,像使用函数一样先声明:

void CD3D12Triangle::CreateRootSignature(ComPtr<ID3D12RootSignature>& rootSignature)
{
	CD3DX12_ROOT_SIGNATURE_DESC rootSignatureDesc;
	rootSignatureDesc.Init(0, nullptr, 0, nullptr, D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);

	ComPtr<ID3DBlob> signature;
	ComPtr<ID3DBlob> error;
	ThrowIfFailed(D3D12SerializeRootSignature(&rootSignatureDesc, D3D_ROOT_SIGNATURE_VERSION_1, &signature, &error));
	ThrowIfFailed(m_device->CreateRootSignature(0, signature->GetBufferPointer(), signature->GetBufferSize(), IID_PPV_ARGS(&rootSignature)));

}

   5. 编译Shader程序 

void CD3D12Triangle::ShadersCompileFromFile(LPCWSTR pFileName, LPCSTR pEntrypoint, LPCSTR pTarget, ComPtr<ID3DBlob>& shader)
{
	UINT compileFlags = D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION;
	ThrowIfFailed(D3DCompileFromFile(pFileName, nullptr, nullptr, pEntrypoint, pTarget, compileFlags, 0, &shader, nullptr));
}

调用 D3DCompileFromFile 函数这里简单的封装一下,d3d中最少要有两个Shader程序  顶点着色器(Vertex Shader)和片段着色器(Fragment Shader) ,用结构体声明然后绑定到Pipeline State Object渲染管线状态对象

	D3D12_INPUT_ELEMENT_DESC inputElementDescs[] =
	{
        //顶点着色器 声明
		{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
        //片段着色器 声明
		{ "COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 12, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }
	};

    

6. 创建Pipeline State Object(PSO 渲染管线状态对象)

PSO 渲染管线状态对象是复杂而重要的概念,这里也不作说明,先了解如何使用,可以理解成使用gpu绘画时,根签名是函数的声明,PSO 渲染管线状态对象则是函数的定义。填充D3D12_GRAPHICS_PIPELINE_STATE_DESC结构体然后用CreateGraphicsPipelineState创建,创建PSO 渲染管线状态对象, 主要依赖

1输入数据布局

2顶点着色器Shader程序

3片段着色器Shader程序 

4根签名上步已创建

5其他项填默认值   代码如下:

void CD3D12Triangle::CreateGPUPipelineState(ComPtr<ID3D12PipelineState>& pipelineState, ComPtr<ID3D12CommandAllocator>& commandAllocator, ComPtr<ID3D12GraphicsCommandList>& commandList)
{
	ComPtr<ID3DBlob> vertexShader;
	ComPtr<ID3DBlob> pixelShader;
    /// 编译 顶点着色器
	ShadersCompileFromFile(GetShaderFilePath(L"Shader.hlsl").c_str(), "VSMain", "vs_5_0", vertexShader);
    /// 编译 片段着色器
	ShadersCompileFromFile(GetShaderFilePath(L"Shader.hlsl").c_str(), "PSMain", "ps_5_0", pixelShader); 
	/// 声明 顶点着色器 片段着色器 输入数据布局 结构
	D3D12_INPUT_ELEMENT_DESC inputElementDescs[] =
	{
		{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
		{ "COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 12, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }
	};

	D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc = {};
	psoDesc.InputLayout = { inputElementDescs, _countof(inputElementDescs) };  ///  顶点着色器 片段着色器 数据布局 结构
	psoDesc.pRootSignature = m_rootSignature.Get();  ///  根签名
	psoDesc.VS = CD3DX12_SHADER_BYTECODE(vertexShader.Get());  ///  顶点着色器 Shader程序
	psoDesc.PS = CD3DX12_SHADER_BYTECODE(pixelShader.Get());   ///  片段着色器 Shader程序
	psoDesc.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT);
	psoDesc.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT);
	psoDesc.DepthStencilState.DepthEnable = FALSE;
	psoDesc.DepthStencilState.StencilEnable = FALSE;
	psoDesc.SampleMask = UINT_MAX;
	psoDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
	psoDesc.NumRenderTargets = 1;
	psoDesc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM;
	psoDesc.SampleDesc.Count = 1;
	ThrowIfFailed(m_device->CreateGraphicsPipelineState(&psoDesc, IID_PPV_ARGS(&pipelineState)));

	ThrowIfFailed(m_device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&commandAllocator)));
	ThrowIfFailed(m_device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, m_commandAllocator.Get(), m_pipelineState.Get(), IID_PPV_ARGS(&commandList)));
	ThrowIfFailed(commandList->Close());

}

7.初始化顶点着色器和片段着色器数据

也就是把cpu内存数据 提交到gpu显卡内存上,使用CreateCommittedResource创建资源,D3D12_HEAP_TYPE_UPLOAD说明是 cpu数据到gpu内存,Map->memcpy->Unmap三步进行提交数据,代码:

void CD3D12Triangle::InitializeVertexBuffer(ComPtr<ID3D12Resource>& vertexBuffer,D3D12_VERTEX_BUFFER_VIEW& vertexBufferView)
{
	Vertex triangleVertices[] =
	{
		{ { -0.5, -0.5, 0.0f }, { 1.0f, 0.0f, 0.0f, 1.0f } },
		{ { -0.5, 0.5, 0.0f }, { 0.0f, 1.0f, 0.0f, 1.0f } },
		{ { 0.5, -0.5, 0.0f }, { 0.0f, 1.0f, 0.0f, 1.0f } },
		///{ { 0.5, 0.5, 0.0f }, { 0.0f, 0.0f, 1.0f, 1.0f } },

	};

	const UINT vertexBufferSize = sizeof(triangleVertices);

	ThrowIfFailed(m_device->CreateCommittedResource(
		&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
		D3D12_HEAP_FLAG_NONE,
		&CD3DX12_RESOURCE_DESC::Buffer(vertexBufferSize),
		D3D12_RESOURCE_STATE_GENERIC_READ,
		nullptr,
		IID_PPV_ARGS(&vertexBuffer)));

	UINT8* pVertexDataBegin = NULL;
	CD3DX12_RANGE readRange(0, 0);        
	ThrowIfFailed(vertexBuffer->Map(0, &readRange, reinterpret_cast<void**>(&pVertexDataBegin)));
	memcpy(pVertexDataBegin, triangleVertices, sizeof(triangleVertices));
	vertexBuffer->Unmap(0, nullptr);

	vertexBufferView.BufferLocation = vertexBuffer->GetGPUVirtualAddress();
	vertexBufferView.StrideInBytes = sizeof(Vertex);
	vertexBufferView.SizeInBytes = vertexBufferSize;

}

Vertex  triangleVertices[]={...} 数组中float值是啥意思?它们分别代表了 三角形顶点坐标,三角形顶点背景色RGBA (1f,1f,1f,1f)表示白色,(0f,0f,0f,1f)表示黑色。

d3d12坐标系统 取值范围 -1到1,以中心为原点如图:

8. 创建命令列表接口 

void CD3D12Triangle::CreateGPUPipelineState(ComPtr<ID3D12PipelineState>& pipelineState, ComPtr<ID3D12CommandAllocator>& commandAllocator, ComPtr<ID3D12GraphicsCommandList>& commandList)
{
	
        / 创建命令列表分配器

	ThrowIfFailed(m_device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&commandAllocator)));
       创命令列表
	ThrowIfFailed(m_device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, m_commandAllocator.Get(), m_pipelineState.Get(), IID_PPV_ARGS(&commandList)));
	ThrowIfFailed(commandList->Close());

}

命令列表依赖 1令列表分配器 2PSO对象

9. 创建围栏接口

创建围栏目的是为CPU与GPU渲染间的同步,简单的说窗口刷新(CPU)要等GPU渲染线程完成,再进入下次窗口刷新,用事件做为同步信号,代码:

void CD3D12Triangle::CreateCpuAndGpuSynchronization()
{
	ThrowIfFailed(m_device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&m_fence)));
	m_fenceValue = 1;

	m_fenceEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);
	if (m_fenceEvent == nullptr)
	{
		ThrowIfFailed(HRESULT_FROM_WIN32(GetLastError()));
	}
}

等GPU渲染线程完成代码:

void CD3D12Triangle::WaitForPreviousFrame(void)
{
	const UINT64 fence = m_fenceValue;
	ThrowIfFailed(m_commandQueue->Signal(m_fence.Get(), fence));
	m_fenceValue++;

	// Wait until the previous frame is finished.
	if (m_fence->GetCompletedValue() < fence)
	{
		ThrowIfFailed(m_fence->SetEventOnCompletion(fence, m_fenceEvent));
		WaitForSingleObject(m_fenceEvent, INFINITE);
	}

	m_frameIndex = m_swapChain->GetCurrentBackBufferIndex();
}

到这里所有的创建对象工作都完了,接下来是渲染。

调用D3D12接口渲染

复杂的对象创建完成后,渲染相对来说就简单很多了

第一步使用命令列表接口设置预定操作和设置资源屏障(还没有执行,只是预定操作)

第二步使用命令队列接口 ExecuteCommandLists 执行操作

第三步使用交换链接口刷新Present(1, 0)

第四步等GPU渲染完成后到第一步操作

预定操作和设置资源屏障代码:

void CD3D12Triangle::PopulateCommandList(void)
{
	ThrowIfFailed(m_commandAllocator->Reset());
	ThrowIfFailed(m_commandList->Reset(m_commandAllocator.Get(), m_pipelineState.Get()));

	
	m_commandList->SetGraphicsRootSignature(m_rootSignature.Get());
	m_commandList->RSSetViewports(1, &m_viewport);
	m_commandList->RSSetScissorRects(1, &m_scissorRect);

	
	m_commandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(m_renderTargets[m_frameIndex].Get(), D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_RENDER_TARGET));

	CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(m_rtvHeap->GetCPUDescriptorHandleForHeapStart(), m_frameIndex, m_rtvDescriptorSize);
	m_commandList->OMSetRenderTargets(1, &rtvHandle, FALSE, nullptr);

	
	const float clearColor[] = { 0.0f, 0.2f, 0.4f, 1.0f };
	m_commandList->ClearRenderTargetView(rtvHandle, clearColor, 0, nullptr);						 
	m_commandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP);
	m_commandList->IASetVertexBuffers(0, 1, &m_vertexBufferView);
	m_commandList->DrawInstanced(3, 1, 0, 0);


	m_commandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(m_renderTargets[m_frameIndex].Get(), D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT));

	ThrowIfFailed(m_commandList->Close());
}

窗口刷新代码:

void CD3D12Triangle::OnRender(void)
{

	PopulateCommandList();

	ID3D12CommandList* ppCommandLists[] = { m_commandList.Get() };
	m_commandQueue->ExecuteCommandLists(_countof(ppCommandLists), ppCommandLists);

	
	ThrowIfFailed(m_swapChain->Present(1, 0));

	WaitForPreviousFrame();
}

工程代码

【免费】DirectX12学习记录资源

;