D3D渲染技术之简易框架

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/jxw167/article/details/81975294

在上一篇博客中,我们介绍了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

阅读更多

扫码向博主提问

海洋_

博客专家

非学,无以致疑;非问,无以广识
  • 擅长领域:
  • 3D引擎架构
  • 服务器架构
  • GPU渲染
  • 客户端架构
  • 引擎优化
去开通我的Chat快问
换一批

没有更多推荐了,返回首页