Notice
Recent Posts
Recent Comments
Link
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Archives
Today
Total
관리 메뉴

codingfarm

4. 시간 측정과 애니메이션 본문

computer graphics/DX12 book

4. 시간 측정과 애니메이션

scarecrow1992 2021. 2. 7. 20:23

 정확한 애니메이션의 수행을 위해선 인접한 두 프레임 사이에 흐른 시간의 양을 측정할 수 있어야 한다.

프레임률이 높은 경우 프레임간 경과 시간이 상당히 짧으므로, 정밀도가 높은 타이머를 사용할 필요가 있다.

 

1. 성능타이머

이 책의 예제들은 Windows가 제공하는 성능 타이머(performance timer)를 사용한다.

이를 성능 카운터(performance counter) 라고도 부른다.

성능 타이머를 조회하는 Win32 함수를 사용하려면 반드시 Windows.h를 포함시켜야 한다.

 

성능 타이머의 시간 측정 단위는 '지나간 클릭 틱(tick)들의 갯수(count)' 즉, 틱수 이다.

성능 타이머로부터 틱 수 단위의 현재 시간을 얻을 때에는 QueryPerformanceCounter 함수를 사용한다.

1
2
3
BOOL QueryPerformanceCounter(
  LARGE_INTEGER *lpPerformanceCount
);
cs

사용법은 아래와 같다.

1
2
__int64 currTime;
QueryPerformanceCounter((LARGE_INTEGER*)&currTime);
cs

이 함수는 매개변수를 통해서 현재 시간 값을 돌려줌에 주목하라.

함수가 돌려주는 현재 시간 값은 64비트 정수이며, 이 값은 지나간 클럭 틱(tick) 들의 갯수(count)이다.

 

 

QueryPerformanceFrequency함수를 이용하면  1초당 틱수를 반환하게된다.

만약 1틱당 초수를 얻고싶다면 아래처럼 역수를 취하면 된다.

1
2
3
__int64 countsPerSec;
QueryPerformanceFrequency((LARGE_INTEGER*)&countsPerSec);
mSecondsPerCount = 1.0 / (double)countsPerSec;
cs

이제 틱당 초 수 mSecondsPerCount에 틱 수 valueInCounts를 곱하면 초 단위 시간. 즉, 틱이 valueInCounts번 발생하는 동안 흐른시간이 나온다.

1
valueInSecs = valueInCounts * mSecondsPerCount;
cs

 

하지만 이 틱수는 지극히 상대적인 수치이기 때문에 애니메이션에 필요한 것은 두 측정치의 차이 이다.

즉 한번의 QueryPerformanceCounter 호출로 얻은 값을 그 다음번 QueryPerformanceCounter 호출로 얻은 값에서 뺀 결과이다.

1
2
3
4
5
6
__int64 A = 0, B = 0;
QueryPerformanceCounter((LARGE_INTEGER*)&A);
 
//작업 수행
 
QueryPerformanceCounter((LARGE_INTEGER*)&B);
cs

위의 경우 '어떤 작업'에 걸린 틱의 갯수는 (B-A)개, 이며 걸린 시간은 (B-A) * mSecondsPerCount 초 이다.

 

MSDN에는 QueryPerformanceCounter에 대한 주의 사항이 있다.

docs.microsoft.com/ko-kr/windows/win32/sysinfo/acquiring-high-resolution-time-stamps?redirectedfrom=MSDN

다중 프로세서 컴퓨터의 경우 이 함수가 어떤 프로세서에서 실행되는지에 다라 결과가 달라져서는 안 된다. 그러나 기본 입출력 시스템(BIOS) 또는 하드웨어 추상층(HAL)의 버그 때문에 프로세서에 따라 다른 결과가 나올 수 있ㄷ. SetThreadAffinityMask 함수를 적절히 이용하면 응용 프로그램의 주 스레드가 다른 프로세서로 전환되는 일을 방지할 수 있다.

 


2. GameTimer 클래스

이번절과 다음 두절에서는 GameTimer 클래스의 구현을 논의한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class GameTimer {
public:
    GameTimer();
 
    float TotalTime()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;
};
cs

mSecondsPerCount : 주기 (틱 한번 발생시 경과 시간)

mDeltaTime : 두 프레임 사이의 경과 시간.

 

1
2
3
4
5
6
7
8
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;
}
cs

생성자의 주된 임무는 성능 타이머의 주파수를 조회해서 틱당 초 수를 설정하는 것이다.

 


3. 프레임 간 경과 시간

애니메이션의 프레임들을 렌더링할 때에는 프레임들 사이에서 시간이 얼마나 흘렀는지 알아야 한다.

실시간 렌더링을 위해서는 프레임률(framerate : 초당 프레임 수)이 적어도 30은 넘어야 한다.

두 프레임 사이의 경과 시간 $\Delta t$은 아래처럼 구한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void GameTimer::Tick() {
    if( mStopped )     {
        mDeltaTime = 0.0;
        return;
    }
 
    __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;
}
cs

 

1
2
3
float GameTimer::DeltaTime() const {
    return (float)mDeltaTime;
}
cs

 

mCurrTime - mPrevTime을 통해 이전의 Tick함수와 이번 Tick함수 호출사이의 틱의 갯수를 구할 수 있으며 mSecondsPerCount(1틱당 초수)를 곱함으로써 Tick함수 호출 사이의 경과 시간을 구하게 된다. 이 값이 두 프레임 사이의 경과시간(mDeltaTime)이 된다.

응용 프로그램은 Tick 메서드를 아래의 방식으로 호출한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
int D3DApp::Run() {
    MSG msg = {0};
 
    mTimer.Reset();
 
    while(msg.message != WM_QUIT) {
        // If there are Window messages then process them.
        if(PeekMessage( &msg, 000, 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;
}
cs

프레임마다 $\Delta t$를 계산해서 UpdateScene에 넘겨준다. 이에 의해 응용 프로그램은 애니메이션의 이전 프레임으로부터 흐른 시간에 기초해서 장면을 적절히 갱신할 수 있게 된다.

 

Reset 메서드는 아래와 같다.

1
2
3
4
5
6
7
8
9
void GameTimer::Reset() {
    __int64 currTime;
    QueryPerformanceCounter((LARGE_INTEGER*)&currTime);
 
    mBaseTime = currTime;
    mPrevTime = currTime;
    mStopTime = 0;
    mStopped  = false;
}
cs

mBaseTime과 mPrevTime을 같은값으로 초기화 하였는데

최초 Tick 호출시에는 mPrevTime 값이 비어있으므로 엉뚱한 결과가 나오면 안되기에 같은값을 집어넣었다.

 

 


4. 전체 시간(Total Time)

전체 시간이란 응용 프로그램이 시작된 이후에 흐른 시간이다.

이는 게임 제한시간, 스킬 쿨타임 등에 즐겨쓰이며 특히 시간의 함수로 애니메이션 할때에도 유용하다.

$$\begin{cases}
x = 10 \cos t \\
y = 20 \\
z = 10 \sin t
\end{cases}$$

가련 시간의 흐름에 따른 광원의 위치를 관리하고자 할 때 $t$를 게임 경과 시간이라 하면

단순히 위 식에 $t$값을 대입하는것으로 광원의 위치를 쉽게 알 수 있다.

전체시간을 구하기 위해 gameTime클래스는 아래와 같은 멤버 변수들을 사용한다.

1
2
3
__int64 mBaseTime;
__int64 mPausedTime;
__int64 mStopTime;
cs

mBaseTime은 Reset이 호출될 때 현재 시간으로 초기화된다. 그리고 그 시간을 기준으로 응용 프로그램이 시작한 시간으로 간주할 수 있다. Reset은 메시지 루프로 진입하기 전에 한번만 호출되므로, mBaseTime은 응용 프로그램의 수명과 비슷한 의미를 지닌다 볼 수 있다.

mPausedTime은 타이머가 일시정지된 동안 계속해서 누적된다. 그러므로 유효한 전체 시간을 구하려면 mBaseTime에 mPausedTime을 빼야 한다.

mStoptime은 타이머가 정지된 시점의 시각으로, 일시 정지 누적 시간을 계산하는 데 쓰인다. 나중에 정지가 풀리면 현재 시간에 이값을 뺀 값이 정지된 시간 간격이 되며, 해당 시간차를 mPuasedTime에 더하여 총 정지시간을 갱신한다. 

 

응용 프로그램은 Stop과 Start 메서드를 이용하여 타이머를 일시 정지하거나 재개한다. 그래야 GameTimer가 누적 시간을 적절히 갱신할 수 있다. 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void GameTimer::Stop() {
    if!mStopped )    {
        __int64 currTime;
        QueryPerformanceCounter((LARGE_INTEGER*)&currTime);
        mStopTime = currTime;
        mStopped  = true;
    }
}
 
 
void GameTimer::Start() {
    __int64 startTime;
    QueryPerformanceCounter((LARGE_INTEGER*)&startTime);
 
 
    // Accumulate the time elapsed between stop and start pairs.
    //
    //                     |<-------d------->|
    // ----*---------------*-----------------*------------> time
    //  mBaseTime       mStopTime        startTime     
 
    if( mStopped )    {
        mPausedTime += (startTime - mStopTime);    
 
        mPrevTime = startTime;
        mStopTime = 0;
        mStopped  = false;
    }
}
cs

Stop이 호출되면서 mStoptime 에 currTime을 저장 하고

Start가 호출되면서 startTime에 mStopTime을 뺀 값 즉, 정지된 시간을 mPausedTime에 더한다.

그리고 이전 프레임 시간 mPrevTime을 이번 프레임의 시간으로 설정한다.

 

TotalTime 멤버 함수는 Reset이 호출된 이후 흐른시간(mCurrTime)에 일시정지된 시간(mPausedTime)을 뺀 값을 돌려준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Returns the total time elapsed since Reset() was called, NOT counting any
// time when the clock is stopped.
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:  
    //
    //                     |<--paused time-->|
    // ----*---------------*-----------------*------------*------------*------> time
    //  mBaseTime       mStopTime        startTime     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 
    //
    //                     |<--paused time-->|
    // ----*---------------*-----------------*------------*------> time
    //  mBaseTime       mStopTime        startTime     mCurrTime
 
    else
        return (float)(((mCurrTime - mPausedTime) - mBaseTime) * mSecondsPerCount);
}
cs

 

 

Comments