欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

D3D渲染技术之简易框架

程序员文章站 2022-07-13 09:10:53
...

在上一篇博客中,我们介绍了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秒并且输了。 显然在这种情况下,我们不希望任何时候游戏暂停对玩家。
D3D渲染技术之简易框架
计算自关卡开始以来的时间,请注意,我们选择应用程序开始时间作为原点(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秒时,就可以引发炸弹爆炸的事件。
在这里定时器讲的比较多,它也是最重要的,也是框架的一部分,下面先把框架的结构给读者看一下:

D3D渲染技术之简易框架

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

相关标签: D3D12