美文网首页
【DirectX12笔记】第4章 DirectX初始化

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

作者: crossous | 来源:发表于2019-07-10 01:45 被阅读0次
    封面

    前言

      本文是《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页有相关介绍。
      这里用到了两个成员变量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能退出:

    相关文章

      网友评论

          本文标题:【DirectX12笔记】第4章 DirectX初始化

          本文链接:https://www.haomeiwen.com/subject/ksywhctx.html