由于网上很多文章已经写的很好了,重复的内容也很多,又是第一次做DX12渲染器,难免会有一些做的不太好的地方。先贴一下对我很有帮助的专栏Windows渲染引擎开发入门教学(1): HelloWorld – 知乎,之前有一些概念非常模糊,即便是看了很多遍龙书也依旧费解的问题,麦老师三言两语就讲清楚了,非常感谢。 自我感觉看龙书的最大问题是,龙书一下子塞了太多内容了,从第四章初始化就在上难度,在还没有理清各个概念的时候去看龙书的配套资源,一口气塞了个封装好的代码需要不停跳转学习,非常容易混乱。概念这种事情没有办法,只能反复再反复直到吃透(虽然到目前为止我仍旧没有吃透)。我是直接看的中译版,虽然里面有些地方有些小错,但是阅读体验总体来说还算是不错。下面提页数的时候都是按照中译版来的。
创建项目这里就不多说了,注意不要创建成控制台应用,从头开始创建窗口的操作非常推荐看一下浅墨的《Windows游戏编程之从零开始》,看懂之后再来操作。环境还是参考的麦老师的文章,这里我就不多介绍了。由于创建窗口是固定操作,直接贴下代码。这一篇文章都会在一个main文件里创建,后面再慢慢封装,不要急。(icon是我自己随手画的,可以随便找张图塞进去,不想塞图标的话把icon那行删掉不碍事的。)
这一篇结束我们可以学会搭一个简易渲染器并绘制出一个三角形
一、窗口创建
#include <Windows.h>
UINT width = 800;
UINT height = 600;
HWND hwnd;
//窗口过程函数
LRESULT CALLBACK WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_DESTROY://窗口销毁消息
PostQuitMessage(0);//向系统表明有个线程有终止请求。用来响应 WM_DESTROY消息
return 0;
}
return DefWindowProc(hWnd, message, wParam, lParam);//调用默认的窗口过程来为应用程序没有处理的窗口消息提供默认的处理。
}
int CALLBACK WinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPSTR lpCmdLine, _In_ int nShowCmd)
{
//窗口类的设计
//开始设计一个完整的窗口类
//用WINDCLASSEX定义了一个窗口类,即用wndClass实例化了WINCLASSEX,用于之后窗口的各项初始化
WNDCLASSEX windowClass = { 0 };
//设置结构体的字节数大小
windowClass.cbSize = sizeof(WNDCLASSEX);
//设置窗口的样式
windowClass.style = CS_HREDRAW | CS_VREDRAW;
//指向窗口过程函数的指针
windowClass.lpfnWndProc = WindowProc;
//指定包含窗口过程的程序的实例
windowClass.hInstance = hInstance;
//指定窗口类的光标句柄
windowClass.hCursor = LoadCursor(NULL, IDC_ARROW);
//从全局的::LoadImage函数从本地加载自定义ico图标
windowClass.hIcon = (HICON)::LoadImage(NULL, L"icon.ico", IMAGE_ICON, 0, 0, LR_DEFAULTSIZE | LR_LOADFROMFILE);
//用一个以空终止的字符串,指定窗口类的名字
windowClass.lpszClassName = L"RenderClass";
//窗口类的注册
RegisterClassEx(&windowClass);
//窗口的正式创建
hwnd = CreateWindow(
windowClass.lpszClassName,
L"饼子的渲染器ヾ(≧▽≦*)o",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT,
width,
height,
nullptr,
nullptr,
hInstance,
nullptr);
//显示窗口
ShowWindow(hwnd, SW_SHOW);
MSG msg = {};//定义并初始化消息
while (msg.message != WM_QUIT)//不断从消息队列中取出消息
{
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
TranslateMessage(&msg);//将虚拟键消息转换为字符消息
DispatchMessage(&msg);//分发一个消息给窗口程序
}
}
return 0;
}
然后到了非常关键的创建资源和命令了。强烈建议多看几遍龙书,多看。 接下来我们开始创建管线资源(不会一口气创建完,我们先看下整个管线运行起来需要哪些东西)。思维导图如下,想要整个管线运行起来,我们需要命令队列三件套:CommandList, CommandAllocator, CommandQueue。为了防止资源覆写导致的错误,我们需要创建fence。(具体见龙书p94到p99) 具体操作是,我们先向CommandList中录制命令,然后上传至CommandAllocator,最后再Execute到CommandQueue中提交指令。为什么需要CommandAllocator呢?引用下大麦老师的原文: “那么为何要多一个Command Allocator,书中提到Allocator是用来给CommandList分配内存的,那为何不可以每个CommandList都开一个Allocator,或者全局所有的CommandList都使用同一个Allocator呢?其实Allocator同样是为了兼顾性能和方便设计出来的。如果有自己实现过内存分配练习,就可以知道这个过程很难在多线程下做到完全无锁无等待,因此如果全局共享同一个Allocator,毫无疑问CPU会将性能浪费在锁的等待上,这对性能要求极高的实时渲染来说,是无法接受的。那么为何不让每个CommandList都有一个自己的Allocator?这则是为了空间考虑,从驱动实现者的角度考虑,每个Allocator都需要构建一个独立的分配池和页,这会让中间有许多气泡,尤其是许多渲染器的架构设计会把每个小Pass都做成一个独立的CommandList,所以可能会有多个CommandList都在同一个线程被录制完成,在这种情况下开多个Allocator完全没有必要,因此我们可以把Allocator做成每个线程固定留一个,而线程内则可以录制多个CommandList,录制结束后分别按需提交。”
这里我们提交的指令只有一个,就是让交换链中的前后缓冲区资源进行交换,因此我们需要创建出对应的资源。思维导图如下:
我们会根据右图的需要,创建左图的资源。这里显示的都是GPU中需要的资源,有时需要CPU中的资源辅助,比如HANDLE fenceEvent;
在图中没有标注,具体可以看代码来理解。当renderTargets[0]和renderTargets[1]需要资源转换时,是通过ResoueceBarrier标记状态的。因此ResoueceBarrier是CommandList来创建的。
因为DX12不像控制台会直接把报错输出,首先先把debug开了。写在最前面
std::string HrToString(HRESULT hr)
{
char s_str[64] = {};
sprintf_s(s_str, "HRESULT of 0x%08X", static_cast<UINT>(hr));
return std::string(s_str);
}
class HrException : public std::runtime_error
{
public:
HrException(HRESULT hr) : std::runtime_error(HrToString(hr)), m_hr(hr) {}
HRESULT Error() const { return m_hr; }
private:
const HRESULT m_hr;
};
void ThrowIfFailed(HRESULT hr)
{
if (FAILED(hr))
{
throw HrException(hr);
}
}
开始创建资源。 需要包含的文件和名称空间。这里需要去官方下载一下d3dx12文件并包含进来。
#include <Windows.h>
#include <d3d12.h>
#include <dxgi1_6.h>
#include <D3Dcompiler.h>
#include <DirectXMath.h>
#include "Common/d3dx12.h"
#include <wrl.h>
#include <iostream>
#include <string>
#include <wrl.h>
#include <shellapi.h>
#pragma comment(lib, "d3d12.lib")
#pragma comment(lib, "dxgi.lib")
using namespace Microsoft::WRL;
我们需要的公用对象如下
const UINT FrameCount = 2;
UINT width = 800;
UINT height = 600;
HWND hwnd;
ComPtr<ID3D12Device> device;
ComPtr<ID3D12CommandQueue> commandQueue;
ComPtr<IDXGISwapChain3> swapChain;
ComPtr<ID3D12Resource> renderTargets[FrameCount];
ComPtr<ID3D12DescriptorHeap> rtvHeap;
ComPtr<ID3D12CommandAllocator> commandAllocator;
ComPtr<ID3D12GraphicsCommandList> commandList;
ComPtr<ID3D12PipelineState> pipelineState;
UINT rtvDescriptorSize;
UINT frameIndex;
HANDLE fenceEvent;
ComPtr<ID3D12Fence> fence;
UINT64 fenceValue;
二、搭建一个简陋版本的渲染管线
1.枚举适配器
适配器具体概念见龙书89页。你可以理解为显卡,创建工厂和设备时需要它。
DXGIAdapter1* GetSurpportedAdapter(ComPtr<IDXGIFactory4>& factory, const D3D_FEATURE_LEVEL featurelevel)
{
IDXGIAdapter1* adapter = nullptr;
for (std::uint32_t adapterIndex = 0U; ; ++adapterIndex)
{
IDXGIAdapter1* currentAdapter = nullptr;
if (DXGI_ERROR_NOT_FOUND == factory->EnumAdapters1(adapterIndex, ¤tAdapter))
{
break;
}
const HRESULT hr = D3D12CreateDevice(currentAdapter, featurelevel, _uuidof(ID3D12Device), nullptr);
if (SUCCEEDED(hr))
{
adapter = currentAdapter;
break;
}
currentAdapter->Release();
}
return adapter;
}
2.加载管线资源
首先还是打开debug
void LoadPipeline()
{
#if defined(_DEBUG)
{
ComPtr<ID3D12Debug> debugController;
if (SUCCEEDED(D3D12GetDebugInterface(IID_PPV_ARGS(&debugController))))
{
debugController->EnableDebugLayer();
}
}
#endif
创建工厂
ComPtr<IDXGIFactory4> mDxgiFactory;
ThrowIfFailed(CreateDXGIFactory1(IID_PPV_ARGS(mDxgiFactory.GetAddressOf())));
创建适配器
// 枚举适配器
D3D_FEATURE_LEVEL featurelevels[] =
{
D3D_FEATURE_LEVEL_12_1,
D3D_FEATURE_LEVEL_12_0,
D3D_FEATURE_LEVEL_11_1,
D3D_FEATURE_LEVEL_11_0
};
// 创建适配器
IDXGIAdapter1* adapter = nullptr;
for (std::uint32_t i = 0U; i <= _countof(featurelevels); ++i)
{
adapter = GetSurpportedAdapter(mDxgiFactory, featurelevels[i]);
if (adapter != nullptr)
{
break;
}
}
创建设备
// 创建设备
if (adapter != nullptr)
{
D3D12CreateDevice(adapter, D3D_FEATURE_LEVEL_12_1, IID_PPV_ARGS(device.GetAddressOf()));
}
创建命令队列
// 创建命令队列
D3D12_COMMAND_QUEUE_DESC queueDesc = {};
queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
ThrowIfFailed(device->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&commandQueue)));
创建交换链
// 创建交换链
DXGI_SWAP_CHAIN_DESC1 swapChainDesc = {};
swapChainDesc.BufferCount = FrameCount;
swapChainDesc.Width = width;
swapChainDesc.Height = height;
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> swapchain1;
ThrowIfFailed(mDxgiFactory->CreateSwapChainForHwnd(commandQueue.Get(),
hwnd, &swapChainDesc, nullptr, nullptr, &swapchain1));
ThrowIfFailed(swapchain1.As(&swapChain));
frameIndex = swapChain->GetCurrentBackBufferIndex();
创建RTV描述符堆
// 创建RTV描述符堆
D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc = { };
rtvHeapDesc.NumDescriptors = FrameCount;
rtvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;
rtvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
ThrowIfFailed(device->CreateDescriptorHeap(&rtvHeapDesc, IID_PPV_ARGS(&rtvHeap)));
rtvDescriptorSize = device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
创建RTV描述符
// 创建RTV描述符
CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(rtvHeap->GetCPUDescriptorHandleForHeapStart()); // 指向 CPU 可访问的描述符的句柄
for (UINT n = 0; n<FrameCount; n++)
{
ThrowIfFailed(swapChain->GetBuffer(n, IID_PPV_ARGS(&renderTargets[n])));
device->CreateRenderTargetView(renderTargets[n].Get(), nullptr, rtvHandle);
rtvHandle.Offset(1, rtvDescriptorSize); // 偏移几个,偏移夺少
}
创建命令分配器
// 创建命令分配器
ThrowIfFailed(device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&commandAllocator)));
}
以上,我们初始的管线资源就创建好了。
3.加载资源
接下来加载资源。因为我们只需要把缓冲区给推上去就行,所以加载资源这里只会创建list,fence和一个空event。这里创建一个空的event为后面的Flush()使用。具体怎么用后面会说。
// 加载资源
void LoadAsset()
{
ThrowIfFailed(device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, commandAllocator.Get(), nullptr, IID_PPV_ARGS(&commandList)));
ThrowIfFailed(commandList->Close());
// 以下为加载资源对象
ThrowIfFailed(device->CreateFence(fenceValue, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&fence)));
fenceValue = 1;
// 这里创建了一个空事件,为后面的 Flush() 使用
fenceEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);
if (fenceEvent == nullptr)
{
ThrowIfFailed(HRESULT_FROM_WIN32(GetLastError()));
}
}
4.录制命令
// 添加命令
void PopulateCommandList()
{
ThrowIfFailed(commandAllocator->Reset());
ThrowIfFailed(commandList->Reset(commandAllocator.Get(), pipelineState.Get()));
D3D12_RESOURCE_BARRIER resBarrier = CD3DX12_RESOURCE_BARRIER::Transition(renderTargets[frameIndex].Get(), D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_RENDER_TARGET);
commandList->ResourceBarrier(1, &resBarrier);
CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(rtvHeap->GetCPUDescriptorHandleForHeapStart(), frameIndex, rtvDescriptorSize);
const float clearColor[] = { 1.0f, 0.7f, 0.7f, 1.0f };
commandList->ClearRenderTargetView(rtvHandle, clearColor, 0, nullptr);
resBarrier = CD3DX12_RESOURCE_BARRIER::Transition(renderTargets[frameIndex].Get(), D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT);
commandList->ResourceBarrier(1, &resBarrier);
ThrowIfFailed(commandList->Close());
}
5.同步CPU与GPU资源
这里解释一下。我们首先把fenceValue
给一个临时变量tempFenceValue
上去,然后标记fence。如果GPU马上就完成这步那是极好的,可是GPU有可能还没有完成指令,也就是fence->GetCompletedValue() < tempFenceValue
时,就需要让CPU等待,也就是后面的方法。不过这个办法效率很低,我们暂时先用这种办法。
//同步CPU与GPU资源
void Flush()
{
const UINT64 tempFenceValue = fenceValue;
ThrowIfFailed(commandQueue->Signal(fence.Get(), tempFenceValue));
fenceValue++;
if (fence->GetCompletedValue() < tempFenceValue)
{
ThrowIfFailed(fence->SetEventOnCompletion(tempFenceValue, fenceEvent));
WaitForSingleObject(fenceEvent, INFINITE);
}
frameIndex = swapChain->GetCurrentBackBufferIndex();
}
4.OnRender()
这里不要和资源屏障搞混了。Present真正的交换前后缓冲区,ResoueceBarrier是用来标记资源状态的(比如同一个target转换成可写状态,这样才能写命令。)
void OnRender()
{
PopulateCommandList();
ID3D12CommandList* ppcommandLists[] = { commandList.Get() };
commandQueue->ExecuteCommandLists(_countof(ppcommandLists), ppcommandLists);
// 将渲染结果呈现到屏幕上,并交换前后缓冲区
ThrowIfFailed(swapChain->Present(1, 0));
Flush();
}
5.OnDestory
void OnDestroy()
{
Flush();
CloseHandle(fenceEvent);
}
6.窗口过程函数
因为我们这一次需要把目标缓冲区的资源绘制上去,所以需要改变窗口过程函数
//窗口过程函数
LRESULT CALLBACK WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_PAINT:
OnRender();
return 0;
case WM_DESTROY://窗口销毁消息
PostQuitMessage(0);//向系统表明有个线程有终止请求。用来响应 WM_DESTROY消息
return 0;
}
return DefWindowProc(hWnd, message, wParam, lParam);//调用默认的窗口过程来为应用程序没有处理的窗口消息提供默认的处理。
}
7.完整代码
需要在显示窗口之前加载渲染管线资源,最后需要销毁。具体代码如下
#include <Windows.h>
#include <d3d12.h>
#include <dxgi1_6.h>
#include <D3Dcompiler.h>
#include <DirectXMath.h>
#include "Common/d3dx12.h"
#include <wrl.h>
#include <iostream>
#include <string>
#include <wrl.h>
#include <shellapi.h>
#pragma comment(lib, "d3d12.lib")
#pragma comment(lib, "dxgi.lib")
using namespace Microsoft::WRL;
const UINT FrameCount = 2;
UINT width = 800;
UINT height = 600;
HWND hwnd;
ComPtr<ID3D12Device> device;
ComPtr<ID3D12CommandQueue> commandQueue;
ComPtr<IDXGISwapChain3> swapChain;
ComPtr<ID3D12Resource> renderTargets[FrameCount];
ComPtr<ID3D12DescriptorHeap> rtvHeap;
ComPtr<ID3D12CommandAllocator> commandAllocator;
ComPtr<ID3D12GraphicsCommandList> commandList;
ComPtr<ID3D12PipelineState> pipelineState;
UINT rtvDescriptorSize;
UINT frameIndex;
HANDLE fenceEvent;
ComPtr<ID3D12Fence> fence;
UINT64 fenceValue;
std::string HrToString(HRESULT hr)
{
char s_str[64] = {};
sprintf_s(s_str, "HRESULT of 0x%08X", static_cast<UINT>(hr));
return std::string(s_str);
}
class HrException : public std::runtime_error
{
public:
HrException(HRESULT hr) : std::runtime_error(HrToString(hr)), m_hr(hr) {}
HRESULT Error() const { return m_hr; }
private:
const HRESULT m_hr;
};
void ThrowIfFailed(HRESULT hr)
{
if (FAILED(hr))
{
throw HrException(hr);
}
}
IDXGIAdapter1* GetSurpportedAdapter(ComPtr<IDXGIFactory4>& factory, const D3D_FEATURE_LEVEL featurelevel)
{
IDXGIAdapter1* adapter = nullptr;
for (std::uint32_t adapterIndex = 0U; ; ++adapterIndex)
{
IDXGIAdapter1* currentAdapter = nullptr;
if (DXGI_ERROR_NOT_FOUND == factory->EnumAdapters1(adapterIndex, ¤tAdapter))
{
break;
}
const HRESULT hr = D3D12CreateDevice(currentAdapter, featurelevel, _uuidof(ID3D12Device), nullptr);
if (SUCCEEDED(hr))
{
adapter = currentAdapter;
break;
}
currentAdapter->Release();
}
return adapter;
}
// 加载管线资源
void LoadPipeline()
{
#if defined(_DEBUG)
{
ComPtr<ID3D12Debug> debugController;
if (SUCCEEDED(D3D12GetDebugInterface(IID_PPV_ARGS(&debugController))))
{
debugController->EnableDebugLayer();
}
}
#endif
// 创建工厂
ComPtr<IDXGIFactory4> mDxgiFactory;
ThrowIfFailed(CreateDXGIFactory1(IID_PPV_ARGS(mDxgiFactory.GetAddressOf())));
// 枚举适配器
D3D_FEATURE_LEVEL featurelevels[] =
{
D3D_FEATURE_LEVEL_12_1,
D3D_FEATURE_LEVEL_12_0,
D3D_FEATURE_LEVEL_11_1,
D3D_FEATURE_LEVEL_11_0
};
// 创建适配器
IDXGIAdapter1* adapter = nullptr;
for (std::uint32_t i = 0U; i <= _countof(featurelevels); ++i)
{
adapter = GetSurpportedAdapter(mDxgiFactory, featurelevels[i]);
if (adapter != nullptr)
{
break;
}
}
// 创建设备
if (adapter != nullptr)
{
D3D12CreateDevice(adapter, D3D_FEATURE_LEVEL_12_1, IID_PPV_ARGS(device.GetAddressOf()));
}
// 创建命令队列
D3D12_COMMAND_QUEUE_DESC queueDesc = {};
queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
ThrowIfFailed(device->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&commandQueue)));
// 创建交换链
DXGI_SWAP_CHAIN_DESC1 swapChainDesc = {};
swapChainDesc.BufferCount = FrameCount;
swapChainDesc.Width = width;
swapChainDesc.Height = height;
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> swapchain1;
ThrowIfFailed(mDxgiFactory->CreateSwapChainForHwnd(commandQueue.Get(),
hwnd, &swapChainDesc, nullptr, nullptr, &swapchain1));
ThrowIfFailed(swapchain1.As(&swapChain));
frameIndex = swapChain->GetCurrentBackBufferIndex();
// 创建RTV描述符堆
D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc = { };
rtvHeapDesc.NumDescriptors = FrameCount;
rtvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;
rtvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
ThrowIfFailed(device->CreateDescriptorHeap(&rtvHeapDesc, IID_PPV_ARGS(&rtvHeap)));
rtvDescriptorSize = device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
// 创建RTV描述符
CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(rtvHeap->GetCPUDescriptorHandleForHeapStart()); // 指向 CPU 可访问的描述符的句柄
for (UINT n = 0; n<FrameCount; n++)
{
ThrowIfFailed(swapChain->GetBuffer(n, IID_PPV_ARGS(&renderTargets[n])));
device->CreateRenderTargetView(renderTargets[n].Get(), nullptr, rtvHandle);
rtvHandle.Offset(1, rtvDescriptorSize); // 偏移几个,偏移夺少
}
// 创建命令分配器
ThrowIfFailed(device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&commandAllocator)));
}
// 加载资源
void LoadAsset()
{
ThrowIfFailed(device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, commandAllocator.Get(), nullptr, IID_PPV_ARGS(&commandList)));
ThrowIfFailed(commandList->Close());
// 以下为加载资源对象
ThrowIfFailed(device->CreateFence(fenceValue, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&fence)));
fenceValue = 1;
// 这里创建了一个空事件,为后面的 Flush() 使用
fenceEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);
if (fenceEvent == nullptr)
{
ThrowIfFailed(HRESULT_FROM_WIN32(GetLastError()));
}
}
// 添加命令
void PopulateCommandList()
{
ThrowIfFailed(commandAllocator->Reset());
ThrowIfFailed(commandList->Reset(commandAllocator.Get(), pipelineState.Get()));
D3D12_RESOURCE_BARRIER resBarrier = CD3DX12_RESOURCE_BARRIER::Transition(renderTargets[frameIndex].Get(), D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_RENDER_TARGET);
commandList->ResourceBarrier(1, &resBarrier);
CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(rtvHeap->GetCPUDescriptorHandleForHeapStart(), frameIndex, rtvDescriptorSize);
const float clearColor[] = { 1.0f, 0.7f, 0.7f, 1.0f };
commandList->ClearRenderTargetView(rtvHandle, clearColor, 0, nullptr);
resBarrier = CD3DX12_RESOURCE_BARRIER::Transition(renderTargets[frameIndex].Get(), D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT);
commandList->ResourceBarrier(1, &resBarrier);
ThrowIfFailed(commandList->Close());
}
//同步CPU与GPU资源
void Flush()
{
const UINT64 tempFenceValue = fenceValue;
ThrowIfFailed(commandQueue->Signal(fence.Get(), tempFenceValue));
fenceValue++;
if (fence->GetCompletedValue() < tempFenceValue)
{
ThrowIfFailed(fence->SetEventOnCompletion(tempFenceValue, fenceEvent));
WaitForSingleObject(fenceEvent, INFINITE);
}
frameIndex = swapChain->GetCurrentBackBufferIndex();
}
void OnRender()
{
PopulateCommandList();
ID3D12CommandList* ppcommandLists[] = { commandList.Get() };
commandQueue->ExecuteCommandLists(_countof(ppcommandLists), ppcommandLists);
// 将渲染结果呈现到屏幕上,并交换前后缓冲区
ThrowIfFailed(swapChain->Present(1, 0));
Flush();
}
void OnDestroy()
{
Flush();
CloseHandle(fenceEvent);
}
//窗口过程函数
LRESULT CALLBACK WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_PAINT:
OnRender();
return 0;
case WM_DESTROY://窗口销毁消息
PostQuitMessage(0);//向系统表明有个线程有终止请求。用来响应 WM_DESTROY消息
return 0;
}
return DefWindowProc(hWnd, message, wParam, lParam);//调用默认的窗口过程来为应用程序没有处理的窗口消息提供默认的处理。
}
int CALLBACK WinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPSTR lpCmdLine, _In_ int nShowCmd)
{
// 窗口类的设计
//开始设计一个完整的窗口类
//用WINDCLASSEX定义了一个窗口类,即用wndClass实例化了WINCLASSEX,用于之后窗口的各项初始化
WNDCLASSEX windowClass = { 0 };
//设置结构体的字节数大小
windowClass.cbSize = sizeof(WNDCLASSEX);
//设置窗口的样式
windowClass.style = CS_HREDRAW | CS_VREDRAW;
//指向窗口过程函数的指针
windowClass.lpfnWndProc = WindowProc;
//指定包含窗口过程的程序的实例
windowClass.hInstance = hInstance;
//指定窗口类的光标句柄
windowClass.hCursor = LoadCursor(NULL, IDC_ARROW);
//从全局的::LoadImage函数从本地加载自定义ico图标
windowClass.hIcon = (HICON)::LoadImage(NULL, L"icon.ico", IMAGE_ICON, 0, 0, LR_DEFAULTSIZE | LR_LOADFROMFILE);
//用一个以空终止的字符串,指定窗口类的名字
windowClass.lpszClassName = L"RenderClass";
//窗口类的注册
RegisterClassEx(&windowClass);
//窗口的正式创建
hwnd = CreateWindow(
windowClass.lpszClassName,
L"饼子的渲染器ヾ(≧▽≦*)o",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT,
width,
height,
nullptr,
nullptr,
hInstance,
nullptr);
LoadPipeline();
LoadAsset();
//显示窗口
ShowWindow(hwnd, SW_SHOW);
MSG msg = {};//定义并初始化消息
while (msg.message != WM_QUIT)//不断从消息队列中取出消息
{
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
TranslateMessage(&msg);//将虚拟键消息转换为字符消息
DispatchMessage(&msg);//分发一个消息给窗口程序
}
}
OnDestroy();
return 0;
}
运行之后会出现这样的结果
我们发现此时窗口没有任何的变化,那是因为我们只是单纯地把rendertarget设置成一个简单的颜色推上去了而已。我们继续丰富每帧命令。
接下来我们绘制一个三角形。
三、绘制图形需要的渲染管线
根签名
绘制三角形需要顶点和索引,我们后面还会再加坐标变换的矩阵等等,这个时候就需要根签名。根签名是用来做什么的呢?由于GPU千变万化,没办法一套二进制走天下,所以这个时候需要一个中间码。这个中间码被保存在Blob中,保存到Blob的这个操作就需要根签名来做了。 根签名的根参数可以是根描述符表、根描述符、根常量,在龙书第六章中用的是根描述符表,龙书第七章又分别讲解了剩下两种。引入太多概念非常容易混淆,所以我们在创建根签名时直接把跟参数设置为0,后面绘制立方体时再做添加。我们往LoadAsset()中添加内容
// 创建根签名
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(device->CreateRootSignature(0, signature->GetBufferPointer(), signature->GetBufferSize(), IID_PPV_ARGS(&rootSignature)));
创建着色器
这里自己建一个文件夹Shaders,然后里面创建一个Shader.hlsl
// 创建着色器
ComPtr<ID3DBlob> vertexShader;
ComPtr<ID3DBlob> pixelShader;
#if defined(_DEBUG)
UINT compileFlags = D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION;
#else
UINT compileFlags = 0;
#endif
ThrowIfFailed(D3DCompileFromFile(std::wstring(L"Shaders/Shader.hlsl").c_str(), nullptr, nullptr, "VS", "vs_5_0", compileFlags, 0, &vertexShader, nullptr));
ThrowIfFailed(D3DCompileFromFile(std::wstring(L"Shaders/Shader.hlsl").c_str(), nullptr, nullptr, "PS", "ps_5_0", compileFlags, 0, &pixelShader, nullptr));
记得给项目属性更改一下hlsl设置 ![[Pasted image 20241224102209.png]]
着色器
在Shader.hlsl里面我们写了一个简单的shader,这里只是把顶点色传过去而已,没做什么多余的事情
struct VertexIn
{
float3 PositionL : POSITION;
float4 color : COLOR;
};
struct VertexOut
{
float3 positionH : SV_Position;
float4 color : COLOR;
};
VertexOut VS(VertexIn vertex)
{
VertexOut vout;
vout.positionH = vertex.PositionL;
vout.color = vertex.color;
return vout;
}
float4 PS(VertexOut pin) : SV_Target
{
return pin.color;
}
输入布局
这里规定了shader中的语义。具体看龙书第189到192讲的很清楚了。
//输入布局
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 }
};
渲染管线状态PSO
可以认为PSO是一个可执行的GPU Shader在CPU端的Handle,PSO的全称则是Pipeline State Object。
// 渲染管线状态PSO
D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc = {};
psoDesc.InputLayout = { inputElementDescs, _countof(inputElementDescs) };
psoDesc.pRootSignature = rootSignature.Get();
psoDesc.VS = CD3DX12_SHADER_BYTECODE(vertexShader.Get());
psoDesc.PS = CD3DX12_SHADER_BYTECODE(pixelShader.Get());
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(device->CreateGraphicsPipelineState(&psoDesc, IID_PPV_ARGS(&pipelineState)));
创建三角形
我们先写一个有顶点位置信息和颜色信息的结构体
struct Vertex
{
XMFLOAT3 position;
XMFLOAT4 color;
};
根据这个结构体来继续在LoadAsset() 方法中创建三角形
Vertex triangleVertices[] =
{
{ { 0.0f, 0.5f, 0.0f }, { 1.0f, 0.0f, 0.0f, 1.0f } },
{ { 0.5f, -0.5f, 0.0f }, { 0.0f, 1.0f, 0.0f, 1.0f } },
{ { -0.5f, -0.5f, 0.0f }, { 0.0f, 0.0f, 1.0f, 1.0f } }
};
然后我们需要创建顶点缓冲区(可以理解为一段内存),和顶点缓冲区视图(BufferView和描述符一个意思),就可以把这块资源创建出来了。我们继续在LoadAsset() 方法中添加代码。 181页龙书中写“我们无需为顶点缓冲区视图创建描述符堆。”这句话不是说真的不用创建堆了,因为龙书将创建缓冲区的方法封装在d3dUtil::CreateDefaultBuffer中了,在这个方法里面已经把堆创建好了(111页)所以在这里我们先直接把方法写出来。描述符堆看111页,会发现有三种描述符堆,我们在这里暂时上传到UploadBuffer中。 还有一点不一样的地方,就是龙书是直接用SetValue(&GetValue());
的办法,这样的话在新的语法中是不支持的,所以我们会创建一个临时描述符堆变量。
// 创建顶点缓冲区描述符
const UINT vertexBufferSize = sizeof(triangleVertices);
CD3DX12_HEAP_PROPERTIES heapProperties = CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD);
CD3DX12_RESOURCE_DESC resourceDesc = CD3DX12_RESOURCE_DESC::Buffer(vertexBufferSize);
ThrowIfFailed(device->CreateCommittedResource(&heapProperties,
D3D12_HEAP_FLAG_NONE,
&resourceDesc,
D3D12_RESOURCE_STATE_GENERIC_READ,
nullptr,
IID_PPV_ARGS(&vertexBuffer))); // 这个方法会根据我们提供的属性创建一个描述符和一个对应的描述符堆
光是创建是不行的,我们需要把资源映射过去。 龙书上使用的是这两行代码:
ThrowIfFailed(D3DCreateBlob(vbByteSize, &geo->VertexBufferCPU));
CopyMemory(geo->VertexBufferCPU->GetBufferPointer(), vertices.data(), vbByteSize);
我们会使用这两行:
ThrowIfFailed(vertexBuffer->Map(0, &readRange, reinterpret_cast<void**>(&pVertexDataBegin))); memcpy(pVertexDataBegin, triangleVertices, sizeof(triangleVertices));
说一下区别。龙书上的代码使用D3DCreateBlob
创建一个 ID3DBlob
对象,实际上是分配一块内存,这块内存可以存储数据。CopyMemory
是将顶点数据(vertices.data()
)从 CPU 内存拷贝到 ID3DBlob
对象中。这时,数据存在于 CPU 内存中,并未直接上传到 GPU 内存。这种方法适用于初始化缓冲区或静态数据的上传,尤其是当数据量很大时,可能需要分批上传。 我们的代码中,Map
是 DirectX 12 的一种方式,它将 GPU 缓冲区映射到 CPU 可访问的地址空间。调用 Map
后,pVertexDataBegin
指针指向 GPU 内存区域的一个指针,允许直接操作这个区域的数据(通常是写入数据)。memcpy
将顶点数据(triangleVertices
)从 CPU 内存拷贝到 GPU 缓冲区(即映射后的 pVertexDataBegin
指针指向的区域)。通过这种方法数据直接进入 GPU 内存,不需要先存储在 CPU 内存中。这种方法适用于动态更新缓冲区的数据,例如每帧渲染时更新顶点数据、常量缓冲区或动态数据缓存。
// 声明一个指针,用于指向映射后的缓冲区数据。
UINT8* pVertexDataBegin;
// 定义一个读取范围。这里范围为 (0, 0),表示 GPU 不需要从这个缓冲区读取任何数据,
// 因为我们仅仅是写入数据。这种设置能够优化性能,避免多余的数据传输。
// 在映射期间,不允许 GPU 读取数据(我们会放在后面读取)
CD3DX12_RANGE readRange(0, 0);
// 映射顶点缓冲区
ThrowIfFailed(vertexBuffer->Map(0, // 表示子资源索引(常用值为 0,表示默认子资源)
&readRange, // 指定 GPU 要读取的范围
reinterpret_cast<void**>(&pVertexDataBegin))); // 返回映射后的指针,指向GPU缓冲区的起始位置。
// 使用 memcpy 将 triangleVertices 数据拷贝到映射后的缓冲区。
memcpy(pVertexDataBegin, triangleVertices, sizeof(triangleVertices));
// map之后需要unmap
vertexBuffer->Unmap(0, nullptr);
最后我们需要设置一下顶点缓冲区视图
// 设置一下vertexBufferView
vertexBufferView.BufferLocation = vertexBuffer->GetGPUVirtualAddress();
vertexBufferView.StrideInBytes = sizeof(Vertex);
vertexBufferView.SizeInBytes = vertexBufferSize;
添加PopulateCommandList()命令
既然刚才我们已经创建好根签名了,那在这里我们就需要拿到它
commandList->SetGraphicsRootSignature(rootSignature.Get());
我们把视口和裁剪矩形也补上,先添加一下公共变量
CD3DX12_VIEWPORT viewport(0.0f,0.0f, width,height);
CD3DX12_RECT scissorRect(0, 0, width, height);
然后添加命令
commandList->RSSetViewports(1, &viewport);
commandList->RSSetScissorRects(1, &scissorRect);
因为我们绘制了三角形,所以要记得输出到合并阶段 输出合并阶段是渲染管线的最后一个阶段,它将着色器输出的数据写入渲染目标
commandList->OMSetRenderTargets(1, &rtvHandle, FALSE, nullptr);
增加三角形绘制的命令
commandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
commandList->IASetVertexBuffers(0, 1, &vertexBufferView);
commandList->DrawInstanced(3, 1, 0, 0);
完整代码
#include <Windows.h>
#include <d3d12.h>
#include <dxgi1_6.h>
#include <D3Dcompiler.h>
#include <DirectXMath.h>
#include "Common/d3dx12.h"
#include <wrl.h>
#include <iostream>
#include <string>
#include <wrl.h>
#include <shellapi.h>
#pragma comment(lib, "d3d12.lib")
#pragma comment(lib, "dxgi.lib")
using namespace Microsoft::WRL;
using namespace DirectX;
const UINT FrameCount = 2;
UINT width = 800;
UINT height = 600;
HWND hwnd;
ComPtr<ID3D12Device> device;
ComPtr<ID3D12CommandQueue> commandQueue;
ComPtr<IDXGISwapChain3> swapChain;
ComPtr<ID3D12Resource> renderTargets[FrameCount];
ComPtr<ID3D12DescriptorHeap> rtvHeap;
ComPtr<ID3D12CommandAllocator> commandAllocator;
ComPtr<ID3D12GraphicsCommandList> commandList;
ComPtr<ID3D12PipelineState> pipelineState;
ComPtr<ID3D12RootSignature> rootSignature;
ComPtr<ID3D12Resource> vertexBuffer;
D3D12_VERTEX_BUFFER_VIEW vertexBufferView;
CD3DX12_VIEWPORT viewport(0.0f, 0.0f, width, height);
CD3DX12_RECT scissorRect(0, 0, width, height);
UINT rtvDescriptorSize;
UINT frameIndex;
HANDLE fenceEvent;
ComPtr<ID3D12Fence> fence;
UINT64 fenceValue;
struct Vertex
{
XMFLOAT3 position;
XMFLOAT4 color;
};
std::string HrToString(HRESULT hr)
{
char s_str[64] = {};
sprintf_s(s_str, "HRESULT of 0x%08X", static_cast<UINT>(hr));
return std::string(s_str);
}
class HrException : public std::runtime_error
{
public:
HrException(HRESULT hr) : std::runtime_error(HrToString(hr)), m_hr(hr) {}
HRESULT Error() const { return m_hr; }
private:
const HRESULT m_hr;
};
void ThrowIfFailed(HRESULT hr)
{
if (FAILED(hr))
{
throw HrException(hr);
}
}
IDXGIAdapter1* GetSurpportedAdapter(ComPtr<IDXGIFactory4>& factory, const D3D_FEATURE_LEVEL featurelevel)
{
IDXGIAdapter1* adapter = nullptr;
for (std::uint32_t adapterIndex = 0U; ; ++adapterIndex)
{
IDXGIAdapter1* currentAdapter = nullptr;
if (DXGI_ERROR_NOT_FOUND == factory->EnumAdapters1(adapterIndex, ¤tAdapter))
{
break;
}
const HRESULT hr = D3D12CreateDevice(currentAdapter, featurelevel, _uuidof(ID3D12Device), nullptr);
if (SUCCEEDED(hr))
{
adapter = currentAdapter;
break;
}
currentAdapter->Release();
}
return adapter;
}
// 加载管线资源
void LoadPipeline()
{
#if defined(_DEBUG)
{
ComPtr<ID3D12Debug> debugController;
if (SUCCEEDED(D3D12GetDebugInterface(IID_PPV_ARGS(&debugController))))
{
debugController->EnableDebugLayer();
}
}
#endif
// 创建工厂
ComPtr<IDXGIFactory4> mDxgiFactory;
ThrowIfFailed(CreateDXGIFactory1(IID_PPV_ARGS(mDxgiFactory.GetAddressOf())));
// 枚举适配器
D3D_FEATURE_LEVEL featurelevels[] =
{
D3D_FEATURE_LEVEL_12_1,
D3D_FEATURE_LEVEL_12_0,
D3D_FEATURE_LEVEL_11_1,
D3D_FEATURE_LEVEL_11_0
};
// 创建适配器
IDXGIAdapter1* adapter = nullptr;
for (std::uint32_t i = 0U; i <= _countof(featurelevels); ++i)
{
adapter = GetSurpportedAdapter(mDxgiFactory, featurelevels[i]);
if (adapter != nullptr)
{
break;
}
}
// 创建设备
if (adapter != nullptr)
{
D3D12CreateDevice(adapter, D3D_FEATURE_LEVEL_12_1, IID_PPV_ARGS(device.GetAddressOf()));
}
// 创建命令队列
D3D12_COMMAND_QUEUE_DESC queueDesc = {};
queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
ThrowIfFailed(device->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&commandQueue)));
// 创建交换链
DXGI_SWAP_CHAIN_DESC1 swapChainDesc = {};
swapChainDesc.BufferCount = FrameCount;
swapChainDesc.Width = width;
swapChainDesc.Height = height;
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> swapchain1;
ThrowIfFailed(mDxgiFactory->CreateSwapChainForHwnd(commandQueue.Get(),
hwnd, &swapChainDesc, nullptr, nullptr, &swapchain1));
ThrowIfFailed(swapchain1.As(&swapChain));
frameIndex = swapChain->GetCurrentBackBufferIndex();
// 创建RTV描述符堆
D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc = { };
rtvHeapDesc.NumDescriptors = FrameCount;
rtvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;
rtvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
ThrowIfFailed(device->CreateDescriptorHeap(&rtvHeapDesc, IID_PPV_ARGS(&rtvHeap)));
rtvDescriptorSize = device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
// 创建RTV描述符
CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(rtvHeap->GetCPUDescriptorHandleForHeapStart()); // 指向 CPU 可访问的描述符的句柄
for (UINT n = 0; n < FrameCount; n++)
{
ThrowIfFailed(swapChain->GetBuffer(n, IID_PPV_ARGS(&renderTargets[n])));
device->CreateRenderTargetView(renderTargets[n].Get(), nullptr, rtvHandle);
rtvHandle.Offset(1, rtvDescriptorSize); // 偏移几个,偏移夺少
}
// 创建命令分配器
ThrowIfFailed(device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&commandAllocator)));
}
// 加载资源
void LoadAsset()
{
// 创建根签名
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(device->CreateRootSignature(0, signature->GetBufferPointer(), signature->GetBufferSize(), IID_PPV_ARGS(&rootSignature)));
// 创建着色器
ComPtr<ID3DBlob> vertexShader;
ComPtr<ID3DBlob> pixelShader;
#if defined(_DEBUG)
UINT compileFlags = D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION;
#else
UINT compileFlags = 0;
#endif
ThrowIfFailed(D3DCompileFromFile(std::wstring(L"Shaders/Shader.hlsl").c_str(), nullptr, nullptr, "VS", "vs_5_0", compileFlags, 0, &vertexShader, nullptr));
ThrowIfFailed(D3DCompileFromFile(std::wstring(L"Shaders/Shader.hlsl").c_str(), nullptr, nullptr, "PS", "ps_5_0", compileFlags, 0, &pixelShader, nullptr));
//输入布局
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 }
};
// 渲染管线状态PSO
D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc = {};
psoDesc.InputLayout = { inputElementDescs, _countof(inputElementDescs) };
psoDesc.pRootSignature = rootSignature.Get();
psoDesc.VS = CD3DX12_SHADER_BYTECODE(vertexShader.Get());
psoDesc.PS = CD3DX12_SHADER_BYTECODE(pixelShader.Get());
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(device->CreateGraphicsPipelineState(&psoDesc, IID_PPV_ARGS(&pipelineState)));
// 创建命令队列
ThrowIfFailed(device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, commandAllocator.Get(), pipelineState.Get(), IID_PPV_ARGS(&commandList)));
ThrowIfFailed(commandList->Close());
Vertex triangleVertices[] =
{
{ { 0.0f, 0.5f, 0.0f }, { 1.0f, 0.0f, 0.0f, 1.0f } },
{ { 0.5f, -0.5f, 0.0f }, { 0.0f, 1.0f, 0.0f, 1.0f } },
{ { -0.5f, -0.5f, 0.0f }, { 0.0f, 0.0f, 1.0f, 1.0f } }
};
// 创建顶点缓冲区描述符
const UINT vertexBufferSize = sizeof(triangleVertices);
CD3DX12_HEAP_PROPERTIES heapProperties = CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD);
CD3DX12_RESOURCE_DESC resourceDesc = CD3DX12_RESOURCE_DESC::Buffer(vertexBufferSize);
ThrowIfFailed(device->CreateCommittedResource(&heapProperties,
D3D12_HEAP_FLAG_NONE,
&resourceDesc,
D3D12_RESOURCE_STATE_GENERIC_READ,
nullptr,
IID_PPV_ARGS(&vertexBuffer))); // 这个方法会根据我们提供的属性创建一个描述符和一个对应的描述符堆
// 声明一个指针,用于指向映射后的缓冲区数据。
UINT8* pVertexDataBegin;
// 定义一个读取范围。这里范围为 (0, 0),表示 GPU 不需要从这个缓冲区读取任何数据,
// 因为我们仅仅是写入数据。这种设置能够优化性能,避免多余的数据传输。
// 在映射期间,不允许 GPU 读取数据(我们会放在后面读取)
CD3DX12_RANGE readRange(0, 0);
// 映射顶点缓冲区
ThrowIfFailed(vertexBuffer->Map(0, // 表示子资源索引(常用值为 0,表示默认子资源)
&readRange, // 指定 GPU 要读取的范围
reinterpret_cast<void**>(&pVertexDataBegin))); // 返回映射后的指针,指向GPU缓冲区的起始位置。
// 使用 memcpy 将 triangleVertices 数据拷贝到映射后的缓冲区。
memcpy(pVertexDataBegin, triangleVertices, sizeof(triangleVertices));
// map之后需要unmap
vertexBuffer->Unmap(0, nullptr);
// 设置一下vertexBufferView
vertexBufferView.BufferLocation = vertexBuffer->GetGPUVirtualAddress();
vertexBufferView.StrideInBytes = sizeof(Vertex);
vertexBufferView.SizeInBytes = vertexBufferSize;
// 以下为加载资源对象
ThrowIfFailed(device->CreateFence(fenceValue, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&fence)));
fenceValue = 1;
// 这里创建了一个空事件,为后面的 Flush() 使用
fenceEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);
if (fenceEvent == nullptr)
{
ThrowIfFailed(HRESULT_FROM_WIN32(GetLastError()));
}
}
// 添加命令
void PopulateCommandList()
{
ThrowIfFailed(commandAllocator->Reset());
ThrowIfFailed(commandList->Reset(commandAllocator.Get(), pipelineState.Get()));
commandList->SetGraphicsRootSignature(rootSignature.Get());
commandList->RSSetViewports(1, &viewport);
commandList->RSSetScissorRects(1, &scissorRect);
D3D12_RESOURCE_BARRIER resBarrier = CD3DX12_RESOURCE_BARRIER::Transition(renderTargets[frameIndex].Get(), D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_RENDER_TARGET);
commandList->ResourceBarrier(1, &resBarrier);
CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(rtvHeap->GetCPUDescriptorHandleForHeapStart(), frameIndex, rtvDescriptorSize);
commandList->OMSetRenderTargets(1, &rtvHandle, FALSE, nullptr);
const float clearColor[] = { 1.0f, 0.7f, 0.7f, 1.0f };
commandList->ClearRenderTargetView(rtvHandle, clearColor, 0, nullptr);
commandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
commandList->IASetVertexBuffers(0, 1, &vertexBufferView);
commandList->DrawInstanced(3, 1, 0, 0);
resBarrier = CD3DX12_RESOURCE_BARRIER::Transition(renderTargets[frameIndex].Get(), D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT);
commandList->ResourceBarrier(1, &resBarrier);
ThrowIfFailed(commandList->Close());
}
//同步CPU与GPU资源
void Flush()
{
const UINT64 tempFenceValue = fenceValue;
ThrowIfFailed(commandQueue->Signal(fence.Get(), tempFenceValue));
fenceValue++;
if (fence->GetCompletedValue() < tempFenceValue)
{
ThrowIfFailed(fence->SetEventOnCompletion(tempFenceValue, fenceEvent));
WaitForSingleObject(fenceEvent, INFINITE);
}
frameIndex = swapChain->GetCurrentBackBufferIndex();
}
void OnRender()
{
PopulateCommandList();
ID3D12CommandList* ppcommandLists[] = { commandList.Get() };
commandQueue->ExecuteCommandLists(_countof(ppcommandLists), ppcommandLists);
// 将渲染结果呈现到屏幕上,并交换前后缓冲区
ThrowIfFailed(swapChain->Present(1, 0));
Flush();
}
void OnDestroy()
{
Flush();
CloseHandle(fenceEvent);
}
//窗口过程函数
LRESULT CALLBACK WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_PAINT:
OnRender();
return 0;
case WM_DESTROY://窗口销毁消息
PostQuitMessage(0);//向系统表明有个线程有终止请求。用来响应 WM_DESTROY消息
return 0;
}
return DefWindowProc(hWnd, message, wParam, lParam);//调用默认的窗口过程来为应用程序没有处理的窗口消息提供默认的处理。
}
int CALLBACK WinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPSTR lpCmdLine, _In_ int nShowCmd)
{
// 窗口类的设计
//开始设计一个完整的窗口类
//用WINDCLASSEX定义了一个窗口类,即用wndClass实例化了WINCLASSEX,用于之后窗口的各项初始化
WNDCLASSEX windowClass = { 0 };
//设置结构体的字节数大小
windowClass.cbSize = sizeof(WNDCLASSEX);
//设置窗口的样式
windowClass.style = CS_HREDRAW | CS_VREDRAW;
//指向窗口过程函数的指针
windowClass.lpfnWndProc = WindowProc;
//指定包含窗口过程的程序的实例
windowClass.hInstance = hInstance;
//指定窗口类的光标句柄
windowClass.hCursor = LoadCursor(NULL, IDC_ARROW);
//从全局的::LoadImage函数从本地加载自定义ico图标
windowClass.hIcon = (HICON)::LoadImage(NULL, L"icon.ico", IMAGE_ICON, 0, 0, LR_DEFAULTSIZE | LR_LOADFROMFILE);
//用一个以空终止的字符串,指定窗口类的名字
windowClass.lpszClassName = L"RenderClass";
//窗口类的注册
RegisterClassEx(&windowClass);
//窗口的正式创建
hwnd = CreateWindow(
windowClass.lpszClassName,
L"饼子的渲染器ヾ(≧▽≦*)o",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT,
width,
height,
nullptr,
nullptr,
hInstance,
nullptr);
LoadPipeline();
LoadAsset();
//显示窗口
ShowWindow(hwnd, SW_SHOW);
MSG msg = {};//定义并初始化消息
while (msg.message != WM_QUIT)//不断从消息队列中取出消息
{
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
TranslateMessage(&msg);//将虚拟键消息转换为字符消息
DispatchMessage(&msg);//分发一个消息给窗口程序
}
}
OnDestroy();
return 0;
}
绘制三角形的部分结束,这一部分讲了渲染管线需要的主要流程,这一章需要掌握渲染管线的搭建和命令队列的相关设置。 绘制结果:
总结
我们首先创建了一个窗口,窗口的创建是固定流程
基础渲染管线的搭建:
渲染管线必需品:
- 创建工厂(为了创建适配器和交换链)
- 创建适配器(也就是显卡,需要先枚举再创建)
- 创建设备(为了创建后续的一系列资源)
- 创建命令队列
- 创建交换链
- 创建RTV描述符堆
- 创建RTV描述符
- 创建命令分配器
加载资源
录制命令
同步CPU与GPU资源(Flush()函数,OnRender()每帧同步一次)
OnRender()绘制
OnDestory()销毁
窗口过程函数
绘制图形需要搭建的内容:
渲染管线必需品:
- 根签名(用来保存中间码)
- 创建着色器,写shader
- 创建PSO(可执行的GPU Shader在CPU端的Handle)
- 创建三角形(需要写输入布局,定义和输入布局匹配的结构体,创建顶点缓冲区和与缓冲区对应的视图,需要做一个资源映射)
加载资源:
- 把根签名Set掉
- 输出合并阶段
- 绘制三角形
- (我们另外添加了设置视口和矩形的操作)
虽然已经尽力把内容拆开了,但是还是好多哦[]~( ̄▽ ̄)~*