欢迎您访问365答案网,请分享给你的朋友!
生活常识 学习资料

D3D11

时间:2023-06-02
文章目录

11:D3D初始化篇—— COM(Component Object Model)12:D3D架构 / 交换链13:初始化设备14:调试层15:智能指针16:画一个三角形(上集)17:画一个三角形(下集)18:做实验19:常数缓存流水线行主序矩阵与列主序矩阵 11:D3D初始化篇—— COM(Component Object Model)

非常推荐的文章:
https://zhuanlan.zhihu.com/p/121800182
https://zhuanlan.zhihu.com/p/122482719
https://zhuanlan.zhihu.com/p/457348124

COM:Component Object Model

C++重用是通过源码而非编译好的二进制文件,它不太会关心二进制对象的格式。

那么首先来看,微软为啥要搞这么一个东西。

拿虚表举例,MSVC编译器会将指向虚表的指针放在类的开头(也就是先虚表指针,再类成员),然而对于GNU就是放在类的末尾。这就会导致一个不兼容问题:比如我在GNU环境下想用MSVC编译出来的dll咋办?即C++的不同编译器之间难以交互。

并且就算是同一个编译器,不同版本之间也很难交互。

比如这个问题下的大佬们所说:怎么通俗的解释COM组件? - 知乎 (zhihu.com)、

当自己写的一个dll升级的时候,内部可能增加了成员,导致分配的空间发生变化,从而使得次dll和以前的dll不能兼容。这个就是臭名昭著的dll hell,为此微软最开始想了个很挫的方法,那就是在dll后面加上自己的版本号,如:myDll_1.dll, myDll_2.dll……

作者:Froser
链接:https://www.zhihu.com/question/49433640/answer/116028598

并且像灵剑前辈说的那样,如果库升级了,一个类新增了成员,大小发生变化,那么之前编出来的dll中还是旧的定义下new出来的空间,在新的库中就不能使用。

所以COM的目标是想解决这样一些矛盾,为软件提供二进制层面的接口。我们可以直接让二进制文件被其他软件使用,而无需获取源码。这是一个很稳定的接口,就算重新编译了二进制文件,依赖此的客户端也不会崩溃。

COM的一些好处如下(但也正如灵剑前辈所言,其是一个遗留问题,但它仍然是Windows上的C++的二进制本机代码的动态链接的最佳的选择):

不用区分语言
可以在不同版本编译器、不同编译器甚至不同语言间提供接口,只要这种语言支持函数指针。如果这种语言支持操作内存,那么还可以创建COM物体资源分配
不依赖于任何特定语言,有一套独立的资源分配系统。UUID
构造了一个操作系统级别的Factory,规定所有人的Interface都统一用UUID来标识独立于语言的强大封装线程安全支持分布…

对于C++,我们可以这样制作interface:比如两个接口,一个openable,一个punchable,把它们弄成两个纯虚类,然后拥有这两种方法的就利用C++的多继承机制去继承他们俩。

还记得我们之前在龙书学习笔记中说的,I开头对象:COM 接口都以大写字母“I”作为开头。例如表示命令列表的COM接口为 ID3D12GraphicsCommandList

对于COM接口,首先告诉COM工厂让它创建COM对象,创建完后就给一些COM对象的接口,前缀为I。我们只需专注和接口交互。

当创建COM对象的时候,需要调用工厂相关的函数,让COM去做处理。因此可以想象对于COM对象就不是new和delete了:

在这样改造之后,出问题的还有析构过程~MyClass()或者说delete myClass,因为同一个对象可能返回了很多个接口,有些接口还在被使用,如果其中一个被人delete了,其他接口都会出错,所以又引入了引用计数,来让许多人可以共享同一个对象。

作者:灵剑
链接:https://www.zhihu.com/question/49433640/answer/115952604

COM对象会统计其引用次数,因此在使用完某接口时我们应调用它的 Release 方法而不是 delete ——当COM对象的引用计数为0它将自行释放自己所占用的内存。一开始则是creates初始化引用计数为1,别处使用就AddRef引用计数++,别处不使用了就Release然后引用计数–。

接着还要介绍几个重要的函数:

IUnknown::QueryInterface(REFIID,void) - Win32 apps | Microsoft Docs

Queries a COM object for a pointer to one of its interface; identifying the interface by a reference to its interface identifier (IID)、If the COM object implements the interface, then it returns a pointer to that interface after calling IUnknown::AddRef on it.

那么对于UUID,一种是在头文件中通过类 IActiveDesktop ,这个类名中就包含有 __uuidof 的方法。

而像我们会用到的如:

__uuidof(ID3D12CommandAllocator)

其在d3d12.h中就封装好了uuid:

MIDL_INTERFACE("6102dee4-af59-4b09-b999-b44d73f09b24")ID3D12CommandAllocator : public ID3D12Pageable{public: virtual HRESULT STDMETHODCALLTYPE Reset( void) = 0; };

示例代码(获取桌面壁纸的名称和路径):

#include #include #include #include int main(){CoInitialize(nullptr); // 使用COM前先初始化子系统;不过在D3D中使用的是“轻量COM”,根本无需初始化子系统IActiveDesktop* pDesktop = nullptr;WCHAR wszWallpaper[MAX_PATH]; // 用于储存壁纸的名称的缓存// 创建真正的COM对象CoCreateInstance(CLSID_ActiveDesktop, // 活动的桌面nullptr,CLSCTX_INPROC_SERVER, // 创建对象的上下文。如进程、本地机器、远程上下文等__uuidof(IActiveDesktop), // 接口的UUID,希望函数查询的物体有这个接口reinterpret_cast(&pDesktop));pDesktop->GetWallpaper(wszWallpaper, MAX_PATH, 0); pDesktop->Release();std::wcout << wszWallpaper;CoUninitialize();std::cin.get();return 0;}

输出结果:

再来看一下COM对象的一些细节:

如上图,比如一个COM对象有两个接口,那么它就会有两个虚表,一个虚表对应一组接口的函数。

12:D3D架构 / 交换链

D3D是面向对象的架构,建立在 COM 对象上。

如上图,这些父类都是 Device 。上图黄色的貌似打错了,应该是 IDXGIDEVICE

DXGI 承担的是底层的可以从 D3D 中剥离出去的任务,并且在版本迭代不会像 D3D 那样快。DXGI 现在的工作是遍历设备上所有可用的硬件。显示渲染帧,控制 Gamma 值。这些东西在各版本的 D3D 都不怎么需要变,所以干脆把这些东西独立出来放进 DXGI 里。

我们创建 D3D11 程序时并不意味着我们需要支持 D3D11 显卡。

当你编写 D3D11 应用程序时,如下图所示:

如上图字幕,其方法就是在创建设备时,特性级别选择 9 就行了。

这个不要和 SDKVersion 搞混了:

目标 SDK 版本11,这意味着用户只需要更新其应用程序的 D3D 版本,而与显卡无关。

设备用于创建物体,上下文(ConTEXT 用于绘制,即发出渲染命令并配置渲染管道):

DX11 里的上下文分为即时的和延迟的两种:


所以延迟上下文在多线程中表项良好。但唯一延迟上下文做不到的是查询图形驱动程序,因为它只会建立以下命令列表,在将来的某个时间执行。即时上下文可以查询到信息。(D3D12 似乎已经取消了立即上下文)

13:初始化设备

先来创建 Graphics.h:

#pragma once#include "ChiliWin.h"#include class Graphics{public:Graphics(HWND hWnd);};

因为必须使用窗口的 handle ,所以我们构造函数传一个 HWND

随后在Windows类中添加这个类的成员(由于初始化依赖 HWND ,所以用智能指针管理):

并添加获取的函数:

Graphics& Gfx(); // 得不到图形的时候抛出异常,所以不用noexcept

补全后的头文件是这样的:

#pragma once#include "ChiliWin.h"#include class Graphics{public:Graphics(HWND hWnd);Graphics(const Graphics&) = delete;Graphics& operator=(const Graphics&) = delete;~Graphics();void Endframe();void ClearBuffer(float red, float green, float blue) noexcept;private:ID3D11Device* pDevice = nullptr;IDXGISwapChain* pSwap = nullptr;ID3D11DeviceContext* pContext = nullptr;ID3D11RenderTargetView* pTarget = nullptr;};

对应的 Endframe 函数:

void Graphics::Endframe(){pSwap->Present(1u, 0u);}

这里写 1u 是我们觉得能到 60 帧。如果目标帧率只有 30 帧,就写 2u (60 / 2),以此类推。后一个参数 0u 是不要任何标签的意思。

然后就可以在我们的 App 框架中处理了:

void App::Doframe(){wnd.Gfx().Endframe();}

我们希望使用这个函数:

RenderTargetView 一般是从纹理对象(Texture Object)创建的,但我们也没有。但我们可以这样:

因为交换链可以看作是多个纹理的集合,多个帧缓存的集合。我们可以使用交换链上的函数访问后缓存,这其实就是一个纹理。然后在该纹理上调用函数创建渲染目标视图(RenderTargetView)并渲染。

所以这里我们用 pSwap->GetBuffer 得到 pBackBuffer ,第一个参数 0 是后缓存的索引。随后我们用 pTarget 来保存我们的渲染目标视图(RenderTargetView)。

// gain access to texture subresource in swap chain (back buffer)ID3D11Resource* pBackBuffer = nullptr;pSwap->GetBuffer(0, __uuidof(ID3D11Resource), reinterpret_cast(&pBackBuffer));pDevice->CreateRenderTargetView(pBackBuffer,nullptr,&pTarget);pBackBuffer->Release();

然后的 ClearBuffer 就很简单了:

void Graphics::ClearBuffer(float red, float green, float blue) noexcept{const float color[] = { red,green,blue,1.0f };pContext->ClearRenderTargetView(pTarget, color);}

有了上面的结果,我们就可以用计时器让我们的颜色的红绿通道一直变化了:

void App::Doframe(){const float c = sin(timer.Peek()) / 2.0f + 0.5f;wnd.Gfx().ClearBuffer(c, c, 1.0f);wnd.Gfx().Endframe();}

14:调试层

这一节将为 D3D 子系统设置错误检查和丰富的诊断程序。

上一节我们写了类似的代码:

但是没有给返回值,direct3d函数通常会返回 HRESULT ,如果要发生错误它会抛出一些诊断信息会帮助解决异常。

自然的想法是和一起窗口类的处理一样,之前处理窗口类的时候我们写了这两个宏:

// error exception helper macro#define CHWND_EXCEPT( hr ) Window::Exception( __LINE__,__FILE__,hr )#define CHWND_LAST_EXCEPT() Window::Exception( __LINE__,__FILE__,GetLastError() )

然后处理的时候我们像这样直接抛出异常:

最后的 WinMain 再去处理异常:

try{return App{}.Go();}catch (const ChiliException& e){MessageBox(nullptr, e.what(), e.GetType(), MB_OK | MB_ICONEXCLAMATION);}catch (const std::exception& e){MessageBox(nullptr, e.what(), "Standard Exception", MB_OK | MB_ICONEXCLAMATION);}catch (...){MessageBox(nullptr, "No details available", "Unknown Exception", MB_OK | MB_ICONEXCLAMATION);}

但这里有一个问题,很久以前 DirectX SDK 和 Windows SDK 是分开的,而 DirectX SDK 与 Windows 的 HRESULT 不兼容。你需要通过一个名为 DXerror 的独立库来获取 HRESULT 并将其转换为人类可读的字符串。后来 DirectX API 已合并到 Windows SDK 中,当发生这种情况时他们更改了格式消息让他们也支持报告 DX 的错误,所以不再需要 DXerror(DXERR.LIB) 了。不过仅当您使用 Windows 8 或更高版本时才有效。用 WIN7 的不行,WIN7 也必须使用 DXERR.LIB 。但是问题是当 DirectX API 合并到 Windows SDK 的时候 DXERR.LIB 就被废弃了。而如果你的目标平台是 WIN7 那么仍然需要下载 DXERR.LIB 。并且还出现一个问题是 dxerr 仅支持 Unicode 和 nstring(narrow string)。

最后 Chili 解决了这一切,需要文件如下:

除此之外还在图形类中添加了一些类:

而在 DXERR 中我们感兴趣的是这两个方法:

const CHAR* WINAPI DXGetErrorStringA( _In_ HRESULT hr );void WINAPI DXGetErrorDescriptionA( _In_ HRESULT hr, _Out_cap_(count) CHAR* desc, _In_ size_t count );

两个函数前者提供代表该错误的宏的名称,后者则是错误的描述。

接着又添加了 设备删除异常 类:

然后在 Graphics.cpp 中同样我们定义几个宏来让异常抛出的代码更简洁:

// graphics exception checking/throwing macros (some with dxgi infos)#define GFX_EXCEPT_NOINFO(hr) Graphics::HrException( __LINE__,__FILE__,(hr) )#define GFX_THROW_NOINFO(hrcall) if( FAILED( hr = (hrcall) ) ) throw Graphics::HrException( __LINE__,__FILE__,hr )#ifndef NDEBUG#define GFX_EXCEPT(hr) Graphics::HrException( __LINE__,__FILE__,(hr),infoManager.GetMessages() )#define GFX_THROW_INFO(hrcall) infoManager.Set(); if( FAILED( hr = (hrcall) ) ) throw GFX_EXCEPT(hr)#define GFX_DEVICE_REMOVED_EXCEPT(hr) Graphics::DeviceRemovedException( __LINE__,__FILE__,(hr),infoManager.GetMessages() )#else#define GFX_EXCEPT(hr) Graphics::HrException( __LINE__,__FILE__,(hr) )#define GFX_THROW_INFO(hrcall) GFX_THROW_NOINFO(hrcall)#define GFX_DEVICE_REMOVED_EXCEPT(hr) Graphics::DeviceRemovedException( __LINE__,__FILE__,(hr) )#endif

于是我们就可以直接这样包住我们之前写的代码了(检查他们返回的 HRESULT ):

// create device and front/back buffers, and swap chain and rendering contextGFX_THROW_INFO(D3D11CreateDeviceAndSwapChain(nullptr,D3D_DRIVER_TYPE_HARDWARE,nullptr,swapCreateFlags,nullptr,0,D3D11_SDK_VERSION,&sd,&pSwap,&pDevice,nullptr,&pContext));// gain access to texture subresource in swap chain (back buffer)ID3D11Resource* pBackBuffer = nullptr;GFX_THROW_INFO(pSwap->GetBuffer(0, __uuidof(ID3D11Resource), reinterpret_cast(&pBackBuffer)));GFX_THROW_INFO(pDevice->CreateRenderTargetView(pBackBuffer, nullptr, &pTarget));pBackBuffer->Release();

上面这样写宏,是为了在 Debug 和 Release 下有区别,调试模式下就让 InfoManager 添加信息。

但是在我们的 Endframe 方法里头就有点特殊了:

void Graphics::Endframe(){HRESULT hr;#ifndef NDEBUGinfoManager.Set();#endifif (FAILED(hr = pSwap->Present(1u, 0u))){if (hr == DXGI_ERROR_DEVICE_REMOVED){throw GFX_DEVICE_REMOVED_EXCEPT(pDevice->GetDeviceRemovedReason());}else{throw GFX_EXCEPT(hr);}}}

这里 pSwap->Present 返回的可能是已移除设备的错误代码,这是一个特殊的错误代码,因为它还包括其他信息,所以这里我们的处理方法是又加了一个 pDevice->GetDeviceRemovedReason() 用这个函数来获取。这里产生的原因通常是由于驱动程序崩溃,或者超频你的GPU并搞砸了。所以有错误就抛出异常并获取原因。

其他的处理异常和之前 Windows 下的处理方法类似。

在 Window.h 中我们还添加了额外的一个类(无图形异常):

当我们尝试获取图形类,但没有时将被抛出异常。

当我们刻意写错并在调试层工作时:

除了我们刚刚的异常处理,在底下的output中还会有额外的相关信息:

但是我们希望错误弹出窗口就有足够的信息,于是我们搞了一个新的类(通过一个IDXGIInfoQueue):

DxgiInfoManager.h:

#pragma once#include "ChiliWin.h"#include class DxgiInfoManager{public:DxgiInfoManager();~DxgiInfoManager();DxgiInfoManager(const DxgiInfoManager&) = delete;DxgiInfoManager& operator=(const DxgiInfoManager&) = delete;void Set() noexcept;std::vector GetMessages() const;private:unsigned long long next = 0u;struct IDXGIInfoQueue* pDxgiInfoQueue = nullptr;};

实现中我们会加载这样的一个dll,然后在 DLL 中查找该接口的名称,然后调用函数来处理该接口:

当我们获取消息时,本质上是遍历消息队列:

通过调用 GetMessage ,传递nullptr,这将用索引对于消息的长度赋值给 messageLength

我们还用了这个函数便于中间更改:

这里我们传入了 DXGI_DEBUG_ALL ,但也可以仅调试来自 DXGI 或 D3D 的消息,详见 MSDN:

比如此时我故意写错:

sd.OutputWindow = (HWND)216487;

运行就会有报错窗口:

15:智能指针

之前没用智能指针管理,比如:

// gain access to texture subresource in swap chain (back buffer)ID3D11Resource* pBackBuffer = nullptr;GFX_THROW_INFO(pSwap->GetBuffer(0, __uuidof(ID3D11Resource), reinterpret_cast(&pBackBuffer)));GFX_THROW_INFO(pDevice->CreateRenderTargetView(pBackBuffer, nullptr, &pTarget));pBackBuffer->Release();

如果中间两句抛异常,那么 Release 不被执行,内存泄漏。

于是这里我们直接上 ComPtr(需要包含头文件#include ),运用 RAII:

namespace wrl = Microsoft::WRL;// gain access to texture subresource in swap chain (back buffer)wrl::ComPtr pBackBuffer;GFX_THROW_INFO(pSwap->GetBuffer(0, __uuidof(ID3D11Resource), &pBackBuffer));GFX_THROW_INFO(pDevice->CreateRenderTargetView(pBackBuffer.Get(), nullptr, &pTarget));

而我们之前要传递的部分就用 Get 方法:

并且由于智能指针重载了方法,之前的 reinterpret_cast 也可以直接取地址了。

我们之所以不用比如 unique_ptr 之类的,是因为 unique_ptr 默认的删除器不会调用 COM 的release方法。所以我们用 ComPtr

并且,当要获取接口时,我们需要传递一个 pp (pointer to pointer,指向指针的指针),然后函数将帮助你填充这个接口的指针。而使用 Unique 指针时实际上把指针封装了,就无法得到这个 pp 值。再者,ComPtr还有引用计数等等。综上我们需要用 ComPtr 。

ComPtr 也有一些坑,比如我们之前写的函数:

GFX_THROW_INFO(D3D11CreateDeviceAndSwapChain(nullptr,D3D_DRIVER_TYPE_HARDWARE,nullptr,swapCreateFlags,nullptr,0,D3D11_SDK_VERSION,&sd,&pSwap,&pDevice,nullptr,&pContext));

这里的交换链pSwap之类的都用ComPtr包起来了:

Graphics.h:private:#ifndef NDEBUGDxgiInfoManager infoManager;#endifMicrosoft::WRL::ComPtr pDevice;Microsoft::WRL::ComPtr pSwap;Microsoft::WRL::ComPtr pContext;Microsoft::WRL::ComPtr pTarget;

而这里要传 pp 的时候,如果pSwap指向一个实际的 COM 对象,传入我们写的 &pSwap ,它将首先调用释放函数(release)然后再返回地址。这很合理,如果要填充它(传 pp 不就是为了填充这个指针吗),就必须先把以前的东西释放掉。这样就不会泄漏内存资源。

但是有时你只想获取要获取指针的指针的地址,但不想填充指针:

那么就不能使用这个 operator & (不然就把资源释放了,不是我们想要的),可以使用 GetAddressOf 方法:

16:画一个三角形(上集)

可以从官方看流水线:
https://docs.microsoft.com/zh-cn/windows-hardware/drivers/display/pipelines-for-direct3d-version-11

关于缓冲:
https://docs.microsoft.com/en-us/windows/win32/direct3d11/overviews-direct3d-11-resources-buffers-intro
https://docs.microsoft.com/zh-cn/windows/win32/direct3d11/overviews-direct3d-11-resources-buffers-intro

这里 IA 就是渲染管线的 Input Assembler (输入装配阶段)的意思,可以看到第三个参数是一个 pp 表示可以指定多个缓冲区(相当于指针数组了):

IASetVertexBuffers文档:
https://docs.microsoft.com/zh-cn/windows/win32/api/d3d11/nf-d3d11-id3d11devicecontext-iasetvertexbuffers

我们还必须要一个 vertex shader,否则会报错。着色器的输出输入必须用语义标记。

我们可以直接 Build ,VS内置编译,会将其编译成 cso 形式,还会报错之类的:

1>compilation object save succeeded; see E:MyD3D11LearnD3D11_Chili_TutorialD3D11_Chili_TutorialbinWin32DebugVertexShader.cso

我们链接#pragma comment(lib,"D3DCompiler.lib"),可以使用它在运行时编译着色器。不过我们现在只需要使用它的着色器加载功能就行了。还需要包含头文件#include

wrl::ComPtr pBlob;GFX_THROW_INFO(D3DReadFileToBlob(L"VertexShader.cso", &pBlob));

但是我们这样写,还需要配置每次编译的 cso 文件输出位置才行(hlsl文件右键->Properties)。


改成了:$(ProjectDir)%(Filename).cso

还可以选择着色器类型:

17:画一个三角形(下集)

D3D允许我们渲染到离线目标 Off-Screen Target 上。

这里有个坑就是上讲将 ComPtr 取地址会释放的坑,所以我们要用 GetAddressOf 而不能用 & :

// bind render targetpContext->OMSetRenderTargets(1u, pTarget.GetAddressOf(), nullptr);

当然这一节我们先写好 pixel shader:

float4 main() : SV_TARGET{return float4(1.0f, 1.0f, 1.0f, 1.0f);}

这里我们还必须要指定 D3D11_VIEWPORT 。从 NDC 到 屏幕空间,如下图:
D3D11_VIEWPORT 可以不满屏幕空间,比如上图的黄色框框只占屏幕的四分之一,也可以用作 D3D11_VIEWPORT 。

我们还需要设置渲染图元:
参考链接:
https://docs.microsoft.com/zh-cn/windows/win32/direct3d11/d3d10-graphics-programming-guide-primitive-topologies

// Set primitive topology to triangle list (groups of 3 vertices)pContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);


要让着色器正确解析 buffer 数据,还需要 input layout,我们顶点着色器输入是这样的:
float2 pos : Position

这里默认就是 Position0 了,这个0就对应着

// input (vertex) layout (2d position only)wrl::ComPtr pInputLayout;const D3D11_INPUT_ELEMENT_DESC ied[] ={{ "Position",0,DXGI_FORMAT_R32G32_FLOAT,0,0,D3D11_INPUT_PER_VERTEX_DATA,0 },};GFX_THROW_INFO(pDevice->CreateInputLayout(ied, (UINT)std::size(ied),pBlob->GetBufferPointer(),pBlob->GetBufferSize(),&pInputLayout));

D3D11_INPUT_ELEMENT_DESC 的第二个参数(对应上面代码"Position"后面那个0)。

比如:
顶点着色器代码:

float4 main( float2 pos : Hbh2 ) : SV_Position{return float4(pos.x,pos.y,0.0f,1.0f);}

input layout:

const D3D11_INPUT_ELEMENT_DESC ied[] ={{ "Hbh",2,DXGI_FORMAT_R32G32_FLOAT,0,0,D3D11_INPUT_PER_VERTEX_DATA,0 },};

这样写照样是对的。

查阅MSDN我们知道:

可以看见,我们这里的 BytecodeLength 写的是 pBlob->GetBufferSize(),,其实这里的 BytecodeLength 就是为了检查数据描述符与着色器确实匹配。

画一个三角形函数的所有代码:

void Graphics::DrawTestTriangle(){namespace wrl = Microsoft::WRL;HRESULT hr;struct Vertex{float x;float y;};// create vertex buffer (1 2d triangle at center of screen)const Vertex vertices[] ={{ 0.0f,0.5f },{ 0.5f,-0.5f },{ -0.5f,-0.5f },};wrl::ComPtr pVertexBuffer;D3D11_BUFFER_DESC bd = {};bd.BindFlags = D3D11_BIND_VERTEX_BUFFER;bd.Usage = D3D11_USAGE_DEFAULT;bd.CPUAccessFlags = 0u;bd.MiscFlags = 0u;bd.ByteWidth = sizeof(vertices);bd.StructureByteStride = sizeof(Vertex);D3D11_SUBRESOURCE_DATA sd = {};sd.pSysMem = vertices;GFX_THROW_INFO(pDevice->CreateBuffer(&bd, &sd, &pVertexBuffer));// Bind vertex buffer to pipelineconst UINT stride = sizeof(Vertex);const UINT offset = 0u;pContext->IASetVertexBuffers(0u, 1u, pVertexBuffer.GetAddressOf(), &stride, &offset);// create pixel shaderwrl::ComPtr pPixelShader;wrl::ComPtr pBlob;GFX_THROW_INFO(D3DReadFileToBlob(L"PixelShader.cso", &pBlob));GFX_THROW_INFO(pDevice->CreatePixelShader(pBlob->GetBufferPointer(), pBlob->GetBufferSize(), nullptr, &pPixelShader));// bind pixel shaderpContext->PSSetShader(pPixelShader.Get(), nullptr, 0u);// create vertex shaderwrl::ComPtr pVertexShader;GFX_THROW_INFO(D3DReadFileToBlob(L"VertexShader.cso", &pBlob));GFX_THROW_INFO(pDevice->CreateVertexShader(pBlob->GetBufferPointer(), pBlob->GetBufferSize(), nullptr, &pVertexShader));// bind vertex shaderpContext->VSSetShader(pVertexShader.Get(), nullptr, 0u);// input (vertex) layout (2d position only)wrl::ComPtr pInputLayout;const D3D11_INPUT_ELEMENT_DESC ied[] ={{ "Position",0,DXGI_FORMAT_R32G32_FLOAT,0,0,D3D11_INPUT_PER_VERTEX_DATA,0 },};GFX_THROW_INFO(pDevice->CreateInputLayout(ied, (UINT)std::size(ied),pBlob->GetBufferPointer(),pBlob->GetBufferSize(),&pInputLayout));// bind vertex layoutpContext->IASetInputLayout(pInputLayout.Get());// bind render targetpContext->OMSetRenderTargets(1u, pTarget.GetAddressOf(), nullptr);// Set primitive topology to triangle list (groups of 3 vertices)pContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);// configure viewportD3D11_VIEWPORT vp;vp.Width = 800;vp.Height = 600;vp.MinDepth = 0;vp.MaxDepth = 1;vp.TopLeftX = 0;vp.TopLeftY = 0;pContext->RSSetViewports(1u, &vp);GFX_THROW_INFO_ONLY(pContext->Draw((UINT)std::size(vertices), 0u));}

18:做实验

默认情况下,渲染管线将进行背面剔除(back-face culling),三角形点顺序逆时针被认为是背面,会被剔除。

可以看到我们之前的顶点:

const Vertex vertices[] ={{ 0.0f,0.5f },{ 0.5f,-0.5f },{ -0.5f,-0.5f },};

是顺时针的,所以不会被剔除。

这里我们想得到彩色的三角形,我们给顶点加颜色属性:

struct Vertex{struct{float x;float y;} pos;struct{unsigned char r;unsigned char g;unsigned char b;unsigned char a;} color;};Vertex vertices[] ={{ 0.0f,0.5f,255,0,0,0 },{ 0.5f,-0.5f,0,255,0,0 },{ -0.5f,-0.5f,0,0,255,0 },{ -0.3f,0.3f,0,255,0,0 },{ 0.3f,0.3f,0,0,255,0 },{ 0.0f,-0.8f,255,0,0,0 },};

着色器我们将这样写:

VS:

struct VSOut{float3 color : Color;float4 pos : SV_Position;};VSOut main( float2 pos : Position,float3 color : Color ){VSOut vso;vso.pos = float4(pos.x,pos.y,0.0f,1.0f);vso.color = color;return vso;}

PS:

float4 main( float3 color : Color ) : SV_Target{return float4( color,1.0f );}

由于 PS 中我们只想传一个 Color,不需要 Position,所以VS中我们的VSOut内部顺序是先 color 再 pos,不能调换,否则 PS 解析的第一个就不是 color 了就会出错。

同时我们在VS中指定了float3 color : Color,d3d就会把你的输入转换为这个指定的类型。但是我们还可以指定一些规则,通过我们指定的类型:

UINT将会被转换为确切的整数值,但UNORM将会把输入类型归一化。比如此时输入255就会被转换为1.0、这正是我们想要的(颜色在 0 到 1 浮点的范围)。

所以我们在指定 input layout 的时候指定的是DXGI_FORMAT_R8G8B8A8_UNORM:

const D3D11_INPUT_ELEMENT_DESC ied[] ={{ "Position",0,DXGI_FORMAT_R32G32_FLOAT,0,0,D3D11_INPUT_PER_VERTEX_DATA,0 },{ "Color",0,DXGI_FORMAT_R8G8B8A8_UNORM,0,8u,D3D11_INPUT_PER_VERTEX_DATA,0 },};

为了避免顶点重复,我们这一节引入 index buffer:

// create index bufferconst unsigned short indices[] ={0,1,2,0,2,3,0,4,1,2,1,5,};wrl::ComPtr pIndexBuffer;D3D11_BUFFER_DESC ibd = {};ibd.BindFlags = D3D11_BIND_INDEX_BUFFER;ibd.Usage = D3D11_USAGE_DEFAULT;ibd.CPUAccessFlags = 0u;ibd.MiscFlags = 0u;ibd.ByteWidth = sizeof( indices );ibd.StructureByteStride = sizeof( unsigned short );D3D11_SUBRESOURCE_DATA isd = {};isd.pSysMem = indices;GFX_THROW_INFO( pDevice->CreateBuffer( &ibd,&isd,&pIndexBuffer ) );// bind index bufferpContext->IASetIndexBuffer( pIndexBuffer.Get(),DXGI_FORMAT_R16_UINT,0u );

drawcall也就要从原来的 pContext->Draw((UINT)std::size(vertices), 0u)改为:
pContext->DrawIndexed( (UINT)std::size( indices ),0u,0u )

此时我们能输出一个漂亮的六边形了:

测试视口:

我们的视口代码是这样的

// configure viewportD3D11_VIEWPORT vp;vp.Width = 800;vp.Height = 600;vp.MinDepth = 0;vp.MaxDepth = 1;vp.TopLeftX = 0;vp.TopLeftY = 0;pContext->RSSetViewports(1u, &vp);

我们改一下:

vp.Width = 400;vp.Height = 300;

然后就会发现只在左上角渲染了(全屏 800 * 600,视口大小 400 * 300,视口左上角指定的为 0, 0 ):

全屏的大小在我们之前的 App 框架中指定:

App::App():wnd(800, 600, "The Donkey Fart Box"){}

19:常数缓存

对于每一次移动,我们当然可以在CPU计算好再传给GPU,但是,对于上千个点,这样做每次都要传上千个数据,占用大量带宽。所以一般我们用 dynamic constant 数据,每次通过变换来移动。即改变上千个点不如改变只有16个数的变换矩阵。

在测试代码中我们是每一帧传输的,但是真正的引擎是不会这样做的,那只是测试代码。

做法是用一种叫做 着色器常数缓存(shader constant buffer) 的东西。这将允许我们将一些常量值绑定到着色器阶段,可用于该着色器的每次调用。

代码如下:

// create constant buffer for transformation matrixstruct ConstantBuffer{struct{float element[4][4];} transformation;};const ConstantBuffer cb ={{(3.0f / 4.0f) * std::cos(angle),std::sin(angle),0.0f,0.0f,(3.0f / 4.0f) * -std::sin(angle),std::cos(angle),0.0f,0.0f,0.0f,0.0f,1.0f,0.0f,0.0f,0.0f,0.0f,1.0f,}};wrl::ComPtr pConstantBuffer;D3D11_BUFFER_DESC cbd;cbd.BindFlags = D3D11_BIND_CONSTANT_BUFFER;cbd.Usage = D3D11_USAGE_DYNAMIC;cbd.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;cbd.MiscFlags = 0u;cbd.ByteWidth = sizeof(cb);cbd.StructureByteStride = 0u;D3D11_SUBRESOURCE_DATA csd = {};csd.pSysMem = &cb;GFX_THROW_INFO(pDevice->CreateBuffer(&cbd, &csd, &pConstantBuffer));

基本上就是围绕 Z 轴旋转的矩阵。
(angle是我们传入的参数void Graphics::DrawTestTriangle(float angle))

这里我们把 USAGE 设置为动态,这将通常是您使用常数缓存的方式:
(本地文档:file:///E:/%E5%AD%A6%E4%B9%A0%E8%B5%84%E6%96%99/%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%9B%BE%E5%BD%A2%E5%AD%A6%E5%AD%A6%E4%B9%A0%E8%B5%84%E6%96%99/D3D11/%E6%96%B0%E5%BB%BA%E6%96%87%E4%BB%B6%E5%A4%B9/DX11+%E4%B8%AD%E8%AF%91.pdf)

注:参考文档https://docs.microsoft.com/en-us/windows/win32/direct3d11/overviews-direct3d-11-resources-subresources,一个buffer就是一个简单的 subresource ,Textures 则有一点复杂。

创建一个buffer通常就是三步:

然后绑定一下:

// bind constant buffer to vertex shaderpContext->VSSetConstantBuffers(0u, 1u, pConstantBuffer.GetAddressOf());

最后就是 shader 里头用一下了:
VS中先定义一下,cbuffer是hlsl关键字:

cbuffer CBuf{matrix transform;};

然后注意D3D中矩阵乘法是右乘,向量在左边矩阵在右边:

VSOut main( float2 pos : Position,float3 color : Color ){VSOut vso;vso.pos = mul(float4(pos.x,pos.y,0.0f,1.0f), transform);vso.color = color;return vso;}

还有一个小细节:CPU里的二维数组按行存储(row-major ordering),但GPU(HLSL中)则是按列存储(column-major ordering)。所以我们应该传入的时候进行一波转置,但是我们也可以用另一种方法,就是告诉 HLSL ,这个矩阵是按行排列的:

但是缺点是在 GPU 上乘以行主矩阵要比列主矩阵稍慢。但这样我们更轻松。将来我们实际上要在GPU获取矩阵之前将其转置。但现在我们就这样做。

流水线

参考:https://zhuanlan.zhihu.com/p/259535556

行主序矩阵与列主序矩阵

参考:https://www.cnblogs.com/X-Jun/p/9808727.html

为了使效率最高,对于列主序存储的矩阵我们要“右乘”(即矩阵放在右边,这样可以刚好取出连续的一块空间),对于行主序存储的矩阵我们要“左乘”。

Copyright © 2016-2020 www.365daan.com All Rights Reserved. 365答案网 版权所有 备案号:

部分内容来自互联网,版权归原作者所有,如有冒犯请联系我们,我们将在三个工作时内妥善处理。