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,一种是在头文件
而像我们会用到的如:
__uuidof(ID3D12CommandAllocator)
其在d3d12.h中就封装好了uuid:
MIDL_INTERFACE("6102dee4-af59-4b09-b999-b44d73f09b24")ID3D12CommandAllocator : public ID3D12Pageable{public: virtual HRESULT STDMETHODCALLTYPE Reset( void) = 0; };
示例代码(获取桌面壁纸的名称和路径):
#include
输出结果:
再来看一下COM对象的一些细节:
如上图,比如一个COM对象有两个接口,那么它就会有两个虚表,一个虚表对应一组接口的函数。
D3D是面向对象的架构,建立在 COM 对象上。
如上图,这些父类都是 Device 。上图黄色的貌似打错了,应该是 IDXGIDEVICE
DXGI 承担的是底层的可以从 D3D 中剥离出去的任务,并且在版本迭代不会像 D3D 那样快。DXGI 现在的工作是遍历设备上所有可用的硬件。显示渲染帧,控制 Gamma 值。这些东西在各版本的 D3D 都不怎么需要变,所以干脆把这些东西独立出来放进 DXGI 里。
我们创建 D3D11 程序时并不意味着我们需要支持 D3D11 显卡。
当你编写 D3D11 应用程序时,如下图所示:
如上图字幕,其方法就是在创建设备时,特性级别选择 9 就行了。
这个不要和 SDKVersion 搞混了:
目标 SDK 版本11,这意味着用户只需要更新其应用程序的 D3D 版本,而与显卡无关。
设备用于创建物体,上下文(ConTEXT 用于绘制,即发出渲染命令并配置渲染管道):
DX11 里的上下文分为即时的和延迟的两种:
所以延迟上下文在多线程中表项良好。但唯一延迟上下文做不到的是查询图形驱动程序,因为它只会建立以下命令列表,在将来的某个时间执行。即时上下文可以查询到信息。(D3D12 似乎已经取消了立即上下文)
先来创建 Graphics.h:
#pragma once#include "ChiliWin.h"#include
因为必须使用窗口的 handle ,所以我们构造函数传一个 HWND
随后在Windows类中添加这个类的成员(由于初始化依赖 HWND ,所以用智能指针管理):
并添加获取的函数:
Graphics& Gfx(); // 得不到图形的时候抛出异常,所以不用noexcept
补全后的头文件是这样的:
#pragma once#include "ChiliWin.h"#include
对应的 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
然后的 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
上面这样写宏,是为了在 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
实现中我们会加载这样的一个dll,然后在 DLL 中查找该接口的名称,然后调用函数来处理该接口:
当我们获取消息时,本质上是遍历消息队列:
通过调用 GetMessage ,传递nullptr,这将用索引对于消息的长度赋值给 messageLength
我们还用了这个函数便于中间更改:
这里我们传入了 DXGI_DEBUG_ALL ,但也可以仅调试来自 DXGI 或 D3D 的消息,详见 MSDN:
比如此时我故意写错:
sd.OutputWindow = (HWND)216487;
运行就会有报错窗口:
之前没用智能指针管理,比如:
// gain access to texture subresource in swap chain (back buffer)ID3D11Resource* pBackBuffer = nullptr;GFX_THROW_INFO(pSwap->GetBuffer(0, __uuidof(ID3D11Resource), reinterpret_cast
如果中间两句抛异常,那么 Release 不被执行,内存泄漏。
于是这里我们直接上 ComPtr(需要包含头文件#include
namespace wrl = Microsoft::WRL;// gain access to texture subresource in swap chain (back buffer)wrl::ComPtr
而我们之前要传递的部分就用 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
而这里要传 pp 的时候,如果pSwap指向一个实际的 COM 对象,传入我们写的 &pSwap ,它将首先调用释放函数(release)然后再返回地址。这很合理,如果要填充它(传 pp 不就是为了填充这个指针吗),就必须先把以前的东西释放掉。这样就不会泄漏内存资源。
但是有时你只想获取要获取指针的指针的地址,但不想填充指针:
那么就不能使用这个 operator & (不然就把资源释放了,不是我们想要的),可以使用 GetAddressOf 方法:
可以从官方看流水线:
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
但是我们这样写,还需要配置每次编译的 cso 文件输出位置才行(hlsl文件右键->Properties)。
改成了:$(ProjectDir)%(Filename).cso
还可以选择着色器类型:
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
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
默认情况下,渲染管线将进行背面剔除(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
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
基本上就是围绕 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
为了使效率最高,对于列主序存储的矩阵我们要“右乘”(即矩阵放在右边,这样可以刚好取出连续的一块空间),对于行主序存储的矩阵我们要“左乘”。