天天看點

DirectX11 計時和動畫

計時和動畫

要正确實作動畫效果,我們就必須記錄時間,尤其是要精确測量動畫幀之間的時間間隔。當幀速率高時,幀之間的時間間隔就會很短;是以,我們需要一個高精确度計時器。

1. 性能計時器

我們使用性能計時器(或性能計數器)來實作精确的時間測量。為了使用用于查詢性能計時器的Win32函數,我們必須在代碼中添加包含語句“#include

__int64 currTime;
QueryPerformanceCounter((LARGE_INTEGER*)&currTime);
           

注意,該函數通過它的參數傳回目前時間值,該參數是一個64位整數。我們使用QueryPerformanceFrequency函數來擷取性能計時器的頻率(每秒的計數次數):

__int64 countsPerSec;
QueryPerformanceFrequency((LARGE_INTEGER*)&countsPerSec);
           

而每次計數的時間長度等于頻率的倒數(這個值很小,它隻是百分之幾秒或者千分之幾秒):

這樣,要把一個時間讀數valueInCounts轉換為秒,我們隻需要将它乘以轉換因子 mSecondsPerCount:

由QueryPerformanceCounter函數傳回的值本身不是非常有用。我們使用QueryPerformanceCounter函數的主要目的是為了擷取兩次調用之間的時間差——在執行一段代碼之前記下目前時間,在該段代碼結束之後再擷取一次目前時間,然後計算兩者之間的內插補點。也就是,我們總是檢視兩個時間戳之間的相對差,而不是由性能計數器傳回的實際值。下面的代碼更好地說明了這一概念:

__int64 A = ;
QueryPerformanceCounter((LARGE_INTEGER*)&A);
/* Do work */
__int64 B = ;
QueryPerformanceCounter((LARGE_INTEGER*)&B);
           

這樣我們就可以知道執行這段代碼所要花費的計數時間為(B−A),或者以秒表示的時間為(B−A)*mSecondsPerCount。

注意:MSDN指出當使用QueryPerformanceCounter函數時,有以下注意事項:“在多處理器計算機中,任何一個處理器單獨調用該函數都不會出現問題。但是,由于基礎輸入/輸出系統(BIOS)或硬體抽象層(HAL)存在技術瓶頸,是以你在不同的處理器上調用該函數會得到不同的結果”。你可以使用SetThreadAffinityMask函數讓主應用程式線程隻運作在一個處理器上,不在處理器之間進行切換。

2. 遊戲計時器類

在下面的兩節中,我們将讨論GameTimer類的實作。

class GameTimer
{
public:
    GameTimer();

    float TotalTime()const;  // 機關為秒
    float DeltaTime()const; // 機關為秒

    void Reset(); // 消息循環前調用
    void Start(); // 取消暫停時調用
    void Stop();  // 暫停時調用
    void Tick();  // 每幀調用

private:
    double mSecondsPerCount;
    double mDeltaTime;

    __int64 mBaseTime;
    __int64 mPausedTime;
    __int64 mStopTime;
    __int64 mPrevTime;
    __int64 mCurrTime;

    bool mStopped;
};
           

需要特别注意的是,構造函數查詢了性能計數器的頻率。

GameTimer::GameTimer()
: mSecondsPerCount(), mDeltaTime(-), mBaseTime(),
  mPausedTime(), mPrevTime(), mCurrTime(), mStopped(false)
{
    __int64 countsPerSec;
    QueryPerformanceFrequency((LARGE_INTEGER*)&countsPerSec);
    mSecondsPerCount =  / (double)countsPerSec;
}
           

3. 幀之間的時間間隔

當渲染動畫幀時,我們必須知道幀之間的時間間隔,以使我們根據逝去的時間長度來更新遊戲中的物體。我們可以采用以下步驟來計算幀之間的時間間隔:設ti為第i幀時性能計數器傳回的時間值,設ti-1為前一幀時性能計數器傳回的時間值,那麼兩幀之間的時間差為Δt = ti – ti-1。對于實時渲染來說,我們至少要達到每秒30幀的頻率才能得到比較平滑的動畫效果(我們一般可以達到更高的頻率);是以,Δt = ti – ti-1通常是一個非常小的值。

下面的代碼示範了Δt的計算過程:

void GameTimer::Tick()
{
    if( mStopped )
    {
        mDeltaTime = ;
        return;
    }

    __int64 currTime;
    QueryPerformanceCounter((LARGE_INTEGER*)&currTime);
    mCurrTime = currTime;

    // 目前幀和上一幀之間的時間差
    mDeltaTime = (mCurrTime - mPrevTime)*mSecondsPerCount;

    // 為計算下一幀做準備
    mPrevTime = mCurrTime;

    // 確定不為負值。DXSDK中的CDXUTTimer提到:如果處理器進入了節電模式
    // 或切換到另一個處理器,mDeltaTime會變為負值。
    if(mDeltaTime < )
    {
        mDeltaTime = ;
    }
}

float GameTimer::getDeltaTime() const
{
    return (float)mDeltaTime;
}
           

函數Tick在應用程式消息循環中的調用如下:

int D3DApp::Run()
{
    MSG msg = {};

    mTimer.Reset();

    while(msg.message != WM_QUIT)
    {
        // 如果接收到Window消息,則處理這些消息
        if(PeekMessage( &msg, , , , PM_REMOVE ))
        {
            TranslateMessage( &msg );
            DispatchMessage( &msg );
        }
        // 否則,則運作動畫/遊戲
        else
        {  
            mTimer.Tick();

            if( !mAppPaused )
            {
                CalculateFrameStats();
                UpdateScene(mTimer.DeltaTime());   
                DrawScene();
            }
            else
            {
                Sleep();
            }
        }
    }

    return (int)msg.wParam;
}
           

通過這一方式,每幀都會計算出一個Δt并将它傳送給UpdateScene方法,根據目前幀與前一幀之間的時間間隔來更新場景。下面是Reset方法的實作代碼:

void GameTimer::Reset()
{
    __int64 currTime;
    QueryPerformanceCounter((LARGE_INTEGER*)&currTime);

    mBaseTime = currTime;
    mPrevTime = currTime;
    mStopTime = ;
    mStopped  = false;
}
           

我們可以看到,當調用Reset方法時,mPrevTime被初始化為目前時間。這一點非常重要,因為對于動畫的第一幀來說,沒有前面的那一幀,也就是說沒有前面的時間戳。是以個值必須在消息循環開始之前初始化。

4. 遊戲時間

另一個需要測量的時間是從應用程式開始運作時起經過的時間總量,其中不包括暫停時間;我們将這一時間稱為遊戲時間(game time)。下面的情景說明了遊戲時間的用途。假設玩家有300秒的時間來完成一個關卡。當關卡開始時,我們會擷取時間tstart,它是從應用程式開始運作時起經過的時間總量。當關卡開始後,我們不斷地将tstart與總時間t進行比較。如果t – tstart >300(如圖4.8所示),就說明玩家在關卡中的用時超過了300秒,輸掉了這一關。很明顯,在一情景中我們不希望計算遊戲的暫停時間。

DirectX11 計時和動畫

(計算從關卡開始時起的時間。注意,我們将應用程式的開始時間作為原點(0),測量相對于這個時間原點的時間值。)

遊戲時間的另一個用途是通過時間函數來驅動動畫運作。例如,我們希望一個燈光在時間函數的驅動下環繞着場景中的一個圓形軌道運動。燈光位置可由以下參數方程描述:

x = 10 cost

y = 20

z = 10 sint

這裡t表示時間,随着t(時間)的增加,燈光的位置會發生改變,使燈光在平面y = 20上圍繞着半徑為10的圓形軌道運動。對于這種類型的動畫,我們也不希望計算遊戲的暫停時間。

DirectX11 計時和動畫

(如果我們在t1時暫停,在t2時取消暫停,并計算暫停時間,那麼當我們取消暫停時,燈光的位置會從p(t1) 突然跳到p(t2)。)

我們使用以下變量來實作遊戲計時:

__int64 mBaseTime;
__int64 mPausedTime;
__int64 mStopTime;
           

當調用Reset方法時,mBaseTime會被初始化為目前時間。我們可以把它視為從應用程式開始運作時起經過的時間總量。在多數情況下,你隻會在消息循環開始之前調用一次Reset,之後不會再調用個方法,因為mBaseTime在應用程式的整個運作周期中保持不變。變量mPausedTime用于累計遊戲的暫停時間。我們必須累計這一時間,以使我們從總的運作時間中減去暫停時間。當計時器停止時(或者說,當暫停時),mStopTime會幫我們記錄暫停時間。

GameTimer類包含兩個重要的方法Stop和Start,它們分别在應用程式暫停和取消暫停時調用,讓GameTimer記錄暫停時間。代碼中的注釋解釋了這兩個方法的實作思路。

void GameTimer::Stop()
{
    // 如果正處在暫停狀态,則略過下面的操作
    if( !mStopped )
    {
        __int64 currTime;
        QueryPerformanceCounter((LARGE_INTEGER*)&currTime);

        // 記錄暫停的時間,并設定表示暫停狀态的标志
        mStopTime = currTime;
        mStopped  = true;
    }
}

void GameTimer::Start()
{
    __int64 startTime;
    QueryPerformanceCounter((LARGE_INTEGER*)&startTime);


    // 累加暫停與開始之間流逝的時間
    //
    //                     |<-------d------->|
    // ----*---------------*-----------------*------------> time
    //  mBaseTime       mStopTime        startTime    

    // 如果仍處在暫停狀态
    if( mStopped )
    {
        // 則累加暫停時間
        mPausedTime += (startTime - mStopTime);
        // 因為我們重新開始計時,是以mPrevTime的值就不正确了,
        // 要将它重置為目前時間
        mPrevTime = startTime;
        // 取消暫停狀态
        mStopTime = ;     
        mStopped  = false;
    }
}
           

最後,成員函數TotalTime傳回了自調用Reset之後經過的時間總量,其中不包括暫停時間。它的代碼實作如下:

// 傳回自調用Reset()方法之後的總時間,不包含暫停時間
float GameTimer::TotalTime()const
{
    // 如果處在暫停狀态,則無需包含自暫停開始之後的時間。
    // 此外,如果我們之前已經有過暫停,則mStopTime - mBaseTime會包含暫停時間, 我們不想包含這個暫停時間,
    // 是以還要減去暫停時間: 
    //
    //                     |<--paused time-->|
    // ----*---------------*-----------------*------------*------------*------> time
    //  mBaseTime       mStopTime        startTime     mStopTime    mCurrTime

    if( mStopped )
    {
        return (float)(((mStopTime - mPausedTime)-mBaseTime)*mSecondsPerCount);
    }

    // mCurrTime - mBaseTime包含暫停時間,而我們不想包含暫停時間,
    // 是以我們從mCurrTime需要減去mPausedTime:
    //
    //  (mCurrTime - mPausedTime) - mBaseTime
    //
    //                     |<--paused time-->|
    // ----*---------------*-----------------*------------*------> time
    //  mBaseTime       mStopTime        startTime     mCurrTime

    else
    {
        return (float)(((mCurrTime-mPausedTime)-mBaseTime)*mSecondsPerCount);
    }
}
           

注意:我們的示範架構建立了一個GameTimer執行個體用于計算應用程式開始後的總時間和兩幀之間的時間;你也可以建立額外的執行個體作為通用的秒表使用。例如,當點着一個炸彈時,你可以啟動一個新的GameTimer,當TotalTime達到5秒時,你可以引發一個事件讓炸彈爆炸。

5. 程式示例Demo完整項目源代碼下載下傳

http://download.csdn.net/detail/sinat_24229853/9144309

繼續閱讀