D3D渲染技术之简易框架
在上一篇博客中,我们介绍了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
阅读更多- D3D中的一项高级技术---渲染到纹理
- 简易C++测试框架
- D3D Vetex Buffer 多流 渲染
- python自动化框架构思及相关技术的学习
- 熟悉Flask框架------第一天(调试模式、路由、模板渲染等)
- 搭建一个微服务框架所需要哪些技术(spring-cloud)
- Flash渲染技术公式
- AJAX技术框架及开发工具
- Niagara解决设备连接应用的软件框架平台技术。
- 一起谈.NET技术,ASP.NET MVC验证框架中关于属性标记的通用扩展方法
- Java知识拾遗:三大框架的技术起源
- 阿里巴巴分布式服务框架Dubbo使用简易教程
- 一个必用的javascript框架:underscore.js - wine的思考 - ITeye技术网站
- D3D 练习小框架
- 原子级3D渲染技术(Unlimited Detail Technology )
- Silverlight + DomainService 简易框架之一 完成增删改操作
- 延迟渲染技术
- Usb设备驱动5:usb-firmware简易框架
- ADF FACES 部分页面渲染(PPR)技术
- 关于2009年个人技术框架的设想