• DirectX12 3D 游戏开发与实战第四章内容(下)


    Direct3D的初始化(下)

    学习目标

    1. 了解Direct3D在3D编程中相对于硬件所扮演的角色
    2. 理解组件对象模型COM在Direct3D中的作用
    3. 掌握基础的图像学概念,例如2D图像的存储方式,页面翻转,深度缓冲,多重采样以及CPU和GPU之间的交互
    4. 学习使用性能计数器函数,依次读取高精度计时器的数值
    5. 了解Direct3D的初始化过程
    6. 熟悉本书应用程序框架的整体结构,在后续的演示程序中可以经常看到应用程序框架的整体结构

    4.3初始化Direct3D

    对Direct3D进行初始化可以分为以下几个步骤

    1. 用D3D12CreateDevice函数创建ID3D12Device接口实例
    2. 创建一个ID3D12Fence对象,并查询描述符的大小
    3. 检测用户设备对4X MSAA质量级别的支持情况
    4. 依次创建命令队列,命令列表分配器,主命令列表
    5. 描述并创建交换链
    6. 创建应用程序需要的描述符堆
    7. 调整后台缓冲区的大小,并为它创建渲染目标视图
    8. 创建深度/模板缓冲区以及与之关联的深度/模板缓冲区视图
    9. 设置视口(viewport)和裁剪矩形(scissor rectangle)

    4.3.1创建设备

    要初始化Direct3D,必须先创建Direct3D12设备。Direct3D12设备相当于一个显示适配器,显示适配器一般都是一种3D图像硬件(如显卡),但也可以用软件显示适配器来模拟3D图形硬件功能,该设备可以检测系统环境对功能的支持情况,又可以用来创建所有其他的Direct3D接口对象(如资源,命令列表,视图(描述符)等等)。我们可以通过一下函数创建Direct3D12设备:

    //@param:指定在创建设备的时候所用的显示适配器,如果把该指针设为空,则默认使用主显示适配器
    //@param:应用程序需要硬件所支持的最低功能级别
    //@parma:该ID3DDevice接口的COM ID
    //@parma:返回所创建的Direct3D12设备
    HRESULT WINAPI mD3D12CreateDevice(
    	IUnknown* pAdapter,
    	D3D_FEATURE_LEVEL MinimumFeatureLevel,
    	REFIID riid,
    	void** ppDevice
    );
    

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

    创建好设备之后,我们便可以为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);
    //常量缓冲区/着色器资源/无序访问视图(描述符)大小
    mCbvSrvUavDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
    

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

    凡是支持Direct3D11的硬件,都可以支持多重采样技术的开启。所以我们可以不用对此进行检测,但是,对质量级别的检测还是必不可少的。我们可以采用下面的方法进行检测:

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

    4.3.4创建命令队列和命令列表

    在前面的章节可知,ID3D12CommandQueue接口表示命令队列,ID3D12CommandAllocator接口表示命令分配器,ID3D12CommandList接口表示命令列表,下面我们将分别展示这几种对象的创建流程:

    ComPtr<ID3D12CommandQueue> mCommandQueue;
    ComPtr<ID3D12CommandAllocator> mDirectCmdListAlloc;
    ComPtr<ID3D12CommandList> 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, IID_PPV_ARGS(mCommandList.GetAddressOf())));
    
    	//首先要把命令列表关闭,因为第一次使用命令列表时我们要把命令列表重置,重置之前必须确保命令列表
    	//已经关闭
    	mCommandList->Close();
    	}
    

    4.3.5描述并创建交换链

    在创建交换链之前,我们要先填写一份DXGI_SWAP_CHAIN_DESC结构体实例,用它来描述即将创建的交换链的特性。此结构体定义如下:

    typedef struct DXGI_SWAP_CHAIN_DESC {
    	DXGI_MODE_DESC BufferDesc;
    	DXGI_SAMPLE_DESC SampleDesc;
    	DXGI_USAGE BufferUsage;
    	UINT BufferCount;
    	HWND OutputWindow;
    	BOOL Windowed;
    	DXGI_SWAP_EFFECT SwapEffect;
    	UINT Flags;
    }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;
    

    下面的代码将会展示如何在本书的演示框架下方便的创建交换链:

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

    4.3.6创建描述符堆

    在程序中,我们需要创建描述符堆来存储程序中需要用到的描述符(视图),在Direct12中,ID3D12DescriptorHeap接口表示描述符堆,并用ID3D12Device::CreateDescriptorHeap方法来创建描述符堆,在下面的演示代码中,我们将创建两个描述符堆,一个用来存储SwapChainBufferCount个渲染目标视图(Render Target View),还有一个用来存储1个深度/模板视图(Depth/Stencil View)。

    ComPtr<ID3D12DescriptorHeap> mRtvHeap;
    ComPtr<ID3D12DescriptorHeap> 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())));
    }
    

    4.3.7创建渲染目标视图

    由于资源不能直接和渲染流水线直接进行绑定,所以我们需要先为资源创建视图(描述符),并将其绑定到渲染流水线中。为了向后台缓冲区创建一个渲染目标视图,我们需要先获得交换链中的缓冲区资源。

    我们可以通过IDXGISwapChain::GetBuffer()方法后去交换链中的缓冲区资源,每次调用该方法之后,会增加相关后台缓冲区的引用次数,所以在每一次使用后都要释放,我们可以通过Comptr自动实现这个功能。

    接下来,我们可以使用ID3D12Device::CreateRenderTargetView()方法来为获取的后台缓冲区资源创建渲染目标视图。

    以下实例将会通过调用这两个方法为交换链中的每一个缓冲区都创建一个RTV:

    ComPtr<ID3D12Resource> mSwapChainBuffer[SwapChainBufferCount];
    CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHeapHandle(
    	mRtvHeap->GetCPUDescriptorHandleForHeapStart()
    );
    for (UINT i = 0; i < SwapChainBufferCount; i++)
    {
    	//获取交换链中第i个缓冲区
    	ThrowIfFailed(mSwapChain->GetBuffer(i,
    		IID_PPV_ARGS(&mSwapChainBuffer[i])));
    
    	//为此缓冲区创建一个RTV
    	md3dDevice->CreateDepthStencilView(mSwapChainBuffer[i].Get(), nullptr, rtvHeapHandle);
    
    	//偏移到描述符的下一个缓冲区
    	rtvHeapHandle.Offset(1, mRtvDescriptorSize);
    }
    

    4.3.8创建深度/模板缓冲区及其视图(描述符)

    深度缓冲区是一种纹理资源,它存储着离观察者最近的可视对象的深度信息(如果使用了模板,还有附有模板信息)。因为纹理是一种资源,所以我们需要通过填写D3D12_RESOURCE_DESC结构体来描述纹理资源,在使用ID3D12DeveiceCreateCommittedResource方法来创建它。以下代码为D3D12_RESOURCE_DESC结构体的定义:

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

    GPU资源都存于堆(Heap)中,其本质是具有特定属性的GPU显存块,ID3D12Device::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,
    		REFIID riidResource,
    		void ** ppvResource
    		);
    
    	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;
    

    在使用深度/模板缓冲区之前,一定要先创建相关的深度/模板缓冲区视图(描述符),并将它绑定到渲染流水线中。下面的代码将会展示如何创建深度/模板纹理资源以及相对应的深度/模板缓冲区视图(描述符)

    //创建深度/模板缓冲区视图
    D3D12_RESOURCE_DESC depthStencilDesc;
    //资源的维度
    depthStencilDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
    depthStencilDesc.Alignment = 0;
    //以纹素为单位来表示纹理宽度(如果是缓冲区资源,此项表示缓冲区占用的字节数)
    depthStencilDesc.Width = mClientWidth;
    //以纹素为单位来表示纹理高度
    depthStencilDesc.Height = mClientHeight;
    //以纹素为单位来表示纹理深度
    depthStencilDesc.DepthOrArraySize = 1;
    //mipmap层级的数量(后续讲纹理时会介绍mipmap)
    depthStencilDesc.MipLevels = 1;
    //DXGI_FORMAT枚举类型中的成员之一,用于指定纹素的格式
    depthStencilDesc.Format = mDepthStencilFormat;
    //多重采样的质量级别以及对每一个像素的采样次数
    depthStencilDesc.SampleDesc.Count = m4xMsaaState ? 4 : 1;
    depthStencilDesc.SampleDesc.Quality = m4xMsaaState ? (m4xMsaaQuality - 1) : 0;
    //D3D12_TEXTURE_LAYOUT枚举类型的成员之一,用来指定纹理的布局
    depthStencilDesc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN;
    //与资源有关的杂项标志
    depthStencilDesc.Flags = D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL;
    
    //创建一个指向一个D3D12_CLEAR_VALUE对象的指针,该指针描述了一个用于清除资源的优化值,
    //选择适当的优化值可以提高清除操作的效率,如果不希望指定优化值,也可以不创建。
    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())
    ));
    
    //利用此资源的格式,为整个资源的第0层mip创建描述符
    md3dDevice->CreateDepthStencilView(
    	mDepthStencilBuffer.Get(),
    	nullptr,
    	DepthStencilView()
    );
    
    //将资源从初始状态转换到深度缓冲区
    mCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(mDepthStencilBuffer.Get(),
    		D3D12_RESOURCE_STATE_COMMON,D3D12_RESOURCE_STATE_DEPTH_WRITE));
    

    4.3.9设置视口

    视口:我们通常会把3D场景绘制到整个屏幕中,或整个窗口工作区大小相当的后台缓冲区中,但是,有些时候我们只希望把3D场景绘制到后台缓冲区中的某一个矩形子区域中,而这个矩形子区域就称为视口

    视口的结构体定义如下:

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

    填写好D3D12_VIEWPORT结构体之后,我们便可以通过ID3D12GraphicsComandList::RSSetViewPort()方法来设置Direct3D中的视口了。下面将会展示通过创建和设置一个视口,把场景绘制到整个后台缓冲区中

    D3D12_VIEWPORT vp;
    vp.TopLeftX = 0.0f;
    vp.TopLeftY = 0.0f;
    vp.Width = static_cast<float>(mClientWidth);
    vp.Height = static_cast<float>(, mClientHeight);
    vp.MinDepth = 0.0f;
    vp.MaxDepth = 1.0f;
    
    //@param:绑定的视口数量
    //@param:指向视口数组的指针
    mCommandList->RSSetViewports(1, &vp);
    

    4.3.10设置裁剪矩形
    我们可以在相对于后台缓冲区定义一个裁剪矩形,在这个矩形之外的像素都不会被光栅化到后台缓冲区(被剔除),这个方法可以优化程序的性能。比如我们在游戏界面放置了一个UI,我们可以通过设置裁剪矩形使程序不必对3D空间中那些被它遮挡的像素进行处理了。设置裁剪矩形和设置视口一样,要先填写一个D3D12_RECT结构体,该结构体由类型为RECT的D3D12结构体定义而成:

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

    在Direct3D中,要用ID3D12GraphicsCommandList::RSSetScisorRects方法来设置裁剪矩形,下面的实例展现了如何创建并设置一个覆盖后台缓冲区左上角四分之一区域的裁剪矩形

    mScissorRect = { 0,0,mClientWidth / 2,mClientHeight / 2 };
    mCommandList->RSSetScissorRects(1, &mScissorRect);
    

    4.4计时与动画

    为了制作出精准的动画效果就需要精确到计量时间,特别是要准确的度量出动画每帧画面之间的时间间隔,所以了解相关的知识是十分必要的

    4.4.1性能计时器

    为了精确的计量时间,我们将采用性能计时器(performance timer),性能计时器的单位是计数,可以通过QueryPerformanceCounter函数来获取性能计时器测量的当前时刻值(以计数为单位)

    用QueryPerformanceFrequency函数获取性能计时器的频率(单位:计数/秒),通过单位可以看出,如果希望把QueryPerformanceCounter函数获取的时刻值的单位转换为秒,可以通过时刻值(计数)/性能计时器的频率(计数/秒)得到

    4.4.2游戏计时类

    class GameTimer
    {
    public:
    	GameTimer();
    
    	float TotlaTime()const;	//以秒为单位(总时间,不计暂停的时间)
    	float DeltaTime()const;	//以秒为单位(本帧与前一帧的时间差)
    
    	void Reset();		//在开始消息循环之前调用
    	void Start();		//解除计时器暂停时调用
    	void Stop();		//暂停计时器时调用
    	void Tick();		//每一帧都要调用
    
    private:
    	double mSecondPerCount;
    	double mDeltaTime;
    
    	__int64 mBastTime;			//应用程序开始运行的时间
    	__int64 mPauseTime;			//所有暂停时间的总和
    	__int64 mPauseTime;			//暂停的时刻
    	__int64 mPrevTime;			//前一帧的时刻
    	__int64 mCurrTime;			//当前的时刻
    
    	bool mStopped;
    
    };
    

    下面是游戏计时类的构造函数的代码:

    GameTimer::GameTimer() :mSecondsPerCount(0.0f), mPrevTime(0), mCurrTime(0), mStopped(false)
    {
    	__int64 countPersec;
    	QueryPerformanceFrequency((LARGE_INTEGER*)&countPersec);
    	mSecondsPerCount = 1.0 / (double)countPersec;
    }
    

    剩下的一些方法我们会在下面几节介绍。

    4.4.3帧与帧之间的间隔

    当渲染动画帧时,我们需要知道帧与帧之间的间隔,以此来根据时间的流逝对游戏对象进行更新。所以我们要计算出前一帧t1和后一帧t2的差值,即t2 - t1。计算差值的代码如下:

    void GameTimer::Tick()
    {
    	if (mStopped)
    	{
    		mDeltaTime = 0.0f;
    		return;
    	}
    
    	//获得本帧开始显示的时间
    	__int64 currTime;
    	QueryPerformanceCounter((LARGE_INTEGER*)currTime);
    	mCurrTime = currTime;
    
    	//本帧与前一帧的时间差
    	mDeltaTime = (mCurrTime - mPrevTime)*mSecondsPerCount;
    
    	//准备计算下一帧的时间差
    	mPrevTime = mCurrTime;
    
    	if (mCurrTime < 0)
    	{
    		mCurrTime = 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);
    			}
    		}
    	}
    }
    

    下面是GameTimer::Reset()方法的实现:

    void GameTimer::Reset()
    {
    	__int64 currTime;
    	QueryPerformanceCounter((LARGE_INTEGER*)&currTime);
    
    	mBaseTime = currTime;
    	mPrevTime = currTime;
    	mPausedTime = 0;
    	mStopped = false;
    }
    

    总时间

    总时间:总时间是一种自应用程序开始,不计中间暂停时间的时间总和。下面代码将会分别展示游戏计时类的Stop(),Start()和TotalTime()三个方法。

    void GameTimer::Stop()
    {
    	//如果已经处于停止状态,则直接退出函数
    	if (mStopped)
    	{
    		return;
    	}
    	__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;
    	}
    }
    

    4.5应用程序框架

    4.5.1D3DApp类

    D3DApp类是一种基础的Direct3D应用程序类,它提供了创建应用程序主窗口,运行程序消息循环,处理窗口信息以及初始化Direct3D等多种功能的函数,而且它为应用程序例程定义了一组框架函数,我们可以根据需求通过实例化一个继承自D3DApp的类,重写框架里的虚函数,便可以从D3DApp类中派生出自定义的用户代码。如果想要查看D3DApp类的定义,可以在GitHub上自行查找。

    4.5.2非框架方法

    4.5.3框架内容

    4.5.4帧的统计信息

    游戏和图形应用程序往往都会测量每秒渲染的帧数(frame per second,FPS)作为一种画面流畅度的标杆,因此,我们需要统计在特定时间t内所处理的帧数n,则时间t内的平均帧数(FPS)为n/t。在Direct3D中,提供了D3DApp::CalculateFrameStats方法来计算FPS的相关信息。D3DAPP::CalculateFrameStats()方法的定义如下

    void D3DApp::CalculateFrameStats()
    {
    	static int frameCnt = 0;
    	static float timeElapsed = 0;
    
    	//以1秒为统计周期来计算每一帧的平均帧数以及每一帧所花费的渲染时间
    	if ((mTimer.TotalTime() - timeElapsed) >= 1.0f)
    	{
    		float fps = (float)frameCnt;
    		//计算渲染一帧所花费的时间(以毫秒为单位)
    		float mspf = 1000.0f / fps;
    
    		wstring fpsStr = to_wstring(fps);
    		wstring mspfStr = to_wstring(mspf);
    
    		wstring windowText = mMainWndCaption + L"fps:" + fpsStr + L"mspf:" + mspfStr;
    
    		//为计算下一帧重置相关值
    		frameCnt = 0;
    		timeElapsed += 1.0f;
    	}
    }
    

    4.5.5消息处理函数

    接下来我们将了解一下如何处理一些我们要亲自处理的消息,实际上应用程序代码都是在没有窗口信息可以处理时执行的,但是有一些重要的信息需要我们亲自去处理

    1、WM_ACTIVATE:当一个程序被激活或者进入非活动状态时会发送此消息

    2、WM_SIZE:当用户调整窗口的大小的时候会发送此消息,主要目的是可以让我们每次随着用户的调整而对后台缓冲区和深度/模板缓冲区的大小和工作区矩形范围的大小保持一致(这样便不会出现图形拉伸的bug了)

    3、WM_ENTERSIZEMOVE:当用户抓取调整栏时发送WM_ENTERSIZEMOVE消息。

    4、WM_EXITSIZEMOVE:当用户释放调整栏时发送WM_EXITSIZEMOVE消息。

    5、WM_DESTROY:当窗口被销毁时发送WM_DESTROY消息

    6、WM_LBUTTONDOWN/WM_MBUTTONDOWN/WM_RBUTTONDOWN:按下鼠标左键/中键/右键时

    7、WM_LBUTTONUP/WM_MBUTTONUP/WN_RBUTTONUP:松开鼠标左键/中键/右键时

  • 相关阅读:
    087 类的继承
    086 一切皆对象
    085 对象的绑定方法
    在菜鸟教程学 HTML(一)
    生成输出 URL(16.2)
    创建分部视图(7.6.3)
    显示页面链接(7.5.1)
    NuGet 控制台代码
    浏览器对应用程序的根URL发出请求时所发生的情况(结合 DI)
    第 6 章 —— 依赖项注入(DI)容器 —— Ninject
  • 原文地址:https://www.cnblogs.com/yaya12138/p/11546351.html
Copyright © 2020-2023  润新知