前言
本文运用D3D游戏开发实战(红龙书)第六章的绘制盒子例程,详细探讨一下这个绘制立方体的程序。理解这个程序后,我们再进一步探讨如何在D3D程序中进行一些功能的修正,从而改出一个基于D3D的渲染程序。
零 BoxApp的调试
按照书本指导新建项目(注意书本中的VS版本把创建的项目称为C++Win32应用,而我的版本已经变成了C++Windows桌面应用而无法查到创建win32项目的选项),这里用的是VS2021,和当初的范例已经有了较大的差异,导致第一个程序就差点微笑中打出gg。
新建项目myd3d,可以看到其中有相当量的初始代码。
可以看这篇文章D3D游戏开发实战,是大佬对红龙书的一个翻+抄,可以找到这个程序的调试过程。
链接DirectX库
如果严格按照书中的步骤,这部分#pragma指令是已经包含在Common/d3dApp.h当中的。所以无需把这段代码敲到任何地方;
添加源代码并构建项目
这部分很容易因为VS的文件操作不当出现错误。如果复制文件到VS左侧的层次资源管理器窗口,其实会传达所传递文件的引用而非文件的复制,也就是说实际上某个源文件想要访问同文件夹下的某个地址的兄弟文件,会发现那个路径下根本没有兄弟。这样就导致了文件之间的互引出现问题;而如果直接在文件资源管理器中进行复制,
这里建议很多时候配环境如果配出问题了可以大胆删掉从头来过,避免一些改动roll back的时候又造成更多问题(第一次配删了六七次)。
可以看到boxApp.cpp当中的include这样写道:
这其实规定了boxApp.cpp和所引用的其他头文件的相对位置。
我简单数了一下,首先复制一份git下来的d3d12book到任意位置,如果创建项目时根目录选择d3d12book,那么最终设置好后BoxApp.cpp的include相对路径正好是上图所示。
所以千万注意,如果以d3d12book文件夹为根目录创建了项目,不要复制任何Common中的头文件或者源文件。源文件已经能够根据相对位置直接访问Common(当然就算复制了仔细想想也不会出什么问题)。需要拷贝的是d3d12book\Chapter 6 Drawing in Direct3D\Box下的shader和BoxApp.h。我们在文件资源管理器下复制粘贴。
调试
复制后VS资源管理器中并没有显示新的文件,我们复制文件资源管理器内的“拷贝位置”的BoxApp.h和Shader。
Shader文件并不需要引入到VS资源管理器,可以直接通过相对路径来访问。
然后用同样的方式来添加Common中的各个头文件与源文件,这里也可以采用书中的办法,使用add items。注意,Common中的文件没有复制到myd3d项目文件当中,而是通过相对路径访问的——因此要把Common位置的文件复制到VS资源控制器内。
对比书中,完成后的VS资源管理器并没有自动生成的myd3d.cpp文件,但是运行时项目将会搜索同名的myd3d.cpp,找不到就会报错。我们姑且保留;同时我的Shader放在d3d12book\myd3d\myd3d位置,和BoxApp.cpp等源文件同级。
这里运行报错“&需求左值”,解决方案
我这里做了两处修改,一处是上面链接对应的文章所述,要把property-C+±语言-符合模式改为否,采取宽松的编译器审查标准,用于适配VS2015;
另一处是Debug设置,戳下图的Debug下箭-配置管理器,把下面的平台改为Win32(我在创建项目的时候死活也没有找到创建Win32项目的选项)
调试结果如下,还是很好看的:
一 BoxApp的规范
1.1 D3DApp类
D3DApp是一个规范了后续的程序如何工作的基类,它有六个基本的虚拟函数,见龙书4.5.3框架方法。 我们要做的就是编写一个派生类继承D3DApp类,override它的框架方法,包括MsgProc,CreateRtvAndDsvDescriptorHeaps,OnResize,Initialize,
D3DApp类有600多行,见龙书4.5.1D3DApp类的介绍。比较底层,完成了包括WinApi、和设备建立连接等工作,我们简单梳理一下它定义了哪些函数,他们分别干什么。
//D3DApp.h
class D3DApp
{
protected:
D3DApp(HINSTANCE hInstance);//用HINSTANCE的构造器
D3DApp(const D3DApp& rhs) = delete;
D3DApp& operator=(const D3DApp& rhs) = delete;//赋值构造器
virtual ~D3DApp();
public:
static D3DApp* GetApp();
//在D3DApp.cpp里维护了一个D3DApp类的指针,叫mApp
//事实上这个全局变量被做成了this。构造器构造D3DApp对象的时候就会mApp=this,而不能构造两次D3DApp对象,否则mApp == nullptr会被assert拦下抛出异常
//GetApp()返回mApp。注意mApp是一个指针
HINSTANCE AppInst()const;
//返回D3DApp对象的HINSTANCE实例。
//由于构造器要用到一个HINSTANCE参数做输入,构造时mhAppInst用于存放这个参数,构造完成后也保存。
HWND MainWnd()const;
//创建窗口CreateWindow的结果存在mhMainWnd局部变量,用于返回mhMainWnd获得窗口
float AspectRatio()const;
//return static_cast<float>(mClientWidth) / mClientHeight;宽高比
//下面的两个函数是4xMsaaState的读写器,
//查到这里的时候我透,我发现原来消息处理机制里面有一个按下F2开启抗锯齿的设计
//4xMsaaState是一个bool量用于表达四倍超采样抗锯齿是否开启,但是这里按了F2发现出错了后面在1.ex当中谈
bool Get4xMsaaState()const;
void Set4xMsaaState(bool value);
int Run();
virtual bool Initialize();
virtual LRESULT MsgProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
protected:
virtual void CreateRtvAndDsvDescriptorHeaps();
virtual void OnResize();
virtual void Update(const GameTimer& gt)=0;
virtual void Draw(const GameTimer& gt)=0;
// Convenience overrides for handling mouse input.
virtual void OnMouseDown(WPARAM btnState, int x, int y){
}
virtual void OnMouseUp(WPARAM btnState, int x, int y) {
}
virtual void OnMouseMove(WPARAM btnState, int x, int y){
}
protected:
bool InitMainWindow();
bool InitDirect3D();
void CreateCommandObjects();
void CreateSwapChain();
void FlushCommandQueue();
ID3D12Resource* CurrentBackBuffer()const;
D3D12_CPU_DESCRIPTOR_HANDLE CurrentBackBufferView()const;
D3D12_CPU_DESCRIPTOR_HANDLE DepthStencilView()const;
void CalculateFrameStats();
void LogAdapters();
void LogAdapterOutputs(IDXGIAdapter* adapter);
void LogOutputDisplayModes(IDXGIOutput* output, DXGI_FORMAT format);
protected:
static D3DApp* mApp;
HINSTANCE mhAppInst = nullptr; // application instance handle
HWND mhMainWnd = nullptr; // main window handle
bool mAppPaused = false; // is the application paused?
bool mMinimized = false; // is the application minimized?
bool mMaximized = false; // is the application maximized?
bool mResizing = false; // are the resize bars being dragged?
bool mFullscreenState = false;// fullscreen enabled
// Set true to use 4X MSAA (?.1.8). The default is false.
bool m4xMsaaState = false; // 4X MSAA enabled
UINT m4xMsaaQuality = 0; // quality level of 4X MSAA
// Used to keep track of the 揹elta-time?and game time (?.4).
GameTimer mTimer;
Microsoft::WRL::ComPtr<IDXGIFactory4> mdxgiFactory;
Microsoft::WRL::ComPtr<IDXGISwapChain> mSwapChain;
Microsoft::WRL::ComPtr<ID3D12Device> md3dDevice;
Microsoft::WRL::ComPtr<ID3D12Fence> mFence;
UINT64 mCurrentFence = 0;
Microsoft::WRL::ComPtr<ID3D12CommandQueue> mCommandQueue;
Microsoft::WRL::ComPtr<ID3D12CommandAllocator> mDirectCmdListAlloc;
Microsoft::WRL::ComPtr<ID3D12GraphicsCommandList> mCommandList;
static const int SwapChainBufferCount = 2;
int mCurrBackBuffer = 0;
Microsoft::WRL::ComPtr<ID3D12Resource> mSwapChainBuffer[SwapChainBufferCount];
Microsoft::WRL::ComPtr<ID3D12Resource> mDepthStencilBuffer;
Microsoft::WRL::ComPtr<ID3D12DescriptorHeap> mRtvHeap;
Microsoft::WRL::ComPtr<ID3D12DescriptorHeap> mDsvHeap;
D3D12_VIEWPORT mScreenViewport;
D3D12_RECT mScissorRect;
UINT mRtvDescriptorSize = 0;
UINT mDsvDescriptorSize = 0;
UINT mCbvSrvUavDescriptorSize = 0;
// Derived class should set these in derived constructor to customize starting values.
std::wstring mMainWndCaption = L"d3d App";
D3D_DRIVER_TYPE md3dDriverType = D3D_DRIVER_TYPE_HARDWARE;
DXGI_FORMAT mBackBufferFormat = DXGI_FORMAT_R8G8B8A8_UNORM;
DXGI_FORMAT mDepthStencilFormat = DXGI_FORMAT_D24_UNORM_S8_UINT;
int mClientWidth = 800;
int mClientHeight = 600;
};
1.ex 一个解决不了的问题
非常奇怪,我查了BoxApp和D3DApp的MsgProc,确信这个程序的键盘消息处理只有这么多:
case WM_KEYUP:
if(wParam == VK_ESCAPE)
{
PostQuitMessage(0);
}
else if((int)wParam == VK_F2)
Set4xMsaaState(!m4xMsaaState);
而且上述的报错的确是在按下F2,抬起键盘导致WM_KEYUP发生的。
报错发生在:
ThrowIfFailed(mdxgiFactory->CreateSwapChain(
mCommandQueue.Get(),
&sd,
mSwapChain.GetAddressOf()));
mSwapChain求地址这一处。
这是因为Set4xMsaaState(!m4xMsaaState)会重新创建交换链:
void D3DApp::Set4xMsaaState(bool value)
{
if(m4xMsaaState != value)
{
m4xMsaaState = value;
// 用新的多采样设定来重建swap chain和buffer
CreateSwapChain();
OnResize();
}
}
MSDN的D3D开发文档对于IDXGIFactory::CreateSwapChain method (dxgi.h)有这样的记述:
[Starting with Direct3D 11.1, we recommend not to use CreateSwapChain anymore to create a swap chain. Instead, use CreateSwapChainForHwnd, CreateSwapChainForCoreWindow, or CreateSwapChainForComposition depending on how you want to create the swap chain.]希望用CreateSwapChainForHwnd等来代替CreateSwapChain。
//新旧函数的对比
HRESULT CreateSwapChainForHwnd(
[in] IUnknown *pDevice,
[in] HWND hWnd,
[in] const DXGI_SWAP_CHAIN_DESC1 *pDesc,
[in, optional] const DXGI_SWAP_CHAIN_FULLSCREEN_DESC *pFullscreenDesc,
[in, optional] IDXGIOutput *pRestrictToOutput,
[out] IDXGISwapChain1 **ppSwapChain
);
HRESULT CreateSwapChain(
[in] IUnknown *pDevice,
[in] DXGI_SWAP_CHAIN_DESC *pDesc,
[out] IDXGISwapChain **ppSwapChain
);
全篇改成CreateSwapChainForHwnd的过程中,由于DXGI_SWAP_CHAIN_DESC1的内容更少,删去了DXGI_SWAP_CHAIN_DESC sd当中的很多信息,包括
//sd.BufferDesc.RefreshRate.Numerator = 60;
//sd.BufferDesc.RefreshRate.Denominator = 1;
//sd.BufferDesc.Format = mBackBufferFormat;
//sd.BufferDesc.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED;
//sd.BufferDesc.Scaling = DXGI_MODE_SCALING_UNSPECIFIED;
最后导致程序甚至调不出窗口,CreateSwapChain一直报错。
我回溯到原本的代码,直到最后这个程序依然没有提供F2开启MSAA的Bug的解决方案。
二 D3DApp pipeline工作逻辑|必要的着色阶段
2.0 输入装配器阶段
包括三部分工作:输入顶点、设定图元拓扑和用索引指定图元。
2.1 顶点着色器阶段的必要工作(MVP transform)
世界变换model/world transform
一个美术建模好的物体,其顶点坐标是相对于物体自身的原点的,比如立方体的一个顶点为(0,0,0),剩下的顶点为(0,0,1) (0,1,0)之类。因此为了在屏幕上显示一个物体,首先要把局部坐标计算成世界坐标,这个过程通过给每个顶点右乘一个矩阵W来实现。这个矩阵叫做世界矩阵world matrix。
世界矩阵有两种办法获知:
情况一
我们知道局部原点的世界坐标Q
x轴从局部的(1,0,0)对应成世界坐标的
y轴从局部的(0,1,0)对应成世界坐标的
z轴从局部的(0,0,1)对应成世界坐标的
那么只要把他们都写成齐次坐标,原点是点另外三个是向量,排列就能形成世界矩阵:
注意上面的向量uvw都要单位化(除了原点坐标)
但是有时候求局部原点相对坐标也就算了,求三个轴的相对向量还是挺不直观的,所以也用情况二:
情况二
同上我们知道原点相对坐标Q,这样就能得到平移矩阵T
同时我们更是容易知道物体经过了怎样的缩放。
如果用三个轴的转角来描述旋转(欧拉角描述),有下面的结论但注意这里是右手系的结论,左手系Ry里sin 和 -sin要换一下
那么我们自然能推出局部坐标如何还原到世界坐标的旋转矩阵R(如果对三个轴先后旋转,只需要先后乘上对应的三个矩阵)
TIPS1 旋转矩阵的逆=其转置
TIPS2 欧拉角描述并不能描述所有的旋转过程
最后缩放阵S是更加好算的,那么我们的W=SRT,最终让顶点右乘矩阵W时,相当于先缩放、再旋转、再平移,从而回到世界坐标,并且归还了原点。
世界变换就是这样完成的。
观察空间变换view transform
注意不要和视口变换混淆,在顶点着色阶段,我们尚没有引入视口,仅仅在计算各个顶点的数据。
观察空间变换在龙书的翻译中称为取景变换,闫令琪老师的Games101称之为视图变换(闫老师同时把世界变换称之为模型变换model transform),无论是取景还是视图,他们的英文名称都是view transform。
Games101当中这样定义照相机:
这可以对应BoxApp当中的XMMATRIX view = XMMatrixLookAtLH(pos, target, up);
这其实和我们的理解吻合,因为求视图变换只需要知道照相机的位置、朝向和向上方向,我们其实就可以求出view matrix了。
所谓视图变换,是要把物体和照相机相对静止地进行平移、旋转,保证照相机处于标准状态:照相机位于原点,看向z正方向,摆正,up方向为y正方向(一个差异在于,Games101中描述的是右手系的标准状态,因此是look at -z方向;而龙书中是左手系,也就成了看向+z方向)。照相机可能位于一个任意位置、看向任意一个方向、并且歪斜至其up指向任意方向,这可能是游戏逻辑等决定的(比如人物在移动,人物看到的东西也自然发生了改变)。但是在操作中,我们希望先把整个世界伴随着相机一起平移旋转,从而让后面的计算简化。
XMMatrixLookAtLH是一个求视图变换的矩阵,输入当前的相机位置、相机焦点(看着的一个点)和相机上方向即可求得。
投影变换projection transform
整个投影变换在代码中只体现为调用一个函数:
XMMATRIX P = XMMatrixPerspectiveFovLH(0.25f*MathHelper::Pi, AspectRatio(), 1.0f, 1000.0f)