计算机图形学与GPU渲染 -- 图形API--DX
简介
DirectX是一个应用程序编程接口(api)的集合,用于在微软平台上处理与多媒体相关的任务,特别是游戏编程和视频。最初,这些api的名字都是以“Direct”开头的,比如Direct3D, DirectDraw, DirectMusic, DirectPlay, DirectSound等等。DirectX这个名字被创造出来作为所有这些API的缩写(X代表特定的API名称),并很快成为集合的名称。当微软后来开始开发游戏主机时,X被用作Xbox名称的基础,以表明该主机基于DirectX技术。在为Xbox设计的api(如XInput和跨平台音频创建工具(XACT))的命名中保留了X的首字母,而DirectX模式则继续用于Windows api(如Direct2D和DirectWrite)。
Direct3D(简称:D3D)是微软公司在Microsoft Windows操作系统上所开发的一套3D绘图编程接口,是一种低级别 API,可用于绘制每帧三角形、线条或点,或在 GPU 上启动高度并行操作。
Direct3D:
在一致的抽象后面隐藏不同的 GPU 实现。 但仍需要了解如何绘制 3D 图形。
Direct3D
Direct3D在一致的抽象后面隐藏不同的 GPU 实现。 但仍需要了解如何绘制 3D 图形。
抽象概念包括:devices, swap chains和resources。
D3D定义了5种设备驱动类型:
- HAL(hardware abstraction layer):使硬件加速。
- reference:应用程序请求一个reference设备。
- null :不支持渲染,调试无渲染动作的程序的时候可使用。
- Sw:系统实现的一种低效的软件光栅化方案。
- WARP:通过高版本dx支持低版本的dx的高效的软件方案。
Swap chain:
- 每一个设备至少要有一个swap chain.一个swap chain可用来产生一个或多个back buffer surfaces。渲染目标(render target)也是back buffer surface。back buffer是属于渲染(render)的部份。所有的back buffer都是合理的render target,但是并非所有render target都是back buffer。surface是一种资源,包含一个矩形集合的像素数据,如color, alpha, depth/stencil。
resources有4个属性:
- Type:资源的类型,如顶点缓冲区(vexert buffer)或一个渲染目标(render target)。
- Usage:资源的用途,如纹理(texture)或渲染目标,由系统的标志所组成,每个标志位占1 bits。
- Format:数据的格式,如一个二维表面的像素格式。例如,D3DFMT_R8G8B8的值是一个24 bits的颜色深度(colour depth,8 bits是红色,8 bits绿色以及8 bits是蓝色)。
- Pool:资源所分配的内部存储器空间类型。
Direct3D Pipeline
- 设备初始化
- 创建DXGI交换链
- 配置交换链与Direct3D设备的交互
- 创建着色器
- 创建顶点缓冲区
- 配置pipeline状态
- 绘制
设备初始化
Direct3D初始化阶段首先需要创建D3D设备和D3D设备上下文
D3D设备(ID3D11Device)通常代表一个显示适配器(即显卡),它最主要的功能是用于创建各种所需资源,最常用的资源有:资源类(ID3D11Resource, 包含纹理和缓冲区),视图类以及着色器。此外,D3D设备还能够用于检测系统环境对功能的支持情况。
D3D设备上下文(ID3D11DeviceContext)可以看做是一个渲染管线。通常我们在创建D3D设备的同时也会附赠一个立即设备上下文(Immediate Context)。一个D3D设备仅对应一个D3D立即设备上下文,并且只要我们拥有其中一方,就能通过各自的方法获取另一方(即ID3D11Device::GetImmediateContext和ID3D11DeviceContext::GetDevice)。渲染管线主要负责渲染和计算工作,它需要绑定来自与它关联的D3D设备所创建的各种资源、视图和着色器才能正常运转,除此之外,它还能够负责对资源的直接读写操作。
如果你的系统支持Direct3D 11.1的话,则对应的接口类为:ID3D11Device1、ID3D11DeviceContext1,它们分别继承自上面的两个接口类,区别在于额外提供了少数新的接口,并且接口方法的实现可能会有所区别。
HRESULT hr = S_OK;
// 创建D3D设备 和 D3D设备上下文
UINT createDeviceFlags = 0;
#if defined(DEBUG) || defined(_DEBUG)
createDeviceFlags |= D3D11_CREATE_DEVICE_DEBUG;
#endif
// 驱动类型数组
D3D_DRIVER_TYPE driverTypes[] =
{
D3D_DRIVER_TYPE_HARDWARE,
D3D_DRIVER_TYPE_WARP,
D3D_DRIVER_TYPE_REFERENCE,
};
UINT numDriverTypes = ARRAYSIZE(driverTypes);
// 特性等级数组
D3D_FEATURE_LEVEL featureLevels[] =
{
D3D_FEATURE_LEVEL_11_1,
D3D_FEATURE_LEVEL_11_0,
};
UINT numFeatureLevels = ARRAYSIZE(featureLevels);
D3D_FEATURE_LEVEL featureLevel;
D3D_DRIVER_TYPE d3dDriverType;
for (UINT driverTypeIndex = 0; driverTypeIndex < numDriverTypes; driverTypeIndex++)
{
d3dDriverType = driverTypes[driverTypeIndex];
hr = D3D11CreateDevice(nullptr, d3dDriverType, nullptr, createDeviceFlags, featureLevels, numFeatureLevels,
D3D11_SDK_VERSION, m_pd3dDevice.GetAddressOf(), &featureLevel, m_pd3dImmediateContext.GetAddressOf());
if (hr == E_INVALIDARG)
{
// Direct3D 11.0 的API不承认D3D_FEATURE_LEVEL_11_1,所以我们需要尝试特性等级11.0以及以下的版本
hr = D3D11CreateDevice(nullptr, d3dDriverType, nullptr, createDeviceFlags, &featureLevels[1], numFeatureLevels - 1,
D3D11_SDK_VERSION, m_pd3dDevice.GetAddressOf(), &featureLevel, m_pd3dImmediateContext.GetAddressOf());
}
if (SUCCEEDED(hr))
break;
}
if (FAILED(hr))
{
MessageBox(0, L"D3D11CreateDevice Failed.", 0, 0);
return false;
}
// 检测是否支持特性等级11.0或11.1
if (featureLevel != D3D_FEATURE_LEVEL_11_0 && featureLevel != D3D_FEATURE_LEVEL_11_1)
{
MessageBox(0, L"Direct3D Feature Level 11 unsupported.", 0, 0);
return false;
}
// 检测 MSAA支持的质量等级
m_pd3dDevice->CheckMultisampleQualityLevels(
DXGI_FORMAT_R8G8B8A8_UNORM, 4, &m_4xMsaaQuality);
assert(m_4xMsaaQuality > 0);
DXGI交换链
DXGI交换链(IDXGISwapChain)缓存了一个或多个表面(2D纹理),它们都可以称作后备缓冲区(backbuffer)。后备缓冲区则是我们主要进行渲染的场所,我们可以将这些缓冲区通过合适的手段成为渲染管线的输出对象。在进行呈现(Present)的时候有两种方法:
BitBlt Model(位块传输模型):将后备缓冲区的数据进行BitBlt(位块传输,即内容上的拷贝),传入到DWM与DX共享的后备缓冲区,然后进行翻转以显示其内容。使用这种模型至少需要一个后备缓冲区。事实上,这也是Win32应用程序最常使用的方式,在进行呈现后,渲染管线仍然是对同一个后备缓冲区进行输出。(支持Windows 7及更高版本)
Flip Model(翻转模型):该模型可以避免上一种方式多余的复制,后备缓冲区表面可以直接与DWM内的前台缓冲区进行翻转。但是需要创建至少两个后备缓冲区,并且在每次完成呈现后通过代码切换到另一个后备缓冲区进行渲染。该模型可以用于Win32应用程序以及UWP应用程序(需要DXGI1.2,支持Windows 8及更高版本)
注意:考虑到要兼容Win7系统,而且由于我们编写的是Win32应用程序,因此这里使用的是第一种模型。同时这也是绝大多数教程所使用的。
交换链创建
交换链创建代码如下:
ComPtr<IDXGIDevice> dxgiDevice = nullptr;
ComPtr<IDXGIAdapter> dxgiAdapter = nullptr;
ComPtr<IDXGIFactory1> dxgiFactory1 = nullptr; // D3D11.0(包含DXGI1.1)的接口类
ComPtr<IDXGIFactory2> dxgiFactory2 = nullptr; // D3D11.1(包含DXGI1.2)特有的接口类
// 为了正确创建 DXGI交换链,首先我们需要获取创建 D3D设备 的 DXGI工厂,否则会引发报错:
// "IDXGIFactory::CreateSwapChain: This function is being called with a device from a different IDXGIFactory."
HR(m_pd3dDevice.As(&dxgiDevice));
HR(dxgiDevice->GetAdapter(dxgiAdapter.GetAddressOf()));
HR(dxgiAdapter->GetParent(__uuidof(IDXGIFactory1), reinterpret_cast<void**>(dxgiFactory1.GetAddressOf())));
// 查看该对象是否包含IDXGIFactory2接口
hr = dxgiFactory1.As(&dxgiFactory2);
// 如果包含,则说明支持D3D11.1
if (dxgiFactory2 != nullptr)
{
HR(m_pd3dDevice.As(&m_pd3dDevice1));
HR(m_pd3dImmediateContext.As(&m_pd3dImmediateContext1));
// 填充各种结构体用以描述交换链
DXGI_SWAP_CHAIN_DESC1 sd;
ZeroMemory(&sd, sizeof(sd));
sd.Width = m_ClientWidth;
sd.Height = m_ClientHeight;
sd.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
// 是否开启4倍多重采样?
if (m_Enable4xMsaa)
{
sd.SampleDesc.Count = 4;
sd.SampleDesc.Quality = m_4xMsaaQuality - 1;
}
else
{
sd.SampleDesc.Count = 1;
sd.SampleDesc.Quality = 0;
}
sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
sd.BufferCount = 1;
sd.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;
sd.Flags = 0;
DXGI_SWAP_CHAIN_FULLSCREEN_DESC fd;
fd.RefreshRate.Numerator = 60;
fd.RefreshRate.Denominator = 1;
fd.Scaling = DXGI_MODE_SCALING_UNSPECIFIED;
fd.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED;
fd.Windowed = TRUE;
// 为当前窗口创建交换链
HR(dxgiFactory2->CreateSwapChainForHwnd(m_pd3dDevice.Get(), m_hMainWnd, &sd, &fd, nullptr, m_pSwapChain1.GetAddressOf()));
HR(m_pSwapChain1.As(&m_pSwapChain));
}
else
{
// 填充DXGI_SWAP_CHAIN_DESC用以描述交换链
DXGI_SWAP_CHAIN_DESC sd;
ZeroMemory(&sd, sizeof(sd));
sd.BufferDesc.Width = m_ClientWidth;
sd.BufferDesc.Height = m_ClientHeight;
sd.BufferDesc.RefreshRate.Numerator = 60;
sd.BufferDesc.RefreshRate.Denominator = 1;
sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
sd.BufferDesc.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED;
sd.BufferDesc.Scaling = DXGI_MODE_SCALING_UNSPECIFIED;
// 是否开启4倍多重采样?
if (m_Enable4xMsaa)
{
sd.SampleDesc.Count = 4;
sd.SampleDesc.Quality = m_4xMsaaQuality - 1;
}
else
{
sd.SampleDesc.Count = 1;
sd.SampleDesc.Quality = 0;
}
sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
sd.BufferCount = 1;
sd.OutputWindow = m_hMainWnd;
sd.Windowed = TRUE;
sd.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;
sd.Flags = 0;
HR(dxgiFactory1->CreateSwapChain(m_pd3dDevice.Get(), &sd, m_pSwapChain.GetAddressOf()));
}
交换链与Direct3D设备的交互
在创建好上述对象后,如果窗口的大小是固定的,则需要经历下面的步骤:
- 获取交换链后备缓冲区的ID3D11Texture2D接口对象
- 为后备缓冲区创建渲染目标视图ID3D11RenderTargetView
- 通过D3D设备创建一个ID3D11Texture2D用作深度/模板缓冲区,要求与后备缓冲区等宽高
- 创建深度/模板视图ID3D11DepthStrenilView,绑定刚才创建的2D纹理
- 通过D3D设备上下文,在渲染管线的输出合并阶段设置渲染目标
- 在渲染管线的光栅化阶段设置好渲染的视口区域
对应代码实现如下:
// 重设交换链并且重新创建渲染目标视图
ComPtr<ID3D11Texture2D> backBuffer;
HR(m_pSwapChain->ResizeBuffers(1, m_ClientWidth, m_ClientHeight, DXGI_FORMAT_R8G8B8A8_UNORM, 0));
HR(m_pSwapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), reinterpret_cast<void**>(backBuffer.GetAddressOf())));
HR(m_pd3dDevice->CreateRenderTargetView(backBuffer.Get(), nullptr, m_pRenderTargetView.GetAddressOf()));
// 设置调试对象名
D3D11SetDebugObjectName(backBuffer.Get(), "BackBuffer[0]");
backBuffer.Reset();
D3D11_TEXTURE2D_DESC depthStencilDesc;
depthStencilDesc.Width = m_ClientWidth;
depthStencilDesc.Height = m_ClientHeight;
depthStencilDesc.MipLevels = 1;
depthStencilDesc.ArraySize = 1;
depthStencilDesc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT;
// 要使用 4X MSAA? --需要给交换链设置MASS参数
if (m_Enable4xMsaa)
{
depthStencilDesc.SampleDesc.Count = 4;
depthStencilDesc.SampleDesc.Quality = m_4xMsaaQuality - 1;
}
else
{
depthStencilDesc.SampleDesc.Count = 1;
depthStencilDesc.SampleDesc.Quality = 0;
}
depthStencilDesc.Usage = D3D11_USAGE_DEFAULT;
depthStencilDesc.BindFlags = D3D11_BIND_DEPTH_STENCIL;
depthStencilDesc.CPUAccessFlags = 0;
depthStencilDesc.MiscFlags = 0;
// 创建深度缓冲区以及深度模板视图
HR(m_pd3dDevice->CreateTexture2D(&depthStencilDesc, nullptr, m_pDepthStencilBuffer.GetAddressOf()));
HR(m_pd3dDevice->CreateDepthStencilView(m_pDepthStencilBuffer.Get(), nullptr, m_pDepthStencilView.GetAddressOf()));
// 将渲染目标视图和深度/模板缓冲区结合到管线
m_pd3dImmediateContext->OMSetRenderTargets(1, m_pRenderTargetView.GetAddressOf(), m_pDepthStencilView.Get());
// 设置视口变换
m_ScreenViewport.TopLeftX = 0;
m_ScreenViewport.TopLeftY = 0;
m_ScreenViewport.Width = static_cast<float>(m_ClientWidth);
m_ScreenViewport.Height = static_cast<float>(m_ClientHeight);
m_ScreenViewport.MinDepth = 0.0f;
m_ScreenViewport.MaxDepth = 1.0f;
m_pd3dImmediateContext->RSSetViewports(1, &m_ScreenViewport);
着色器
着色器代码是一份编译后可以在GPU内部运行的程序,不同API有不同的着色器编程语言,D3D使用的为HLSL(OpenGL/ES的为GLSL)。它负责GPU pipeline中可编程阶段,传统的可编程着色器有顶点着色器(vertex shader)和片段着色器(fragment shader/pixel shader)。
着色器代码为类C语言风格,可以有头文件包含、结构体定义、输入输出参数等等类似概念,如果我们要绘制一个简单的三角形可以做如下编程;
定义输入/输出参数结构体:
struct VertexIn
{
float3 pos : POSITION;
float4 color : COLOR;
};
struct VertexOut
{
float4 posH : SV_POSITION;
float4 color : COLOR;
};
顶点着色器代码,这是第一个可编程着色器,输入为3D模型的顶点位置,输出为坐标转换后模型在二维屏幕对应的坐标:
Triangle_VS.hlsl
// 顶点着色器
VertexOut VS(VertexIn vIn)
{
VertexOut vOut;
vOut.posH = float4(vIn.pos, 1.0f);
vOut.color = vIn.color; // 这里alpha通道的值默认为1.0
return vOut;
}
片段着色器代码,输入为顶点着色器的输出,输出为着色后像素信息:
Triangle_PS.hlsl
// 像素着色器
float4 PS(VertexOut pIn) : SV_Target
{
return pIn.color;
}
为使着色器代码可以和我们构建pipeline的代码关联起来,我们需要做如下准备:
- 代码中定义输入结构体
struct VertexPosColor
{
DirectX::XMFLOAT3 pos;
DirectX::XMFLOAT4 color;
static const D3D11_INPUT_ELEMENT_DESC inputLayout[2];
};
- 使用D3D11_INPUT_ELEMENT_DESC结构体来描述待传入结构体中每个成员的具体信息:
typedef struct D3D11_INPUT_ELEMENT_DESC
{
LPCSTR SemanticName; //语义名
UINT SemanticIndex; //语义索引
DXGI_FORMAT Format; //输入格式
UINT InputSlot; //输入槽索引(0-15)
UINT AlignedByteOffset; //初始位置(字节偏移量)
D3D11_INPUT_CLASSIFICATION InputSlotClass; //输入类型
UINT InstanceDataStepRate; //忽略
} D3D11_INPUT_ELEMENT_DESC;
- 初始化inputLayout信息
const D3D11_INPUT_ELEMENT_DESC GameApp::VertexPosColor::inputLayout[2] = {
{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 }
};
- 创建着色器和输入布局
ComPtr<ID3DBlob> blob;
// 创建顶点着色器
HR(CreateShaderFromFile(L"HLSL\\Triangle_VS.cso", L"HLSL\\Triangle_VS.hlsl", "VS", "vs_5_0", blob.ReleaseAndGetAddressOf()));
HR(m_pd3dDevice->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, m_pVertexShader.GetAddressOf()));
// 创建并绑定顶点布局
HR(m_pd3dDevice->CreateInputLayout(VertexPosColor::inputLayout, ARRAYSIZE(VertexPosColor::inputLayout),
blob->GetBufferPointer(), blob->GetBufferSize(), m_pVertexLayout.GetAddressOf()));
// 创建像素着色器
HR(CreateShaderFromFile(L"HLSL\\Triangle_PS.cso", L"HLSL\\Triangle_PS.hlsl", "PS", "ps_5_0", blob.ReleaseAndGetAddressOf()));
HR(m_pd3dDevice->CreatePixelShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, m_pPixelShader.GetAddressOf()));
格式设置完后,我们需要把数据和着色器关联起来
- 创建顶点缓冲区
顶点缓冲区需要通过缓冲区描述D3D11_BUFFER_DESC来创建
typedef struct D3D11_BUFFER_DESC
{
UINT ByteWidth; //数据字节长度
D3D11_USAGE Usage; //CPU和GPU读写权限
UINT BindFlags; //缓冲区类型标志
UINT CPUAccessFlags; //CPU读写权限限定
UINT MiscFlags;
UINT StructureByteStride;
} D3D11_BUFFER_DESC;
USAGE表示了缓冲区对于CPU和GPU的可读写状态,参考下表
2. 按要求配置好顶点缓冲区描述,并创建顶点缓冲区:
// 设置三角形顶点
VertexPosColor vertices[] =
{
{ XMFLOAT3(0.0f, 0.5f, 0.5f), XMFLOAT4(0.0f, 1.0f, 0.0f, 1.0f) },
{ XMFLOAT3(0.5f, -0.5f, 0.5f), XMFLOAT4(0.0f, 0.0f, 1.0f, 1.0f) },
{ XMFLOAT3(-0.5f, -0.5f, 0.5f), XMFLOAT4(1.0f, 0.0f, 0.0f, 1.0f) }
};
// 设置顶点缓冲区描述
D3D11_BUFFER_DESC vbd;
ZeroMemory(&vbd, sizeof(vbd));
vbd.Usage = D3D11_USAGE_IMMUTABLE;
vbd.ByteWidth = sizeof vertices;
vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER;
vbd.CPUAccessFlags = 0;
// 新建顶点缓冲区
D3D11_SUBRESOURCE_DATA InitData;
ZeroMemory(&InitData, sizeof(InitData));
InitData.pSysMem = vertices;
HR(m_pd3dDevice->CreateBuffer(&vbd, &InitData, m_pVertexBuffer.GetAddressOf()));
渲染管线
资源准备完成后需要把对应的资源绑定到pipeline上,以便GPU可以正确使用对应资源;如果渲染不同的场景则需要重新创建对应的资源然后重新绑定到pipeline上。
// ******************
// 给渲染管线各个阶段绑定好所需资源
//
// 输入装配阶段的顶点缓冲区设置
UINT stride = sizeof(VertexPosColor); // 跨越字节数
UINT offset = 0; // 起始偏移量
m_pd3dImmediateContext->IASetVertexBuffers(0, 1, m_pVertexBuffer.GetAddressOf(), &stride, &offset);
// 设置图元类型,设定输入布局
m_pd3dImmediateContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
m_pd3dImmediateContext->IASetInputLayout(m_pVertexLayout.Get());
// 将着色器绑定到渲染管线
m_pd3dImmediateContext->VSSetShader(m_pVertexShader.Get(), nullptr, 0);
m_pd3dImmediateContext->PSSetShader(m_pPixelShader.Get(), nullptr, 0);
// ******************
// 设置调试对象名
//
D3D11SetDebugObjectName(m_pVertexLayout.Get(), "VertexPosColorLayout");
D3D11SetDebugObjectName(m_pVertexBuffer.Get(), "VertexBuffer");
D3D11SetDebugObjectName(m_pVertexShader.Get(), "Trangle_VS");
D3D11SetDebugObjectName(m_pPixelShader.Get(), "Trangle_PS");
绘制
D3D提供了Draw方法,调用该方法后,从输入装配阶段开始,该绘制的进行将会经历一次完整的渲染管线阶段,直到输出合并阶段为止。
virtual void STDMETHODCALLTYPE Draw(
/* [annotation] */
_In_ UINT VertexCount,
/* [annotation] */
_In_ UINT StartVertexLocation) = 0;
通过指定VertexCount和StartVertexLocation的值我们可以按顺序绘制从索引StartVertexLocation到StartVertexLocation + VertexCount - 1的顶点
// 绘制三角形
m_pd3dImmediateContext->Draw(3, 0);
HR(m_pSwapChain->Present(0, 0));
补充: UMD和KMD交互流程分析
D3D一次提交命令集的流程
参考:
https://keenjin.github.io/2020/04/DXRender/
https://github.com/MKXJun/DirectX11-With-Windows-SDK