【DirectX12笔记】第4章 DirectX初始化

2019-07-10  本文已影响0人  crossous
封面

前言

  本文是《DirectX 12 3D 游戏开发实战》的个人学习笔记、代码分析,略过前面基础的几何数学部分,直接到第四章程序部分。
  原书中的源代码:https://github.com/d3dcoder/d3d12book
  对于概念并不会放到文章中讲述,请自行参阅书或百度。对于结构体的样式和各成员,以及枚举值,不会深入讲解,请根据页码到书中查询,或到微软官方文档查询(直接结构体名就行)。
  书中本章的结构为:先讲述概念,如交换链、描述符、组件对象模型、同步、资源转换、功能级别等,然后放上初始化的示例代码,最后放上框架代码。
  D3D12用到了一些Windows的窗体编程知识,代码量庞大,原作者的意图并非是要我们跟着书中原模原样的敲出代码。
  书中源代码给出了公共的框架基类d3dApp,同时给了九个虚函数,同时后面的示例都将继承这个App类,根据需要覆写这九个虚函数,我们所需要的是看懂源代码,并能根据需要自己写出需要覆写的虚函数。
  书上的讲述以及MSDN文档已经很全面了,记录本文的原因,其一是重复一遍,验证自己对知识理解的是否有遗漏;其二,是因为理论、接口的说明和实际代码位置往往不一致,在文章中标记出具体页数,更方便查找相关说明。

代码分析

  首先分析下项目结构,本章是初始化DirectX12窗口。用到了如下几个文件:

  找到程序入口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页有相关介绍。
  这里用到了两个成员变量mdxgiFactorymd3dDevice,查看类型:

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发送各种绘图命令不是直接交给队列,而是先塞给命令列表,然后列表提交。而分配器用于给命令列表分配空间。
  对于命令列表的使用顺序如下:

  1. 先发送各种绘图命令给命令列表(调用绘图方法)
  2. 发送完毕后,用Close方法关闭命令列表
  3. 关闭后,用ExecuteCommandLists方法提交给命令队列
  4. 想要复用时,用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页,其中成员BufferDescDXGI_MODE_DESC类型的结构体,定义同样在106页,成员SampleDescDXGI_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则可以查看计时器的频率,两次周期数之差就是经过的计时器周期,除以频率便是过去的时间:\frac{IC_2-IC_1}{f},文中是用频率的倒数周期乘以经过周期,原理一样。
  构造方法就是查看计数器频率(周期):

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能退出:
上一篇下一篇

猜你喜欢

热点阅读