前言
本文是《DirectX 12 3D 游戏开发实战》的个人学习笔记、代码分析,略过前面基础的几何数学部分,直接到第四章程序部分。
原书中的源代码:https://github.com/d3dcoder/d3d12book
对于概念并不会放到文章中讲述,请自行参阅书或百度。对于结构体的样式和各成员,以及枚举值,不会深入讲解,请根据页码到书中查询,或到微软官方文档查询(直接结构体名就行)。
书中本章的结构为:先讲述概念,如交换链、描述符、组件对象模型、同步、资源转换、功能级别等,然后放上初始化的示例代码,最后放上框架代码。
D3D12用到了一些Windows的窗体编程知识,代码量庞大,原作者的意图并非是要我们跟着书中原模原样的敲出代码。
书中源代码给出了公共的框架基类d3dApp,同时给了九个虚函数,同时后面的示例都将继承这个App类,根据需要覆写这九个虚函数,我们所需要的是看懂源代码,并能根据需要自己写出需要覆写的虚函数。
书上的讲述以及MSDN文档已经很全面了,记录本文的原因,其一是重复一遍,验证自己对知识理解的是否有遗漏;其二,是因为理论、接口的说明和实际代码位置往往不一致,在文章中标记出具体页数,更方便查找相关说明。
代码分析
首先分析下项目结构,本章是初始化DirectX12窗口。用到了如下几个文件:
- 作者打好的基础框架d3dApp
- 用于编译Shader、调试、编码转换等功能的工具类文件d3dUtil
- 用于帧控制、计时的GameTimer文件
- 继承并覆写的InitDirect3DApp.cpp文件
找到程序入口int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE prevInstance, PSTR cmdLine, int showCmd)
,在文件InitDirect3DApp.cpp 中。
//在DEBUG下用来检测内存泄露
#if defined(DEBUG) | defined(_DEBUG)
_CrtSetDbgFlag( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF );
#endif
try
{
InitDirect3DApp theApp(hInstance);//初始化
if(!theApp.Initialize())//初始化
return 0;
return theApp.Run();//运行
}
catch(DxException& e)//发现错误,就弹窗抛出
{
MessageBox(nullptr, e.ToString().c_str(), L"HR Failed", MB_OK);
return 0;
}
这里我们看InitDirect3DApp类的声明。
class InitDirect3DApp : public D3DApp//继承于基础框架 D3DApp
{
public:
InitDirect3DApp(HINSTANCE hInstance);//接收一个窗口实例
~InitDirect3DApp();
virtual bool Initialize()override;
private:
virtual void OnResize()override;
virtual void Update(const GameTimer& gt)override;
virtual void Draw(const GameTimer& gt)override;
};
一共覆写了四个方法:初始化、窗口大小重新调整、每帧更新、绘制。
除了Draw函数,其他方法每个基本都是调用父类的方法,而Draw函数用到了很多从父类继承的成员变量,因此往下研究下D3DApp类。
由于成员方法太多,根据方法使用、调用的次序来分析。首先是Initialize方法:
bool D3DApp::Initialize()
{
if(!InitMainWindow())//初始化窗体
return false;
if(!InitDirect3D())//初始化Direct3D
return false;
OnResize();//初始窗口大小
return true;
}
窗体初始化更接近WindowsSDK编程,直接看Direct3D部分的InitDirect3D函数:
InitDirect3D
#if defined(DEBUG) || defined(_DEBUG)
// D3D12的Debug功能
{
ComPtr<ID3D12Debug> debugController;
ThrowIfFailed(D3D12GetDebugInterface(IID_PPV_ARGS(&debugController)));
debugController->EnableDebugLayer();
}
#endif
在DirectX12中,创建某某等操作都会返回HRESULT(long类型),值代表不同的含义,ThrowIfFailed是作者封装的异常抛出宏,根据值抛出DxException异常,使调试简单一些。宏的定义可以在d3dUtil中查看到定义。
ThrowIfFailed(CreateDXGIFactory1(IID_PPV_ARGS(&mdxgiFactory)));
// 尝试创建硬件设备
HRESULT hardwareResult = D3D12CreateDevice(
nullptr, // 默认显卡
D3D_FEATURE_LEVEL_11_0,//功能级别
IID_PPV_ARGS(&md3dDevice));//将类型属性和指针作为第三、四个参数传入
对于功能级别,88页有相关介绍。
对于显卡设备信息的枚举获取,89页有相关介绍。
这里用到了两个成员变量mdxgiFactory和md3dDevice,查看类型:
Microsoft::WRL::ComPtr<IDXGIFactory4> mdxgiFactory;
Microsoft::WRL::ComPtr<ID3D12Device> md3dDevice;
首先COM就是组件对象模型(Component Object Model)的缩写,在第78页,有对使用的简单介绍,我个人理解,将其当做一种智能指针。
模版内的参数是类型,工厂用于创建一些基础设备,例如下面的基础封装适配器、交换链、枚举适配器等,在89页有详细介绍。
// 如果建设图形设备失败,则用基础封装适配器
if(FAILED(hardwareResult))
{
ComPtr<IDXGIAdapter> pWarpAdapter;
ThrowIfFailed(mdxgiFactory->EnumWarpAdapter(IID_PPV_ARGS(&pWarpAdapter)));//用工厂创建封装设备
ThrowIfFailed(D3D12CreateDevice(
pWarpAdapter.Get(),//用封装设备创建图形设备
D3D_FEATURE_LEVEL_11_0,
IID_PPV_ARGS(&md3dDevice)));
}
ThrowIfFailed(md3dDevice->CreateFence(0, D3D12_FENCE_FLAG_NONE,
IID_PPV_ARGS(&mFence)));
用d3d设备创建围栏对象,用于CPU与GPU间的同步问题,使用方法在98页有相关介绍,新增成员变量:
Microsoft::WRL::ComPtr<ID3D12Fence> mFence;
mRtvDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
mDsvDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_DSV);
mCbvSrvUavDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
描述符的使用需要知道描述符的大小。
描述符的介绍见83页,大小获取见104页,新增成员变量:
UINT mRtvDescriptorSize = 0;
UINT mDsvDescriptorSize = 0;
UINT mCbvSrvUavDescriptorSize = 0;
D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS msQualityLevels;
msQualityLevels.Format = mBackBufferFormat;
msQualityLevels.SampleCount = 4;
msQualityLevels.Flags = D3D12_MULTISAMPLE_QUALITY_LEVELS_FLAG_NONE;
msQualityLevels.NumQualityLevels = 0;
ThrowIfFailed(md3dDevice->CheckFeatureSupport(
D3D12_FEATURE_MULTISAMPLE_QUALITY_LEVELS,
&msQualityLevels,
sizeof(msQualityLevels)));
m4xMsaaQuality = msQualityLevels.NumQualityLevels;
assert(m4xMsaaQuality > 0 && "Unexpected MSAA quality level.");
检查对MSAA的支持情况,不同硬件的支持情况,乃至数值都是不同的。
检查方法是给CheckFeatureSupport方法传入D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS类型的结构体,传入前填入需要的参数,调用完毕后,个别参数会成为我们需要的输出值。
87页有支持情况检查的相关介绍,79页有纹理格式的相关介绍。
这里我们遇到的两个成员变量为:
DXGI_FORMAT mBackBufferFormat = DXGI_FORMAT_R8G8B8A8_UNORM;
UINT m4xMsaaQuality = 0;
#ifdef _DEBUG
LogAdapters();
#endif
如果是DEBUG中,就找到所有显卡信息并输出,定义见89页。
CreateCommandObjects();
CreateSwapChain();
CreateRtvAndDsvDescriptorHeaps();
创建命令对象、交换链、描述符堆,同样是三个自建程序,我们进去看一看。
CreateCommandObjects
首先是三个成员变量:
Microsoft::WRL::ComPtr<ID3D12CommandQueue> mCommandQueue;
Microsoft::WRL::ComPtr<ID3D12CommandAllocator> mDirectCmdListAlloc;
Microsoft::WRL::ComPtr<ID3D12GraphicsCommandList> mCommandList;
分别是命令队列、命令分配器、命令列表,在94页讲述。创建队列、列表在105页,方法体:
//填写命令队列的描述符结构体
D3D12_COMMAND_QUEUE_DESC queueDesc = {};
queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
//创建命令队列
ThrowIfFailed(md3dDevice->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&mCommandQueue)));
//创建命令分配器
ThrowIfFailed(md3dDevice->CreateCommandAllocator(
D3D12_COMMAND_LIST_TYPE_DIRECT,
IID_PPV_ARGS(mDirectCmdListAlloc.GetAddressOf())));
//创建命令列表
ThrowIfFailed(md3dDevice->CreateCommandList(
0,
D3D12_COMMAND_LIST_TYPE_DIRECT,
mDirectCmdListAlloc.Get(), // 关联分配器
nullptr, // 初始化流水线状态对象
IID_PPV_ARGS(mCommandList.GetAddressOf())));
//创建时,命令列表为打开状态,需要先关闭
mCommandList->Close();
对于这三个对象,我个人这样理解。
命令队列是GPU真正执行命令的队列,我们想给GPU发送各种绘图命令不是直接交给队列,而是先塞给命令列表,然后列表提交。而分配器用于给命令列表分配空间。
对于命令列表的使用顺序如下:
- 先发送各种绘图命令给命令列表(调用绘图方法)
- 发送完毕后,用Close方法关闭命令列表
- 关闭后,用ExecuteCommandLists方法提交给命令队列
- 想要复用时,用Reset函数将命令列表再打开
CreateSwapChain
方法体:
//释放之前的交换链,重新创建
mSwapChain.Reset();
DXGI_SWAP_CHAIN_DESC sd;
sd.BufferDesc.Width = mClientWidth;//Buffer宽
sd.BufferDesc.Height = mClientHeight;//Buffer高
sd.BufferDesc.RefreshRate.Numerator = 60;//刷新率分子
sd.BufferDesc.RefreshRate.Denominator = 1;//刷新率分母
sd.BufferDesc.Format = mBackBufferFormat;//Buffer纹理格式
sd.BufferDesc.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED;//扫描方式
sd.BufferDesc.Scaling = DXGI_MODE_SCALING_UNSPECIFIED;//拉伸方式
sd.SampleDesc.Count = m4xMsaaState ? 4 : 1;//采样数
sd.SampleDesc.Quality = m4xMsaaState ? (m4xMsaaQuality - 1) : 0;//采样质量
sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;//渲染目标
sd.BufferCount = SwapChainBufferCount;//缓冲数
sd.OutputWindow = mhMainWnd;//渲染窗口的句柄
sd.Windowed = true;
sd.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
sd.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH;//可选标志
//交换链需要命令队列进行刷新
ThrowIfFailed(mdxgiFactory->CreateSwapChain(
mCommandQueue.Get(),
&sd,
mSwapChain.GetAddressOf()));
看起来复杂,还是老套路,填写描述符,然后创建交换链。首先,要传给CreateSwapChain的结构体为DXGI_SWAP_CHAIN_DESC,其定义在106页,其中成员BufferDesc是DXGI_MODE_DESC类型的结构体,定义同样在106页,成员SampleDesc是DXGI_SAMPLE_DESC类型结构体,定义在87页。
CreateRtvAndDsvDescriptorHeaps
成员变量:
Microsoft::WRL::ComPtr<ID3D12DescriptorHeap> mRtvHeap;//渲染目标堆
Microsoft::WRL::ComPtr<ID3D12DescriptorHeap> mDsvHeap;//深度、模版缓冲堆
84页介绍,107页创建。
方法体:
D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc;//创建堆用的描述符
rtvHeapDesc.NumDescriptors = SwapChainBufferCount;//书中固定位2,交换链对应两个渲染目标
rtvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;//类型为渲染目标
rtvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
rtvHeapDesc.NodeMask = 0;//单显卡设置为0
ThrowIfFailed(md3dDevice->CreateDescriptorHeap(
&rtvHeapDesc, IID_PPV_ARGS(mRtvHeap.GetAddressOf())));
D3D12_DESCRIPTOR_HEAP_DESC dsvHeapDesc;
dsvHeapDesc.NumDescriptors = 1;//一个深度缓冲
dsvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_DSV;
dsvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
dsvHeapDesc.NodeMask = 0;
ThrowIfFailed(md3dDevice->CreateDescriptorHeap(
&dsvHeapDesc, IID_PPV_ARGS(mDsvHeap.GetAddressOf())));
InitDirect3D函数最后还调用了OnResize方法,在此之前,先分析一下用于同步的FlushCommandQueue方法。
方法与围栏的定义见99页。
FlushCommandQueue
void D3DApp::FlushCommandQueue()
{
//围栏标记点+1(此时围栏标记点比围栏值大1)
mCurrentFence++;
//在命令队列的末尾增加命令:给围栏值+1
ThrowIfFailed(mCommandQueue->Signal(mFence.Get(), mCurrentFence));
//当CPU运行到此处,如果围栏值=围栏标记点,则不进入,否则进入if,用来等待同步
if(mFence->GetCompletedValue() < mCurrentFence)
{
//声明事件对象,参数分别为:
//lpEventAttributes 指向SECURITY_ATTRIBUTES结构的指针。如果lpEventAttributes为NULL,则子进程不能继承事件句柄。
//lpName 事件名称
//dwFlags 多位值,包括:初始状态是否为通知状态、是否人工重置为通知状态等
//dwDesiredAccess 访问权限掩码
HANDLE eventHandle = CreateEventEx(nullptr, false, false, EVENT_ALL_ACCESS);
//将围栏值与Event对象关联
ThrowIfFailed(mFence->SetEventOnCompletion(mCurrentFence, eventHandle));
//Event对象等待通知
WaitForSingleObject(eventHandle, INFINITE);
//关闭句柄
CloseHandle(eventHandle);
}
}
说下闲话,上面的同步代码第一次见到时,还是在嵌入式的考试中……
然后是OnResize函数。
OnResize
首先是用到的新的成员变量:
Microsoft::WRL::ComPtr<ID3D12Resource> mSwapChainBuffer[SwapChainBufferCount];
Microsoft::WRL::ComPtr<ID3D12Resource> mDepthStencilBuffer;
D3D12_VIEWPORT mScreenViewport;
//断言一下,确定程序员没有犯傻
assert(md3dDevice);
assert(mSwapChain);
assert(mDirectCmdListAlloc);
//CPU等待GPU执行完所有命令
FlushCommandQueue();
//打开命令列表并绑定分配器
ThrowIfFailed(mCommandList->Reset(mDirectCmdListAlloc.Get(), nullptr));
// 重置所有缓冲
for (int i = 0; i < SwapChainBufferCount; ++i)
mSwapChainBuffer[i].Reset();
mDepthStencilBuffer.Reset();
// 改变交换链缓冲大小
ThrowIfFailed(mSwapChain->ResizeBuffers(
SwapChainBufferCount,
mClientWidth, mClientHeight,
mBackBufferFormat,
DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH));
mCurrBackBuffer = 0;
//创建渲染目标视图,见P109
CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHeapHandle(mRtvHeap->GetCPUDescriptorHandleForHeapStart());
for (UINT i = 0; i < SwapChainBufferCount; i++)
{
//绑定交换链缓冲区为后台缓冲区
ThrowIfFailed(mSwapChain->GetBuffer(i, IID_PPV_ARGS(&mSwapChainBuffer[i])));
//为后台缓冲区创建渲染目标视图(创建一个RTV),第二个参数在创建资源时已经指定格式,因此填nullptr
md3dDevice->CreateRenderTargetView(mSwapChainBuffer[i].Get(), nullptr, rtvHeapHandle);
//偏移到RTV的下一个缓冲区。
rtvHeapHandle.Offset(1, mRtvDescriptorSize);
}
// 创建 深度/模版 缓冲和视图 见P110
D3D12_RESOURCE_DESC depthStencilDesc;
depthStencilDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;//资源维度
depthStencilDesc.Alignment = 0;//对齐方式
depthStencilDesc.Width = mClientWidth;
depthStencilDesc.Height = mClientHeight;
depthStencilDesc.DepthOrArraySize = 1;//指定资源的深度(如果为3D),如果是1D或2D资源的数组,则指定数组大小
depthStencilDesc.MipLevels = 1;//Mipmap层级,对于深度/模版 缓冲,只能有一个层级
//下面说明
depthStencilDesc.Format = DXGI_FORMAT_R24G8_TYPELESS;
depthStencilDesc.SampleDesc.Count = m4xMsaaState ? 4 : 1;//采样次数
depthStencilDesc.SampleDesc.Quality = m4xMsaaState ? (m4xMsaaQuality - 1) : 0;//采样质量
depthStencilDesc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN;//纹理布局,暂时不需要考虑
depthStencilDesc.Flags = D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL;//深度模版缓冲指定为此
//颜色优化值,用于创造下面的堆
D3D12_CLEAR_VALUE optClear;
optClear.Format = mDepthStencilFormat;//格式
optClear.DepthStencil.Depth = 1.0f;//深度
optClear.DepthStencil.Stencil = 0;//模版值
ThrowIfFailed(md3dDevice->CreateCommittedResource(//创建显存堆并提交资源 见 P111 页
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT),//默认堆
D3D12_HEAP_FLAG_NONE,//无额外参数
&depthStencilDesc,//资源描述符
D3D12_RESOURCE_STATE_COMMON,//初始状态
&optClear,//清除资源状态
IID_PPV_ARGS(mDepthStencilBuffer.GetAddressOf())));//希望获得资源的COM指针
// 利用此资源的格式,为整个资源的第0 mip层创建描述符,下面讨论一下描述符问题
D3D12_DEPTH_STENCIL_VIEW_DESC dsvDesc;
dsvDesc.Flags = D3D12_DSV_FLAG_NONE;
dsvDesc.ViewDimension = D3D12_DSV_DIMENSION_TEXTURE2D;
dsvDesc.Format = mDepthStencilFormat;
dsvDesc.Texture2D.MipSlice = 0;
//创建深度/模版缓冲视图,DepthStencilView方法返回值就是 mDsvHeap->GetCPUDescriptorHandleForHeapStart(),和渲染目标视图创建方法一致。
md3dDevice->CreateDepthStencilView(mDepthStencilBuffer.Get(), &dsvDesc, DepthStencilView());
// 将资源从初始状态转换为深度缓冲,资源转换见P100,代码见P113
mCommandList->ResourceBarrier(1, //资源屏障数量
&CD3DX12_RESOURCE_BARRIER::Transition(mDepthStencilBuffer.Get(),//资源
D3D12_RESOURCE_STATE_COMMON, //转换前状态
D3D12_RESOURCE_STATE_DEPTH_WRITE));//转换后状态
// 提交命令列表
ThrowIfFailed(mCommandList->Close());//先关闭命令列表
ID3D12CommandList* cmdsLists[] = { mCommandList.Get() };
mCommandQueue->ExecuteCommandLists(_countof(cmdsLists), cmdsLists);//提交并执行命令队列
// CPU等待GPU执行完毕
FlushCommandQueue();
// 更新视口属性,创建视口在Draw方法中,设置视口见书P114
mScreenViewport.TopLeftX = 0;
mScreenViewport.TopLeftY = 0;
mScreenViewport.Width = static_cast<float>(mClientWidth);
mScreenViewport.Height = static_cast<float>(mClientHeight);
mScreenViewport.MinDepth = 0.0f;
mScreenViewport.MaxDepth = 1.0f;
//剪裁矩形
mScissorRect = { 0, 0, mClientWidth, mClientHeight };
创建深度/模版缓冲描述符时,Format成员被赋值为成员DXGI_FORMAT mDepthStencilFormat = DXGI_FORMAT_D24_UNORM_S8_UINT;
,在代码中做出改变,值变为DXGI_FORMAT_R24G8_TYPELESS,代码中的注释是这样说的:
SSAO章节要求SRV到深度缓冲区读取,因此要为这同一资源创建两个视图:SRV需要DXGI_FORMAT_R24_UNORM_X8_TYPELESS格式,而DSV需要DXGI_FORMAT_D24_UNORM_S8_UINT格式。创建时需要使用为类型格式创建深度缓冲区资源,既上面所展示的DXGI_FORMAT_R24G8_TYPELESS
由于这里指定了无类型问题,和P113页代码就有了相关出入,书上这里只需要传入nullptr就好,而这里需要填写dsv描述符。
计时器
之前我的Opengl骨骼动画程序就是因为没有帧控制程序,因此只能实现姿势,而不是动画,这本书倒是上来就实现了计时器。
使用过Unity一段时间后,Unity同样也有对于每帧更新Update的实现,可以见得一个计时器对游戏特别重要。
首先是GameTimer的定义:
class GameTimer
{
public:
GameTimer();
float TotalTime()const; // in seconds
float DeltaTime()const; // in seconds
void Reset(); // 开始消息循环前调用
void Start(); // 从暂停到开始调用
void Stop(); // 暂停计时器时调用
void Tick(); // 每帧调用
private:
double mSecondsPerCount;//计时器周期
double mDeltaTime;//变化时间
__int64 mBaseTime;//开始时间
__int64 mPausedTime;
__int64 mStopTime;
__int64 mPrevTime;
__int64 mCurrTime;
bool mStopped;
};
如何查看时间间隔,原理在P116。windows.h下有方法:QueryPerformanceCounter,可以查看计时器的当前周期;QueryPerformanceFrequency则可以查看计时器的频率,两次周期数之差就是经过的计时器周期,除以频率便是过去的时间:,文中是用频率的倒数周期乘以经过周期,原理一样。
构造方法就是查看计数器频率(周期):
GameTimer::GameTimer()
: mSecondsPerCount(0.0), mDeltaTime(-1.0), mBaseTime(0),
mPausedTime(0), mPrevTime(0), mCurrTime(0), mStopped(false)
{
__int64 countsPerSec;
QueryPerformanceFrequency((LARGE_INTEGER*)&countsPerSec);
mSecondsPerCount = 1.0 / (double)countsPerSec;
}
每帧需要执行的Tick:
void GameTimer::Tick()
{
if( mStopped )//如果停止
{
mDeltaTime = 0.0;//时间没有变化
return;
}
__int64 currTime;
QueryPerformanceCounter((LARGE_INTEGER*)&currTime);
mCurrTime = currTime;//更新当前时间
mDeltaTime = (mCurrTime - mPrevTime)*mSecondsPerCount;//计算变化时间
mPrevTime = mCurrTime;//更新前一时间为当前时间
if(mDeltaTime < 0.0)//说明
{
mDeltaTime = 0.0;
}
}
如果在省电模式下,或换了一个处理器,可能导致变化时间变为负的,这样就要强制令变化时间为0。书中有更详细的说明。
float GameTimer::DeltaTime()const
{
return (float)mDeltaTime;
}
Reset方法有初始化和重置两个功能:
void GameTimer::Reset()
{
__int64 currTime;
QueryPerformanceCounter((LARGE_INTEGER*)&currTime);
mBaseTime = currTime;
mPrevTime = currTime;
mStopTime = 0;
mStopped = false;
}
然后是对暂停和开始的编写:
void GameTimer::Stop()
{
if( !mStopped )
{
__int64 currTime;
QueryPerformanceCounter((LARGE_INTEGER*)&currTime);
mStopTime = currTime;//记录暂停时的周期数
mStopped = true;
}
}
void GameTimer::Start()
{
__int64 startTime;
QueryPerformanceCounter((LARGE_INTEGER*)&startTime);
if( mStopped )
{
mPausedTime += (startTime - mStopTime);//总共停止的周期数
mPrevTime = startTime;
mStopTime = 0;//重置记录
mStopped = false;
}
}
总共时间:
float GameTimer::TotalTime()const
{
if( mStopped )//如果在暂停中
{//暂停时的周期-开始的周期-暂停了的周期
return (float)(((mStopTime - mPausedTime)-mBaseTime)*mSecondsPerCount);
} else//不在停止中
{//当前周期-开始的周期-暂停了的周期
return (float)(((mCurrTime-mPausedTime)-mBaseTime)*mSecondsPerCount);
}
}
主循环
在初始化后,框架对象就会调用Run方法开启循环:
int D3DApp::Run()
{
MSG msg = {0};
mTimer.Reset();//计时器初始化
while(msg.message != WM_QUIT)//如果没有退出信息,便一直循环
{
if(PeekMessage( &msg, 0, 0, 0, PM_REMOVE ))// 将消息队列存放到msg里面
{
TranslateMessage( &msg );//将此时的键盘字符存放到消息队列中
DispatchMessage( &msg );//将取出的信息发送给窗口
}
// 否则执行游戏、动画逻辑
else
{
mTimer.Tick();//每帧执行的计数器
if( !mAppPaused )//如果没有停止
{
CalculateFrameStats();//计算帧率并显示到标题栏
Update(mTimer); //更新逻辑
Draw(mTimer);//渲染逻辑
}
else//停止了,就睡眠0.1秒,防止出现忙等
{
Sleep(100);
}
}
}
return (int)msg.wParam;
}
这样,D3DAPP大体功能实现完毕,可以发现,这个框架提供了九个虚函数:
public:
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;//渲染逻辑
// 鼠标输入事件
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){ }
在本章中,InitDirect3DApp提供了最基础的渲染逻辑,书中可见于P131:
void InitDirect3DApp::Draw(const GameTimer& gt)
{
// 重用命令分配器内存
// 我们只能在GPU执行完所有命令后执行
ThrowIfFailed(mDirectCmdListAlloc->Reset());
// 在提交给命令队列(execute)后,可以reset命令列表,来重用内存
ThrowIfFailed(mCommandList->Reset(mDirectCmdListAlloc.Get(), nullptr));
// 将资源从呈现变为渲染目标状态
mCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(CurrentBackBuffer(),
D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_RENDER_TARGET));
// 设置视口和剪裁矩形,它们要随着命令列表一起重置
mCommandList->RSSetViewports(1, &mScreenViewport);
mCommandList->RSSetScissorRects(1, &mScissorRect);
// 以蓝色清除当前渲染目标缓冲,清楚深度/模版 缓冲
// 参数:1.要清除的缓冲 2.颜色RGBA 3.后面数组的长度 4.要清除的矩形区域,nullptr为全部清除
mCommandList->ClearRenderTargetView(CurrentBackBufferView(), Colors::LightSteelBlue, 0, nullptr);
mCommandList->ClearDepthStencilView(DepthStencilView(), D3D12_CLEAR_FLAG_DEPTH | D3D12_CLEAR_FLAG_STENCIL, 1.0f, 0, 0, nullptr);
// 指定渲染目标,第三个参数说明描述符在内存中是否连续
mCommandList->OMSetRenderTargets(1, &CurrentBackBufferView(), true, &DepthStencilView());
// 渲染完毕,将内存从渲染状态转换为呈现状态
mCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(CurrentBackBuffer(),
D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT));
// 关闭命令列表
ThrowIfFailed(mCommandList->Close());
// 提交命令列表到队列中
ID3D12CommandList* cmdsLists[] = { mCommandList.Get() };
mCommandQueue->ExecuteCommandLists(_countof(cmdsLists), cmdsLists);
// 交换前后缓冲区
ThrowIfFailed(mSwapChain->Present(0, 0));
mCurrBackBuffer = (mCurrBackBuffer + 1) % SwapChainBufferCount;
// 等待GPU执行完毕
// 这样组织代码是低效的,后面章节会演示如何组织渲染代码
FlushCommandQueue();
}
本章大致内容就是这些,运行出来的结果是一个蓝色的窗口,标题栏不断更新fps,按ESC能退出:
网友评论