dx12 龙书第四章学习笔记 -- Direct3D的初始化

1.预备知识:

①Direct3D 12概述:

通过Direct3D这种底层图形应用程序编程接口(Application Programming Interface, API),即可在应用程序中对图形处理器(Graphics Processing Unit, GPU)进行控制和编程

Direct3D层和硬件驱动会协作将Direct3D命令转换为系统中GPU可以执行的本地机器指令 -- 无需考虑GPU的具体规格和硬件控制层面的实现细节 -- GPU的生产厂商和Direct3D团队一起合作的结果

②组件对象模型COM:

COM是一种令DirectX不受编程语言束缚,并且使之向后兼容的技术。通常将COM对象视为一种接口(当前将其作为一个C++类来使用),使用C++编写DirectX程序时,COM帮我们隐藏了大量底层细节。

要获取指向某COM接口的指针,需借助特定函数或另一COM接口的方法,而不是使用new关键字去创建COM接口。另外,COM对象会统计其引用次数,应用完某接口后,调用它的Release方法(COM接口),而不是使用delete关键词来删除

Windows运行时库(Windows Runtime Library,WRL)提供Microsoft::WRL::ComPtr类(#include ),可以把它当作COM对象的智能指针,可以自动回收(调用Release方法)COM对象

常用的ComPtr方法:

  • Get():返回一个指向此底层COM接口的指针

  • GetAddressOf():返回指向此底层COM接口指针的地址

  • Reset():将此ComPtr实例设置为nullptr释放与之相关的所有引用

COM接口都以大写字母"I"作为开头,例如,表示命令队列的COM接口为ID3D12GraphicsCommandList

补充:以大写字母"C"作为开头,往往是某种结构的简易版,相比于原版结构,提供简易的初始化方式

③纹理格式:

// 例子:
DXGI_FORMAT_R32G32B32_FLOAT 
// 每个元素由3个32位浮点数分量构成 
"R G B A"
"格式名称虽然使用颜色和alpha值,但纹理存储的不一定是颜色信息"

④交换链和页面翻转:

为了避免动画中出现画面闪烁的现象,最好将动画帧完整地绘制在一种称为后台缓冲区(off-screen)纹理

前台缓冲区和后台缓冲区
前台缓冲区存储是当前显示在屏幕上的图像数据; 后台缓冲区存储的是下一帧图像数据; 下一帧时,前后台互换 ("呈现") -- 指针交换

前后台缓冲区构成了交换链IDXGISwapChain接口 

g该接口存储前后台缓冲区的两种纹理,还提供了

修改缓冲区大小(IDXGISwapChain::ResizeBuffers)呈现缓冲区内容(IDXGISwapChain::Present)的方法

对于一般的应用,双缓冲就足够了

⑤深度缓冲:

深度缓冲区这种纹理资源存储的并非图像数据,而是特定像素的深度信息,深度值的范围为0.0~1.0(0最近1最远)

若使用深度缓存,则物体的绘制顺序也就变的无关紧要了

深度缓冲区是一种纹理,使用明确的数据格式来创建它:

// 例如:
DXGI_FORMAT_D32_FLOAT:指定一个32位浮点型深度缓冲区 

具体步骤:在开始渲染之前,后天缓冲区会被清理为默认颜色,深度缓冲区也将被清除为默认值1.0,然后依次绘制每个渲染对象,当该像素的深度值比深度缓冲区内深度值小时,深度测试通过,更新缓冲区,否则不通过 -- 以此将离我们最近的保留,丢弃被覆盖的像素

⑥资源与描述符:

⭐资源、 描述符、根签名、根参数较为复杂,单独开一个博客笔记进行学习

在发出绘制命令之前,我们需要将本次绘制调用(draw call)相关的资源绑定(bind或链接 link)到渲染流水线上

GPU资源并非直接与渲染流水线相绑定,而是通过描述符的对象进行间接引用

描述符/视图: 描述符是一个中间层,会为GPU解释资源,以及如何使用

描述符的具体类型:

1.CBV:constant buffer view常量缓冲区视图
2.SRV:shader resource view着色器资源视图
3.UAV:unordered access view无序访问视图
4.RTV:render target view渲染目标视图资源
5.DSV:depth/stencil view深度/模板视图资源
6.采样器描述符

描述符堆中存有一系列描述符,可以为每一种类型的描述符创建单独的描述符堆 -- 可以多个描述符引用同一个资源 

创建描述符的最佳时机为初始化期间 -- 最好不要在运行时才创建描述符

⑦多重采样技术的原理:MSAA

由于屏幕中显示的像素不可能是无穷小的,所以并不是任意一条直线都能在显示器上平滑而完美地呈现出来。显示器屏幕中的像素都有特定的形状、大小以及数量,这也就导致了当我们绘制直线时,实际上是通过数量有限、离散而非连续的“点”构成,继而造成“阶梯”状的锯齿效应。“阶梯”效果称为锯齿状走样(aliasing)。

dx12 龙书第四章学习笔记 -- Direct3D的初始化_第1张图片

 我们可以通过提高显示器的分辨率,缩小像素的大小,使得走样问题得以改善。

在不能提升显示器分辨率,或显示器分辨率首先的情况下,我们就可以运用反走样(antialiasing, 也称为抗锯齿、反锯齿、反失真等)技术。有一种名为超级采样(supersampling, 可简记为SSAA,即Super Sample Anti-Aliasing)的反走样技术,它使用4倍于屏幕分辨率大小的后台缓冲区和深度缓冲区。3D场景将以这种更大的分辨率渲染到后台缓冲区中。当数据要从后台缓冲区调往屏幕显示的时候,会将后台缓冲区按4个像素一组进行解析(resolve, 或降采样,downsample:把放大的采样点树降低回原采样点数):每组用求平均值的方法得到一种相对平滑的像素颜色。因此,超级采样实际上是通过软件的方式提升了画面的分辨率。

超级采样是一种开销高昂的操作,因为它将像素的处理数量和占用的内存大小都增加到之前的4倍。

对此,D3D还支持一种在性能与效果等方面都较为折中的反走样方法,叫做多重采样(multisampling, 可简记为MSAA, MultiSample Anti-Aliasing)。这种技术通过跨子像素[子像素的意思是:在像素内部再次细分出的小像素,用以采样]共享一些计算信息,从而使它比超级采样的开销更低。

现假设采用4X多重采样(即每个像素中都有4个子像素),并同样使用4倍于屏幕分辨率的后台缓冲区和深度缓冲区。值得注意的是,这种技术并不需要对每一个子像素都进行计算,而是仅计算一次像素中心处的颜色,再基于可视性(每个子像素经深度/模板测试的结果)和覆盖性(子像素的中心在多边形的里面还是外面?)将得到的颜色信息分享给其子像素。[百度搜索得到:比如MSAA一个像素的4个子像素处于三角形内的情况,假设4个像素只有2个像素处于三角形内,那么这个母像素颜色*50%即使采样后的结果???]

 dx12 龙书第四章学习笔记 -- Direct3D的初始化_第2张图片

[※除了这两种常见的反锯齿手段,还有许多不同的方式实现反锯齿技术]

实际上,每家硬件厂商所采用的模式(即选定的子像素位置)可能会各不相同,而D3D也并没有定义子像素的具体布局。在各种特定的情况下,不同的布局模式各有千秋

SSAA和MSAA的详细讨论:MSAA和SSAA的详细说明 - 知乎 (zhihu.com)

⑧功能级别:

D3D_FEATURE_LEVEL:为不同级别所支持的功能进行严格界定

应用程序会从高到低依次检测支持的功能级别

⑨DirectX图形基础结构:

DXGI:DirectX Graphics Infrastructure -- 一种与Direct3D配合使用的API

DXGI的基本原理是使多种图形API中所共有的底层任务能够借助一组通用API进行处理

DXGI API的例子:

IDXGISwapChain 交换链        IDXGIAdapter 显示适配器

⑩功能支持的检测:

HRESULT ID3D12Device::CheckFeatureSupport(
    D3D12_FEATURE Feature,
    void *pFeatureSupportData,
    UINT FeatureSupportDataSize
);
// 检测当前图形驱动对xxx的支持

⑩①资源驻留:

Direct3D 12中,应用程序通过控制资源在显存中的去留,主动管理资源的驻留情况

2.CPU和GPU间的交互:

1️⃣命令队列和命令列表

在进行图形编程时,我们需要了解同时有两种处理器在参与处理工作,即CPU和GPU

每个GPU至少维护着一个命令队列(command queue),CPU可以利用命令列表(command list)将命令提交到这个队列中去 -- 区分命令队列和命令列表

①创建命令队列(GPU)

Direct3D 12中,命令队列被抽象为ID3D12CommandQueue接口,通过填写D3D12_COMMAND_QUEUE_DESC结构体来描述队列,再调用ID3D12Device::CreateCommandQueue方法来创建队列

Microsoft::WRL::ComPtr mCommandQueue;

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)
));

IID_PPV_ARGS辅助宏的定义如下:

#define IID_PPV_ARGS(ppType) __uuidof(**(ppType)), IID_PPV_ARGS_Helper(ppType)
// 将ppType强制转换为void**类型

其中__uuidof(**(ppType))将获取(**(ppType))的COM接口ID,在上面的代码段中得到ID3D12CommandQueue接口的COM ID

②将命令列表中的命令添加到命令队列中:

void ID3D12CommandQueue::ExecuteCommandLists(
    UINT Count, // 命令列表数组(此函数的第二个参数)中命令列表的数量
    ID3D12CommandList *const *ppCommandLists // 命令列表数组的指针
);

GPU将从数组里的第一个命令列表开始顺序执行

③向命令列表中添加命令:

ID3D12GraphicsCommandList接口封装了一系列图形渲染命令,实际上它继承于ID3D12CommandList接口

// mCommandList为一个指向ID3D12CommandList接口的指针
mCommandList->RSSetViewports(1, &mScreenViewport); // 设置视口
mCommandList->ClearRenderTargetView(
mBackBufferView, Colors::LightSteelBlue, 0, nullptr
); // 清除渲染目标视图
mCommandList->DrawIndexedInstanced(36, 1, 0, 0, 0); // 发起绘制调用

这些命令不会立即执行,只是将命令加入命令列表(List)而已

我们需要使用②中的函数ExecuteCommandLists()将命令列表中的命令添加到命令队列

在此之前我们必须调用Close方法结束命令的记录:-- 送入命令队列前一定要close

mCommandList->Close();

随着本书的深入,我们会逐步掌握ID3D12GraphicsCommandList所支持的各种命令。

④命令分配器:ID3D12CommandAllocator

记录在命令列表中的命令,实际上是存储在与之关联的命令分配器上

当通过ExecuteCommandLists方法执行命令列表时,命令队列就会引用分配器里的命令

命令分配器通过 ID3D12Device接口来创建

HRESULT ID3D12Device::CreateCommandAllocator(
    D3D12_COMMAND_LIST_TYPE type,
    REFIID riid, // 待创建ID3D12CommandAllocator接口的COM ID
    void **ppCommandAllocator // 输出指向所创建命令分配器的指针
);

// type参数:指定与此命令分配器相关联的命令列表类型
D3D12_COMMAND_LIST_TYPE_DIRECT : 存储直接可供GPU直接执行的命令
D3D12_COMMAND_LIST_TYPE_BUNDLE : 将命令打包,CPU花费时间使之在渲染期间得到优化 
-- 但D3D12的绘制API效率很高,一般不会用到打包技术,我们往往会将之束之高阁

⑤命令列表的创建:

命令列表同样由ID3D12Device接口创建:

HRESULT ID3D12Device::CreateCommandList(
    UINT nodeMask, // 节点掩码 -- 对于单GPU系统而言为0(本书)--指定与所建命令列表相关联的物理GPU
    D3D12_COMMAND_LIST_TYPE type, // DIRECT/BUNDLE 同上
    ID3D12CommandAllocator *pCommandAllocator, // 与命令列表相关联的命令分配器 两者类型必须相同
    ID3D12PipelineState *pInitialState, 
    REFIID riid, // ID3D12CommandList接口的COM ID
    void **ppCommandList // 输出指向所建命令列表的指针
);

// pInitialState参数:
指定命令列表的渲染流水线初始状态
设置为nullptr的情况:①BUNDLE ②对于执行命令队列用于达到初始化目的的特殊情况(不含有绘制命令)

我们可以创建出多个关联于同一命令分配器的命令列表,但是不能同时用它们来记录命令。当其中一个命令列表在记录命令时,必须关闭同一命令分配器的其他命令列表 -- 保证命令列表中的所有命令都会按顺序连续地添加到命令分配器中

还要注意一点,当创建或重置一个命令列表的时候,它(命令分配器)会处于一种打开的状态。所以不能为同一个命令分配器连续创建两个命令列表

命令分配器冲突报错:D3D12错误:此命令分配器正在被拎一个命令列表占用

⑥复用(重置)命令列表、命令分配器:

复用命令列表:

再调用ID3D12CommandQueue::ExecuteCommandList(C)方法之后,我们就可以通过ID3D12GraphicsCommandList::Reset方法,安全地复用命令列表C占用的相关底层内存来记录新的命令集

HRESULT ID3D12GraphicsCommandList::Reset(
    ID3D12CommandAllocator *pAllocator,
    ID3D12PipelineState *pInitialState
);
// Reset参数对应于CreateCommandList参数

Reset方法将命令列表恢复为刚创建时的初始状态,我们可以借此继续复用其底层内容,也可以避免释放旧列表再创建新列表这一系列的烦琐操作 -- 注意:重置命令列表并不会影响命令队列中的命令,因为相关的命令分配器仍在维护着其内存中被命令队列引用的新命令

复用命令分配器:

HRESULT ID3D12CommandAllocator::Reset(void);

当我们为了绘制下一帧而复用命令分配器中的内存

重置命令分配器的时机:只有在确定GPU执行完命令分配器中的所有命令之后,才可以重置命令分配器!

2️⃣CPU和GPU间的同步:

围栏(fence):用接口ID3D12Fence表示

在处理CPU和GPU同步问题时,我们采取让CPU强制等待GPU完成所有命令的处理,直到达到某个指定的围栏点 -- 这种方法叫:刷新命令队列

①创建围栏对象:

HRESULT ID3D12Device::CreateFence(
    UINT64 InitialValue,
    D3D12_FENCE_FLAGS flags,
    REFIID riid,
    void **ppFence
);

// 示例:
ThrowIfFailed(md3dDevice->CreateFence(
    0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&mFence)
));

每个围栏对象都维护着一个UINT64的值,此用来标识围栏点的整数,起初我们设置为0,每当需要标记一个新的围栏点就将它+1

代码示例:

UINT64 mCurrentFence = 0;
void D3DApp::FlushCommandQueue()
{
    mCurrentFence++; // 增加围栏值
    
    // 在命令队列中添加一条设置围栏点的命令
    // 当代码运行到此处时,CPU端的fence值已经+1
    // 而GPU端的fence值还没有更新,因为只将下述命令提交,但GPU还没有执行
    // Signal()函数的作用:从GPU端设置围栏值
    ThrowIfFailed(mCommandQueue->Signal(mFence.Get(), mCurrentFence));
    
    // CPU端等待GPU
    if(mFence->GetCompletedValue() < mCurrentFence)
    {
        // 设置一个事件,若GPU命中当前围栏,则激发预定的事件
        HANDLE eventHandle = CreateEventEx(nullptr, false, false, EVENT_ALL_ACCESS);
        ThrowIfFailed(mFence->SetEventOnCompletion(mCurrentFence, eventHandle));

        // 等待GPU命中围栏,激发事件
        waitForSingleObject(eventHandle, INFINITE);
        CloseHandle(eventHandle);
    }
}

这并不是最完美的方法,后续会提到帧资源,减少CPU等待GPU的

3️⃣资源转换:

资源冒险:我们经常会通过GPU对某个资源进行先写后读的操作,如果还没有完成写操作就进行读取,会导致资源冒险

为此,Direct3D专门针对资源设计了一组相关状态资源在创建伊始会处于默认状态,该状态将一直持续到应用程序通过Direct3D将其转换为另一种状态为止。这样GPU能够针对资源状态转换与防止资源冒险做出适当的操作

例:如果要对某个资源(比如纹理)执行写操作时,需要将它的状态转换为渲染目标状态;而对纹理进行读操作时,再把它的状态变为着色器资源状态

资源转换所带来的负荷会造成程序性能的下降,另外,一个自动跟踪状态转换的系统也在强行增加程序的额外开销

 通过命令列表设置转换资源屏障(transition resource barrier)数组,即可指定资源的转换,可以实现以一次API调用来转换多个资源

资源屏障用D3D12_RESOURCE_BARRIER结构体表示

下列辅助函数(定义位于d3dx12.h)将根据给出的资源和指定的前后转换状态,返回对应的转换资源屏障描述

struct CD3DX12_RESOURCE_BARRIER : public D3D12_RESOURCE_BARRIER
{
    // [...] 辅助方法
    static inline CD3DX12_RESOURCE_BARRIER Transition(
        _In_ ID3D12Resource* pResource,
        D3D12_RESOURCE_STATES stateBefore,
        D3D12_RESOURCE_STATES stateAfter,
        UINT subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES,
        D3D12_RESOURCE_BARRIER_FLAGS flags = D3D12_RESOURCE_BARRIER_FLAG_NONE
    ){
        CD3DX12_RESOURCE_BARRIER result;
        ZeroMemory(&result, sizeof(result));
        D3D12_RESOURCE_BARRIER &barrier = result;
        result.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
        result.Flags = flags;
        barrier.Transition.pResource = pResource;
        barrier.Transition.StateBefore = stateBefore;
        barrier.Transition.StateAfter = stateAfter;
        barrier.Transition.Subresource = subresource;
        return result;
    }
    // [...] 其他辅助方法
}    

CD3DX12_RESOURCE_BARRIER继承于D3DX12_RESOURCE_BARRIER并添加了一些辅助方法

为了方便期间,我们往往偏爱这些结构的变体,以CD3DX12作为前缀的变体全部定义在d3dx12.h头文件中,这个文件不属于Direct12 SDK的核心部分,但是可以通过微软的官方网站下载获得 -- Common目录里附有一份该头文件

代码示例:

mCommandList->ResourceBarrier(
    1, 
    &CD3DX12_RESOURCE_BARRIER::Transition(
        CurrentBackBuffer(),
        D3D12_RESOURCE_STATE_PRESENT,
        D3D12_RESOURCE_STATE_RENDER_TARGET
    )
);

这段代码将以图片形式显示在屏幕中的纹理,从呈现状态转换为渲染目标状态。

我们将此资源屏障转换看作是一条告知GPU某资源状态正在进行转换的命令,所以在执行后续的命令时,GPU便会采取必要措施以防止资源冒险

4️⃣命令与多线程:

命令列表是一种发挥Direct3D多线程优势的途径

命令列表不是自由线程对象,多线程不能共享相同的命令列表,每个线程通常只使用各自的命令列表②命令分配器不是线程自由的对象,每个线程一般都只使用属于自己的命令分配器③命令队列是线程自由的对象,所以多线程可以同时访问同一命令队列,也能够同时调用它的方法,特别是每个线程都能同时向命令队列提交它们自己所生成的命令列表④出于性能的原因,应用程序必须在初始化期间,指出用于并行记录命令的命令列表的最大值

为了简单起见,本书不讨论多线程技术,完成本书阅读后,读者可以通过查阅SDK中的Multithreading12示例来学习怎么并行生成命令列表

3.初始化Direct3D:

我们利用框架初始化Direct3D,这是一个冗长的流程,但每个程序只需执行一次,初始化可以分为以下步骤:

1.用D3D12CreateDevice函数创建ID3D12Device接口实例

2.创建一个ID3D12Fence对象,并查询描述符的大小

3.检测用户设备对4X MSAA质量级别的支持情况

4.依次创建命令队列、命令列表分配器和主命令列表

5.描述并创建交换链

6.创建应用程序所需的描述符堆

7.调整后台缓冲区大小,并为它创建渲染目标视图

8.创建深度/模板缓冲区及与之关联的深度/模板视图

9.设置视口和裁剪矩形

①创建设备:

Direct 3D设备(ID3D12Device),代表着一个显示适配器,一般来说,显示适配器是一种3D图形硬件(显卡),但是一个系统也能用软件显示适配器(warp设备)来模拟3D图形硬件的功能。Direct3D 12设备既可检查系统环境对功能的支持情况,又能创建所有其他的Direct3D接口对象(如资源、视图和命令列表)

创建Direct 3D设备的函数:

HRESULT WINAPI D3D12CreateDevice(
    IUnknown* pAdapter,
    D3D_FEATURE_LEVEL MinimumFeatureLevel, 
    REFIID riid, // ID3D12Device的COM ID
    void** ppDevice // 返回所创建的D3D12设备
);
// pAdapter:
指定在创建设备时所用的显示适配器,如果设置为nullptr(本书采用),则使用主显示适配器
-- 4.1.10节中我们展示了怎么枚举系统中所有的显示适配器
// MinimumFeatureLevel:
最低支持功能级别:我们框架指定D3D_FEATURE_LEVEL_11_0(即支持D3D 11的特性)

调用示例:

#if defined(DEBUG) || defined(_DEBUG)
// 启动D3D12的调试层
{
    ComPtr debugController;
    ThrowIfFailed(D3D12GetDebugInterface(IID_PPV_ARGS(&debugController)));
    debugController->EnableDebugLayer(); // 开启调试层debug layer
}
#endif

ThrowIfFailed(CreateDXGIFactory1(IID_PPV_ARGS(&mdxgiFactory)));

// 尝试创建硬件设备
HRESULT hardwareResult = D3D12CreateDevice(
    nullptr,
    D3D_FEATURE_LEVEL_11_0,
    IID_PPV_ARGS(&md3dDevice)
);

// 回退到WARP设备
if(FAILED(hardwareResult)){
    ComPtr pWarpAdapter;
    ThrowIfFailed(mdxgiFactory->EnumWarpAdapter(IID_PPV_ARGS(&pWarpAdapter)));

    ThrowIfFailed(D3D12CreateDevice(
        pWarpAdapter.Get(),
        D3D_FEATURE_LEVEL_11_0,
        IID_PPV_ARGS(&md3dDevice)
    ));
}

开启调试层后,D3D便会开启额外的调试功能,并在错误发生时向VC++的输出窗口发送调试信息

D3D12Device创建失败后,程序将回退到一种软件适配器(WARP)设备,WARP(Windows Advanced Rasterization Platform)Windows高级光栅化平台

为了创建WARP适配器,需要先创建一个IDXGIFactory4对象,并通过它来枚举WARP适配器

ComPtr mdxgiFactory;
CreateDXGIFactory1(IID_PPV_ARGS(&mdxgiFactory));
mdxgiFactory->EnumWarpAdapter(IID_PPV_ARGS(&pWarpAdapter));

②创建围栏并获取描述符的大小:

一旦创建好设备,便可以为CPU/GPU的同步创建围栏了

另外,若用描述符进行工作,还需要了解它们的大小。描述符在不同GPU平台上大小各异,需要我们去查询相关的信息并把描述符大小缓存起来,需要时直接应用

ThrowIfFailed(md3dDevice->CreateFence(
    0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&mFence)
));
mRtvDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
mDsvDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_DSV);
mCbvUavDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);

③检测对4X MSAA 质量级别的支持:

在支持D3D 11的硬件上,所有的渲染目标格式就皆已支持4X MSAA,会保证这项功能的正常开启,我们也就无需进行检验。但是,对质量级别的检测还是不可获取的的,为此,可采取下列方法加以实现:

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."); // 支持4xmsaa所以一定大于0

③创建命令队列和命令列表:

ComPtr mCommandQueue;
Comptr mDirectCmdListAlloc;
Comptr mCommandList;

void D3DApp::CreateCommandObjects()
{
    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, // 流水线状态对象 -- 设置为nullptr,由于此时不会发起任何绘制命令,所以用不到
        IID_PPV_ARGS(mCommandList.GetAddressOf())
    ));
    
    // 首先要将命令列表处于关闭模式,因为在第一次引用命令列表时,我们要对其重置,调用重置方法前必须关闭
    mCommandList->Close();
}

④描述并创建交换链:

创建交换链需要填写DXGI_SWAP_CHAIN_DESC结构体实例:

typedef struct DXGI_SWAP_CHAIN_DESC{
    DXGI_MODE_DESC BufferDesc;
    DXGI_SAMPLE_DESC SampleDesc; // 参考4.1.8节 多重采样的质量级别(0)以及每个像素的采样次数(1)
    DXGI_USAGE BufferUsage; // 由于我们要将数据渲染到后台缓冲区(数据作为渲染目标),因此DXGI_USAGE_RENDER_TARGET_OUTPUT
    UINT BufferCount; // 交换链中缓冲区数量(2)
    HWND OutputWindow; // 渲染窗口句柄
    BOOL Windowed; // true:窗口模式 false:全屏模式
    DXGI_SWAP_EFFECT SwapEffect; // DXGI_SWAP_EFFECT_FLIP_DISCARD
    UINT Flags; // 可选标志,DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH,切换为全屏模式时,会选择最适于当前应用程序窗口尺寸的显示模式
} DXGI_SWAP_CHAIN_DESC;

// 其中DXGI_MODE_DESC:
typedef struct DXGI_MODE_DESC
{
    UINT Width; // 缓冲区分辨率的宽度
    UINT Height; // 缓冲区分辨率高度
    DXGI_RATIONAL RefreshRate; // 以赫兹(频率)为单位的刷新率
    DXGI_FORMAT Format; // 缓冲区显示格式
    DXGI_MODE_SCANLINE_ORDER ScanlineOrdering; // 逐行扫描/隔行扫描
    DXGI_MODE_SCALING Scaling; // 图像如何相对于屏幕进行拉伸
} DXGI_MODE_DESC;

描述完交换链后,通过CreateSwapChain函数创建:

HRESULT IDXGIFactory::CreateSwapChain(
    IUnknown *pDevice, // 指向命令队列(ID3D12CommandQueue)接口的指针
    DXGI_SWAP_CHAIN_DESC *pDesc, // 描述交换链结构体的指针
    IDXGISwapChain **ppSwapChain // 返回所创建交换链的接口
);

本书提供的框架中,创建新的交换链之前,先要销毁旧的交换链,这样我们就可以用不同的设置来重新创建交换链,借此在运行时修改多重采样的配置

DXGI_FORMAT mBackBufferFormat = DXGI_FORMAT_R8G8B8A8_UNORM;

void D3DApp::CreateSwapChain()
{
    mSwapChain.Reset(); // 释放之前的交换链
    
    DXGI_SWAP_CHAIN_DESC sd;
    sd.BufferDesc.Width = mClientWidth;
    sd.BufferDesc.Height = mClientHeight;
    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;
    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())
    );
}

⑤创建描述符堆:

Direct3D 12以ID3D12DescriptorHeap接口表示描述符堆,并用ID3D12Device::CreateDescriptorHeap方法创建它。在框架示例代码中,我们将为交换链中SwapChainBufferCount个用于渲染数据的缓冲区资源创建对应的渲染目标视图(RTV),并为用于深度测试的深度模板缓冲区创建一个深度模板视图(DSV)。所以我们需要创建两个描述符堆:

ComPtr mRtvHeap;
ComPtr mDsvHeap;
void D3DApp::CreateRtvAndDsvDescriptorHeaps()
{
    D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc;
    rtvHeapDesc.NumDescriptors = SwapChainBufferCount;
    rtvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;
    rtvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
	rtvHeapDesc.NodeMask = 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())));   
}

// 框架中有以下定义:
static const int SwapChainBufferCount = 2;
int mCurrBackBuffer = 0; // 当前后台缓冲区的索引

创建描述符堆之后,还要能访问其中所存的描述符。在程序中,我们是通过句柄来引用描述符的,并以ID3D12DescriptorHeap::GetCPUDescriptorHandleForHeapStart方法获得描述符堆中第一个描述符的句柄

// 借助以下函数获得当前后台缓冲区的RTV和DSV的:
D3D12_CPU_DESCRIPTOR_HANDLE D3DApp::CurrentBackBufferView()const
{
    // 因为RTV堆中有两个RTV
    // 伪代码: 目标描述符句柄=GetCPUDescriptorHandleForHeapStart()+mCurrBackBuffer*mRtvDescriptorSize
	return CD3DX12_CPU_DESCRIPTOR_HANDLE(
		mRtvHeap->GetCPUDescriptorHandleForHeapStart(),
		mCurrBackBuffer,
		mRtvDescriptorSize);
}

D3D12_CPU_DESCRIPTOR_HANDLE D3DApp::DepthStencilView()const
{
	return mDsvHeap->GetCPUDescriptorHandleForHeapStart();
}

⑥创建渲染目标视图:

为了将后台缓冲区绑定到流水线的输出合并阶段,便需要为该后台缓冲区创建一个RTV

第一步:就是要获取交换链中的缓冲区资源:

HRESULT IDXGISwapChain::GetBuffer(
    UINT Buffer, // 想要获得的后台缓冲区索引
    REFIID riid, // ID3D12Resource接口的COM ID
    void **ppSurface // 返回一个指向ID3D12Resource接口的指针,便是希望获得的后台缓冲区
);

第二步:使用ID3D12Device::CreateRenderTargetView方法来为获取的后台缓冲区创建RTV

void ID3D12Device::CreateRenderTargetView(
    ID3D12Resource *pResource, // 指定用作渲染目标的资源
    const D3D12_RENDER_TARGET_VIEW_DESC *pDesc, // 该结构体描述了资源中元素的数据类型 -- 如果资源在创建时给定了具体格式(不是无类型),那么可以设置为空指针,以采用资源创建时的格式,为它创建第一个mipmap层级(第9章讨论)
    D3D12_CPU_DESCRIPTOR_HANDLE DestDescriptor // 所创建RTV的句柄
);

框架中的示例代码为交换链中的每一个缓冲区都创建了一个RTV:

Microsoft::WRL::ComPtr mSwapChainBuffer[SwapChainBufferCount];

CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHeapHandle(mRtvHeap->GetCPUDescriptorHandleForHeapStart());
for (UINT i = 0; i < SwapChainBufferCount; i++)
{
	ThrowIfFailed(mSwapChain->GetBuffer(i, IID_PPV_ARGS(&mSwapChainBuffer[i])));
	md3dDevice->CreateRenderTargetView(mSwapChainBuffer[i].Get(), nullptr, rtvHeapHandle);
	rtvHeapHandle.Offset(1, mRtvDescriptorSize); // 偏移到描述符堆中的下一个缓冲区
}

⑦创建深度模板缓冲区及其视图:

深度缓冲区是一种纹理资源,我们需要填写D3D12_RESOURCE_DESC结构体来描述纹理资源,再用ID3D12Device::CreateCommittedResource()方法来创建它

typedef struct D3D12_RESOURCE_DESC
{
    D3D12_RESOURCE_DIMENSION Dimension;
    UINT64 Alignment;
    UINT64 Width;
    UINT Height;
    UINT16 DepthOrArraySize;
    UINT16 MipLevels;
    DXGI_FORMAT Format;
    DXGI_SAMPLE_DESC SampleDesc;
    D3D12_TEXTURE_LAYOUT Layout;
    D3D12_RESOURCE_MTSC_FLAG Misc Flags;
} D3D12_RESOURCE_DESC;

// Dimension:
资源的维度 是枚举类型D3D12_RESOURCE_DIMENSION成员之一 
比如:D3D12_RESOURCE_DIMENSION_BUFFER=1,D3D12_RESOURCE_DIMENSION_TEXTURE2D=2

// Width/Height:
以纹素为单位为表示的纹理宽度和高度 
width对于缓冲区资源来说,是缓冲区占用的字节数

// DepthOrArraySize:
以纹素为单位来表示的纹理深度或者(对于1D/2D纹理)是纹理数组的大小
-- D3D不存在3D纹理的概念

// MipLevels:
mipmap层级的数量 对于深度模板缓冲区而言,只能有一个Mipmap层级

// Format:
DXGI_FORMAT层级的数量,纹素的格式
对于深度模板缓冲区而言,此格式4.1.5节中介绍的格式中选择

// SampleDesc:
多重采样的质量级别以及每个像素的采样次数 -- 4.1.7 4.1.8节
深度模板缓冲区与渲染目标的多重采样设置一定要相匹配

// Layout:
D3D12_TEXTURE_LAYOUT枚举类型成员之一,指定纹理的布局
暂时不考虑这个问题,设置为D3D12_TEXTURE_LAYOUT_UNKNOWN即可

// Flags:
与资源有关的杂项设置 
对于深度模板缓冲区而言,此项设置为D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL

GPU资源都存于堆(heap)中,其本质是具有特定属性的GPU显存块 -- CreateCommittedResource方法将根据我们所提供的属性创建一个资源与一个堆,并把该资源提交到这个堆中 

HRESULT ID3D12Device::CreateCommittedResource(
    const D3D12_HEAP_PROPERTIES *pHeapProperties, // 堆属性
    D3D12_HEAP_FLAGS HeapFlags,
    const D3D12_RESOURCE_DESC *pDesc,
    D3D12_RESOURCE_STATES InitialResourceState,
    const D3D12_CLEAR_VALUE *pOptimizedClearValue,
    void **ppvResource
);

// 其中D3D12_HEAP_PROPERTIES:
typedef struct D3D12_HEAP_PROPERTIES{
    D3D12_HEAP_TYPE Type; 
    D3D12_CPU_PAGE_PROPERTY CPUPageProperty;
    D3D12_MEMORY_POOL MemoryPoolPreference;
    UINT CreationNodeMask;
    UINT VisibleNodeMask;
} D3D12_HEAP_PROPERTIES;

// 堆属性中目前只考虑Type成员:
D3D12_HEAP_TYPE枚举类型成员如下:
D3D12_HEAP_TYPE_DEFAULT 默认堆 向堆中提交的资源只有GPU可以访问 // 比如GPU会读写深度模板缓冲区,而CPU也不需要访问它,所以放入默认堆中
D3D12_HEAP_TYPE_UPLOAD 上传堆 向堆中提交的资源都是需要经CPU上传到GPU的资源
D3D12_HEAP_TYPE_READBACK 回读堆 向堆中提交的资源都是CPU需要读取的资源
※D3D12_HEAP_TYPE_CUSTOM 用于高级场景

// HeapFlags:
与堆有关的额外选项标志 通常设置为D3D12_HEAP_FLAG_NONE

// pDesc:
描述待创建资源的结构的指针

// InitialResourceState:
资源的初始状态(参考资源冒险那一节
通常将其设置为D3D12_RESOURCE_STATE_COMMON,再利用ResourceBarrier方法辅以D3D12_RESOURCE_STATE_DEPTH_WRITE状态,将其转化为可以绑定在渲染流水线上的深度模板缓冲区

// pOptimizedClearValue:
优化清除值,可提高清除操作的执行速度 若不希望指定优化清除值,可以把参数设置为nullptr
struct D3D12_CLEAR_VALUE{
    DXGI_FORMAT Format;
    union
    {
        FLOAT Color[4];
        D3D12_DEPTH_STENCIL_VALUE DepthStencil;
    };
} D3D12_CLEAR_VALUE;

// riidResource:
我们希望获得的ID3D12Resource接口的COM ID

// ppvResource:
返回一个指向ID3D12Resource的指针 -- 新建的资源

为了使性能最佳,我们通常将资源放在默认堆中,只有需要其他特性时,才选择其他的堆类型

项目框架中使用深度模板缓冲区的示例代码:

// Create the depth/stencil buffer and view.
D3D12_RESOURCE_DESC depthStencilDesc;
depthStencilDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
depthStencilDesc.Alignment = 0;
depthStencilDesc.Width = mClientWidth;
depthStencilDesc.Height = mClientHeight;
depthStencilDesc.DepthOrArraySize = 1;
depthStencilDesc.MipLevels = 1;

// Correction 11/12/2016: SSAO chapter requires an SRV to the depth buffer to read from 
// the depth buffer.  Therefore, because we need to create two views to the same resource:
//   1. SRV format: DXGI_FORMAT_R24_UNORM_X8_TYPELESS
//   2. DSV Format: DXGI_FORMAT_D24_UNORM_S8_UINT
// we need to create the depth buffer resource with a typeless format.  
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(
    &CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT),
	D3D12_HEAP_FLAG_NONE,
    &depthStencilDesc,
	D3D12_RESOURCE_STATE_COMMON,
    &optClear,
    IID_PPV_ARGS(mDepthStencilBuffer.GetAddressOf())));

// Create descriptor to mip level 0 of entire resource using the format of the resource.
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;
md3dDevice->CreateDepthStencilView(mDepthStencilBuffer.Get(), &dsvDesc, DepthStencilView());
// 书中这里第二个参数填的nullptr,沿用创建资源时的结构

// Transition the resource from its initial state to be used as a depth buffer.
mCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(mDepthStencilBuffer.Get(),
	D3D12_RESOURCE_STATE_COMMON, D3D12_RESOURCE_STATE_DEPTH_WRITE));

⑧设置视口:

视口:后台缓冲区中的某个矩形子区域

typedef struct D3D12_VIEWPORT {
    FLOAT TopLeftX;
    FLOAT TopLeftY;
    FLOAT Width;
    FLOAT Height;
    FLOAT MinDepth;
    FLOAT MaxDepth;
} D3D12_VIEWPORT;

// 在D3D中,存储在深度缓冲区中的数据都是0~1的归一化深度值
// MinDepth MaxDepth负责将深度值从[0~1]->[MinDepth,MaxDepth]
// 大多情况下通常会把MinDepth和MaxDepth分别设为0,1,也就是令深度值保持不变

使用RSSetViewports方法来设置视口:

D3D12_VIEWPORT mScreenViewport;
mScreenViewport.TopLeftX = 0;
mScreenViewport.TopLeftY = 0;
mScreenViewport.Width    = static_cast(mClientWidth);
mScreenViewport.Height   = static_cast(mClientHeight);
mScreenViewport.MinDepth = 0.0f;	
mScreenViewport.MaxDepth = 1.0f;

mCommandList->RSSetViewports(1, &mScreenViewport);
// 第一个参数为视口数量 第二个参数为视口数组指针

不能为同一个目标设置多个视口,多视口用于对多个渲染目标同时进行渲染的高级技术

命令队列一旦被重置,视口也就需要随之重置

视口可用于实现游戏的双人分屏模式

⑨裁剪矩形(scissor rectangle):

裁剪矩形外的像素都会被剔除,这些图像部分不会被光栅化至后台缓冲区。这个方法用于优化程序的性能。例如,游戏有一个矩形的UI界面,其覆盖了某块区域,那么我们也就无序对这块区域被遮挡的像素进行处理了

裁剪矩形由类型为RECT的D3D12_RECT结构体定义而成(typedef RECT D3D12_RECT;)

typedef struct tagRECT
{
    LONG left;
    LONG top;
    LONG right;
    LONG bottom;
} RECT;

D3D中使用RSSetScissorRects方法来设置裁剪矩形

mScissorRect = { 0, 0, mClientWidth/2, mClientHeight/2 };
mCommandList->RSSetScissorRects(1, &mScissorRect);
// 第一个参数为裁剪矩形数量 第二个参数为裁剪矩形数组指针

不能为同一个渲染目标指定多个裁剪矩形,多裁剪矩形是一种用于同时对多个渲染目标进行渲染的高级技术

裁剪矩形需要随命令列表的重置而重置

4.计时与动画

①性能计时器:

性能计时器(performance timer):精确地度量时间所用到的计数器

如果希望调用查询性能计时器地Win32函数,需要引入头文件#include

性能计时器所用的时间度量单位叫做计数(count)

可调用QueryPerformanceCounter函数来获取性能计时器测量的当前时刻值(以计数为单位);可调用QueryPerformanceFrequency函数来获取性能计时器的频率(计数/秒) -- 固定值?

__int64 currTime;
QueryPerformanceCounter((LARGE_INTEGER*)&currTime); // 当前时刻值是个64位整数,以参数形式返回

__int64 countsPerSec;
QueryPerformanceFrequency((LARGE_INTEGER*)&countsPerSec);

每个计数所代表的秒数,就是频率的倒数 -- 固定值?

mSecondsPerCount = 1.0 / (double)countsPerSec;

因此当前时刻值乘以转换因子mSecondsPerCount,可以得到秒数:

valueInSecs = valueInCounts * mSecondsPerCount;

获取一段时间对应的秒数,可以利用两次时间戳之差,在乘上转换因子mSecondsPerCount得到秒数Δt

对于一台具有多个处理器的计算机而言,无论在哪个处理器调用此函数都返回当前时刻的计数值,然而,由于BIOS和HAL上的缺陷,导致在不同的处理器上可能得到不同的结果。

我们可以通过SetThreadAffinityMask函数,防止应用程序的主线程切换到其他的处理器上去执行指令

②游戏计时器类:

class GameTimer
{
public:
	GameTimer();

	float TotalTime()const; // 以秒为单位
	float DeltaTime()const; // 以秒为单位

	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;
};

此类的构造函数会查询性能计时器的频率:

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;
}

③帧与帧之间的时间间隔:

void GameTimer::Tick()
{
	if( mStopped )
	{
		mDeltaTime = 0.0;
		return;
	}

	__int64 currTime;
	QueryPerformanceCounter((LARGE_INTEGER*)&currTime);
	mCurrTime = currTime;

	// 本帧与前一帧的时间差
	mDeltaTime = (mCurrTime - mPrevTime)*mSecondsPerCount;

	// 准备计算本帧与下一帧的时间差
	mPrevTime = mCurrTime;

	// 强制使时间差为非负值
    // DXSDK中的CDXUTTimer示例注释里提到:如果处理器处于节能模式,或者在计算两帧时间差的过程中切换到另一个处理器时,则mDeltaTime可能为负值
	if(mDeltaTime < 0.0)
	{
		mDeltaTime = 0.0;
	}
}
float GameTimer::DeltaTime()const
{
	return (float)mDeltaTime;
}

Tick函数被调用于程序的消息循环之中:

int D3DApp::Run()
{
	MSG msg = {0};
 
	mTimer.Reset();

	while(msg.message != WM_QUIT)
	{
		// 如果有窗口消息就进行处理
		if(PeekMessage( &msg, 0, 0, 0, PM_REMOVE ))
		{
            TranslateMessage( &msg );
            DispatchMessage( &msg );
		}
		// 否则就执行动画与游戏的相关逻辑
		else
        {	
			mTimer.Tick(); 

			if( !mAppPaused )
			{
				CalculateFrameStats();
				Update(mTimer);	
                Draw(mTimer);
			}
			else
			{
				Sleep(100);
			}
        }
    }

	return (int)msg.wParam;
}

我们在每帧都计算Δt,并将其送入Update方法,只有这样,才可以根据前一动画帧所花费的时间对场景进行更新。

Reset代码实现:

void GameTimer::Reset()
{
	__int64 currTime;
	QueryPerformanceCounter((LARGE_INTEGER*)&currTime);

	mBaseTime = currTime;
	mPrevTime = currTime; // 因为第一帧画面之前没有任何动画帧,所以暂时将上一帧时间设置为当前时刻
	mStopTime = 0;
	mStopped  = false;
}

⑤总时间:

为了统计总时间,我们使用下列变量:

__int64 mBaseTime; // 应用程序的开始时刻 -- 在调用Reset函数时,设置为当前时刻
__int64 mPausedTime; // 所有暂停时间之和
__int64 mStopTime; // 计时器暂停的时刻,借此即可记录暂停的时间

Stop和Start是两个关键方法:

void GameTimer::Start()
{
	__int64 startTime;
	QueryPerformanceCounter((LARGE_INTEGER*)&startTime);


	// Accumulate the time elapsed between stop and start pairs.
	//
	//                     |<-------d------->|
	// ----*---------------*-----------------*------------> time
	//  mBaseTime       mStopTime        startTime     

	if( mStopped )
	{
		mPausedTime += (startTime - mStopTime);	

		mPrevTime = startTime;
		mStopTime = 0;
		mStopped  = false;
	}
}

void GameTimer::Stop()
{
	if( !mStopped )
	{
		__int64 currTime;
		QueryPerformanceCounter((LARGE_INTEGER*)&currTime);

		mStopTime = currTime;
		mStopped  = true;
	}
}

因此TotalTime函数:

// Returns the total time elapsed since Reset() was called, NOT counting any
// time when the clock is stopped.
float GameTimer::TotalTime()const
{
	// 如果处于停止时刻,则忽略本次停止时刻到当前时刻的时间。
    // 此外,如果之前已有停止的情况,那么也不统计mStopTime-mBaseTime这段时间内的暂停时间
    // 为了做到这一点,可以从mStopTime-mPausedTime [mStopTime是最右侧的]
	//
	//                     |<--paused time-->|
	// ----*---------------*-----------------*------------*------------*------> time
	//  mBaseTime       mStopTime        startTime     mStopTime    mCurrTime

	if( mStopped )
	{
		return (float)(((mStopTime - mPausedTime)-mBaseTime)*mSecondsPerCount);
	}

	// 我们并不希望统计mCurrTime-mBaseTime内的暂停时间 
	//
	//  (mCurrTime - mPausedTime) - mBaseTime 
	//
	//                     |<--paused time-->|
	// ----*---------------*-----------------*------------*------> time
	//  mBaseTime       mStopTime        startTime     mCurrTime
	
	else
	{
		return (float)(((mCurrTime-mPausedTime)-mBaseTime)*mSecondsPerCount);
	}
}

在演示框架中,为了度量从程序开始到某时刻的总时间而创建一个GameTimer实例。其实,我们可以再创建一个GameTimer实例,把它当作一个通用的秒表。比如炸弹被点燃时,我们开启一个全新的GameTimer实例,当TotalTime达到5秒时就触发爆炸事件

5.应用程序框架示例:

d3dUtil.h d3dUtil.cpp d3dApp.h d3dApp.c

框架中代码后续单独开一个博客研究~

你可能感兴趣的:(DirectX,笔记,游戏,学习,c++)