栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

D3D11

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

D3D11

文章目录

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 context
GFX_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 NDEBUG
	infoManager.Set();
#endif
	if (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 NDEBUG
	DxgiInfoManager infoManager;
#endif
	Microsoft::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 target
pContext->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 pipeline
	const UINT stride = sizeof(Vertex);
	const UINT offset = 0u;
	pContext->IASetVertexBuffers(0u, 1u, pVertexBuffer.GetAddressOf(), &stride, &offset);


	// create pixel shader
	wrl::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 shader
	pContext->PSSetShader(pPixelShader.Get(), nullptr, 0u);


	// create vertex shader
	wrl::ComPtr pVertexShader;
	GFX_THROW_INFO(D3DReadFileToBlob(L"VertexShader.cso", &pBlob));
	GFX_THROW_INFO(pDevice->CreateVertexShader(pBlob->GetBufferPointer(), pBlob->GetBufferSize(), nullptr, &pVertexShader));

	// bind vertex shader
	pContext->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 layout
	pContext->IASetInputLayout(pInputLayout.Get());


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


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


	// configure viewport
	D3D11_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 buffer
const 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 buffer
pContext->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 viewport
D3D11_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 matrix
struct 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 shader
pContext->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

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

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/738445.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号