在上一篇博客中,我们介绍了D3D12初始化流程实现过程,本篇博客我们搭建一个简易的迷你型小框架用于实现D3D12的初始化流程,在游戏编程中,都需要定时器的封装,比如骨骼动画需要,联网也需要定期判断是否断线,断线后多久开始重连,帧的时间间隔等等,可见时间定时器是很重要的,在这里未雨绸缪也实现一个定时器,为了准确测量时间,使用性能计时器(或性能计数器), 要使用Win32函数查询性能计时器,必须包含#include
__int64 currTime;
QueryPerformanceCounter((LARGE_INTEGER*)&currTime);
注意,此函数通过其参数返回当前时间值,该参数是64位整数值,为了获得性能计时器的频率(每秒计数),我们使用QueryPerformanceFrequency函数:
__int64 countsPerSec;
QueryPerformanceFrequency((LARGE_INTEGER*)&countsPerSec);
每个计数的秒数(或几分之一)就是每秒计数的倒数:
mSecondsPerCount = 1.0 / (double)countsPerSec;
因此,要将时间读取valueInCounts转换为秒,我们只需将其乘以转换因子
mSecondsPerCount
valueInSecs = valueInCounts * mSecondsPerCount;
我们所做的是使用QueryPerformanceCounter获取当前时间值,然后再使用QueryPerformanceCounter获取当前时间值。 也就是说,我们总是查看两个时间戳之间的相对差异来衡量时间,而不是性能计数器返回的实际值。 以下代码更好地说明了这个想法:
__int64 A = 0;
QueryPerformanceCounter((LARGE_INTEGER*)&A);
/* Do work */
__int64 B = 0;
QueryPerformanceCounter((LARGE_INTEGER*)&B);
因此,(B-A)或(B-A)* mSecondsPerCount秒来完成计数。
MSDN对QueryPerformanceCounter有如下评论:“在多处理器计算机上,调用哪个处理器无关紧要。 但是,由于基本输入/输出系统(BIOS)或硬件抽象层(HAL)中的错误,您可以在不同的处理器上获得不同的结果。“您可以使用SetThreadAffinityMask函数,以便主应用程序线程不会切换到 另一个处理器
分析完成后,接下来实现时间类GameTimer :
class GameTimer
{
public:
GameTimer();
float GameTime()const; // in seconds
float DeltaTime()const; // in seconds
void Reset(); // Call before message loop.
void Start(); // Call when unpaused.
void Stop(); // Call when paused.
void Tick(); // Call every frame.
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;
}
对于实时渲染,我们通常需要每秒至少30帧才能获得平滑动画(而且我们通常具有更高的速率); 我们实现了一个计时器函数如下所示:
void GameTimer::Tick()
{
if( mStopped )
{
mDeltaTime = 0.0;
return;
}
// Get the time this frame.
__int64 currTime;
QueryPerformanceCounter((LARGE_INTEGER*)&currTime);
mCurrTime = currTime;
// Time difference between this frame and the previous.
mDeltaTime = (mCurrTime - mPrevTime)*mSecondsPerCount;
// Prepare for next frame.
mPrevTime = mCurrTime;
// Force nonnegative. The DXSDK’s CDXUTTimer mentions that if the
// processor goes into a power save mode or we get shuffled to
// another processor, then mDeltaTime can be negative.
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 there are Window messages then process them.
if(PeekMessage( &msg, 0, 0, 0, PM_REMOVE ))
{
TranslateMessage( &msg );
DispatchMessage( &msg );
}
// Otherwise, do animation/game stuff.
else
{
mTimer.Tick();
if( !mAppPaused )
{
CalculateFrameStats();
Update(mTimer);
Draw(mTimer);
}
else
{
Sleep(100);
}
}
}
return (int)msg.wParam;
}
计时器重置函数如下所示:
void GameTimer::Reset()
{
__int64 currTime;
QueryPerformanceCounter((LARGE_INTEGER*)&currTime);
mBaseTime = currTime;
mPrevTime = currTime;
mStopTime = 0;
mStopped = false;
}
另一个有用的时间测量是从应用程序启动以来经过的时间量,不包括暂停时间; 我们称之为总时间。 以下情况说明了这可能有用。 假设玩家有300秒完成一个关卡。 当级别开始时,我们可以得到时间tstart,它是自应用程序启动以来经过的时间。 然后在级别开始之后,我们经常可以检查自应用程序启动以来的时间t。 如果t - tstart> 300s(参见图4.10),那么玩家已经处于该等级超过300秒并且输了。 显然在这种情况下,我们不希望任何时候游戏暂停对玩家。
计算自关卡开始以来的时间,请注意,我们选择应用程序开始时间作为原点(0),并测量相对于该参考帧的时间值。
为了实现总时间,我们使用以下变量:
__int64 mBaseTime;
__int64 mPausedTime;
__int64 mStopTime;
当调用Reset时,mBaseTime被初始化为当前时间。 我们可以将此视为应用程序启动的时间。 在大多数情况下,您只会在消息循环之前调用Reset一次,因此mBaseTime在整个应用程序的生命周期内保持不变。 变量mPausedTime会累积暂停时经过的所有时间。 我们需要累积这个时间,以便我们可以从总运行时间中减去它,以便不计算暂停时间, mStopTime变量为我们提供了计时器停止(暂停)的时间,这用于帮助我们跟踪暂停时间。
GameTimer类的两个重要方法是Stop和Start, 应用程序分别暂停和取消暂停时应调用它们,以便GameTimer可以跟踪暂停时间,代码注释解释了这两种方法的细节。
void GameTimer::Stop()
{
// If we are already stopped, then don’t do anything.
if( !mStopped )
{
__int64 currTim就 QueryPerformanceCounter((LARGE_INTEGER*)&currTime);
// Otherwise, save the time we stopped at, and set
// the Boolean flag indicating the timer is stopped.
mStopTime = currTime;
mStopped = true;
}
}
void GameTimer::Start()
{
__int64 startTime;
QueryPerformanceCounter((LARGE_INTEGER*)&startTime);
// Accumulate the time elapsed between stop and start pairs.
//
// |<-------d------->|
// ---------------*-----------------*-----------u-> time
// mStopTime startTime
// If we are resuming the timer from a stopped state...
if( mStopped )
{
// then accumulate the paused time.
mPausedTime += (startTime - mStopTime);
// since we are starting the timer back up, the current
// previous time is not valid, as it occurred while paused.
// So reset it to the current time.
mPrevTime = startTime;
// no longer stopped...
mStopTime = 0;
mStopped = falise;
}
}
最后,TotalTime成员函数返回自调用Reset以来经过的时间,实现如下:
float GameTimer::TotalTime()const
{
// If we are stopped, do not count the time that has passed
// since we stopped. Moreover, if we previously already had
// a pause, the distance mStopTime - mBaseTime includes paused
// time,which we do not want to count. To correct this, we can
// subtract the paused time from mStopTime:
//
// previous paused time
// |<----------->|
// ---*------------*-------------*-------*-------j----*------> time
// mBaseTime mStopTime mCurrTime
if( mStopped )
{
return (float)(((mStopTime - mPausedTime)-
mBaseTime)*mSecondsPerCount);
}
// The distance mCurrTime - mBaseTime includes paused time,
// which we do not want to count. To correct this, we can subtract
// the paused time from mCurrTime:
//
// (mCurrTime - mPausedTime) - mBaseTime
//
}
// The distance mCurrTime - mBaseTime includes paused time,
// which we do not want to count. To correct this, we can subtract
// the paused time from mCurrTime:
//
// (mCurrTime - mPausedTime) - mBaseTime
//
// |<--paused time-->|
// ----*---------------*-----------------*------------*------> time
// mBaseTime mStopTime startTime mCurrTime
else
{
return (float)(((mCurrTime-mPausedTime)-
mBaseTime)*mSecondsPerCount);
}
}
我们的演示框架创建了一个GameTimer实例,用于测量从应用程序启动以来的总时间,以及帧之间经过的时间, 当然,我们也可以创建其他实例用作“秒表”。例如,当炸弹被点燃时,可以启动新的GameTimer,当TotalTime达到5秒时,就可以引发炸弹爆炸的事件。
在这里定时器讲的比较多,它也是最重要的,也是框架的一部分,下面先把框架的结构给读者看一下:
D3DApp类是基本的Direct3D应用程序类,它提供了创建主应用程序窗口,运行应用程序消息循环,处理窗口消息和初始化Direct3D的功能。 此外,该类定义了演示应用程序的框架函数。 客户端派生自D3DApp,覆盖虚拟框架函数,并仅实例化派生D3DApp类的单个实例,上图中的InitDirect3DApp就是继承此类,前面D3D12初始化流程已经给读者介绍完了,其他的大家看看代码调试一下就明白了,我用的系操作统是Win10,编辑器是VS2017。
在此给读者把技术点和注意事项总结如下:
Direct3D可以被认为是程序员和图形硬件之间的中介, 例如,程序员调用Direct3D函数将资源视图绑定到硬件渲染管道,配置渲染管道的输出以及绘制3D几何。
组件对象模型(COM)是一种允许DirectX独立于语言并具有向后兼容性的技术, Direct3D程序员不需要知道COM的细节及其工作原理,只需知道如何获取COM接口以及如何释放它们。
1D纹理类似于数据元素的1D阵列,2D纹理类似于数据元素的2D数组,3D纹理类似于数据元素的3D数组, 纹理的元素必须具有由DXGI_FORMAT枚举类型的成员描述的格式, 纹理通常包含图像数据,但是它们也可以包含其他数据,例如深度信息(例如,深度缓冲器), GPU可以对纹理执行特殊操作,例如过滤和多重采样。
为了避免动画中的闪烁,最好将整个动画帧绘制到称为后台缓冲区的屏幕外纹理中,一旦整个场景被绘制到给定动画帧的后缓冲区,它就作为一个完整的帧呈现给屏幕; 在帧被绘制到后缓冲区之后,后缓冲区和前缓冲区的角色相反:后缓冲区变为前缓冲区,前缓冲区成为下一帧动画的后缓冲区。 交换后台和前台缓冲区的角色称为呈现。 前后缓冲区形成交换链,由IDXGISwapChain接口表示, 使用两个缓冲区(前面和后面)称为双缓冲。
假设不透明的场景对象,最靠近相机的点会遮挡它们后面的物体, 深度缓冲是用于确定最靠近相机的场景中的点的技术,通过这种方式,我们不必担心绘制场景对象的顺序。
在Direct3D中,资源不直接绑定到管道, 相反,我们通过指定将在draw调用中引用的描述符将资源绑定到呈现管道, 描述符对象可以被认为是轻量级结构,用于标识和描述GPU的资源。 可以创建单个资源的不同描述符,通过这种方式,可以以不同的方式查看单个资源; 例如,绑定到渲染管道的不同阶段或将其解释为不同的DXGI_FORMAT。 应用程序创建描述符堆,形成描述符的内存支持。
ID3D12Device是主要的Direct3D接口,可以被认为是物理图形设备硬件的软件控制器; 通过它,可以创建GPU资源,并创建其他专用接口,用于控制图形硬件并指导它做事。
GPU有指令队列, CPU使用指令列表通过Direct3D API将指令提交到队列, 指令指示GPU执行某些操作, 在GPU到达队列前端之前,GPU不会执行提交的指令。 如果指令队列变空,GPU将空闲,因为它没有任何工作要做; 另一方面,如果命令队列太满,CPU在某些时候必须在GPU赶上时空闲, 这两种情况都未充分利用系统的硬件资源。
GPU是系统中与CPU并行运行的第二个处理器,有时CPU和GPU需要同步。 例如,如果GPU在其队列中有一个引用资源的指令,则在GPU完成之前,CPU不得修改或销毁该资源。 任何导致其中一个处理器等待和空闲的同步方法都应该最小化,因为这意味着没有充分利用这两个处理器。
性能计数器是一个高分辨率计时器,可提供测量小时间差异所需的精确定时测量,例如帧之间经过的时间, 性能计时器以称为计数的时间单位工作, QueryPerformanceFrequency输出性能计时器的每秒计数,然后可用于将计数单位转换为秒, 使用QueryPerformanceCounter函数获取性能计时器的当前时间值(以计数度量)。
示例框架用于提供一致的界面,博客中的所有演示应用程序都遵循该界面, d3dUtil.h,d3dUtil.cpp,d3dApp.h和d3dApp.cpp文件中提供的代码包装了每个应用程序必须实现的标准初始化代码。
最后提供了一个Debug输出函数,可以查看输出的信息,找到问题所在。
代码下载地址:链接:https://pan.baidu.com/s/1X0Vikf6qGYGPKU-Nwf-wYA 密码:h79q