4.4 시간 측정과 애니메이션
애니메이션을 정확히 수행하려면 시간을 측정해야 한다. 애니메이션의 인접한 두 프레임 사이에 흐른 시간의 양을 측정할 수 있어야 한다.
4.4.1 성능 타이머
정밀한 시간 측정을 위해, Windows가 제공하는 성능 타이머를 사용한다. 성능 타이머의 시간 측정 단위는 '지나간 클럭 틱 들의 개수'이다. 성능 타이머로부터 틱 수 단위의 현재 시간을 얻을 때에는 다음과 같이 QueryPerformanceCounter 함수를 사용한다.
__int64 currTime;
QueryPerformanceCounter((LARGE_INTEGER*)&currTime);
초 단위 시간을 얻으려면, 우선 QueryPerformanceFrequency 함수를 이용해서 성능 타이머의 주파수(초당 틱 수)를 알아내야 한다.
__int64 countsPerSec;
QueryPerformanceFrequency((LARGE_INTEGER*)&countsPerSec);
다음으로, 주파수의 역수를 취해 틱당 초 수를 얻는다.
mSecondsPerCount = 1.0 / (double)countsPerSec;
이제 틱당 초 수에 틱 수를 곱하면 초 단위 시간이 나온다.
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 초이다.
4.4.2 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::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;
}
4.4.3 프레임 간 경과 시간
애니메이션의 프레임들을 렌더링할 때에는 프레임들 사이에서 시간이 얼마나 흘렀는지 알아야 한다. 그래야 게임의 물체들을 경과 시간에 기초해서 적절히 갱신할 수 있다. 두 프레임 사이의 경과 시간은 현재 프레임을 렌더링 할 때 측정한 타이머 값에 이전 프레임에서의 성능 타이머 값의 차이다.
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;
}
응용 프로그램은 프레임마다 △t를 계산해서 UpdateScene에 넘겨준다. 이에 의해 응용 프로그램은 애니메이션의 이전 프레임으로부터 흐른 시간에 기초해서 장면을 적절히 갱신할 수 있게 된다. 이 예에 나오는 Reset 메서드의 구현은 다음과 같다.
void GameTimer::Reset()
{
__int64 currTime;
QueryPerformanceCounter((LARGE_INTEGER*)&currTime);
mBaseTime = currTime;
mPrevTime = currTime;
mStopTime = 0;
mStopped = false;
}
주목할 것은 Reset 메서드가 mPrevTime을 현재 시간으로 설정한다는 점이다. 애니메이션의 첫 프레임에서는 이전 프레임이라는 것이 없으므로 이전 시간 값도 없다. 따라서 메시지 루프를 시작하기 전에 이처럼 Reset 메서드 안에서 이전 시간 값을 초기화해 주어야 한다.
4.4.4 전체 시간
응용 프로그램이 시작된 이후에 흐른 시간이 전체 시간이다. 일시 정지된 시간은 제외한다. 예를 들어 게임의 시간 제한을 둘 때도 사용할 수 있다.
전체 시간은 어떤 수량을 시간의 함수로서 애니메이션할 때에도 유용하다. 예를 들어 장면의 주변을 도는 광원의 운동을 시간의 함수로 표현한다고 하자. 그런 경우 광원의 위치를 다음과 같은 매개변수 방정식으로 서술할 수 있다.
이러면 y = 20 평면에서 반지름이 10인 원을 따라 광원이 움직이게 된다.
전체 시간을 구현하기 위해 GameTime 클래스는 다음과 같은 멤버 변수들을 제공한다.
__int64 mBaseTime;
__int64 mPausedTime;
__int64 mStopTime;
mBaseTime은 Reset이 호출될 때 현재 시간으로 초기화된다. 그 시간을 응용 프로그램이 시작한 시간으로 간주할 수 있다. 대부분의 경우 Reset은 메시지 루프로 진입하기 전에 한 번만 호출된다. 그런 경우 응용 프로그램의 수명동안 변하지 않는다.
mPauseTime은 타이머가 일시 정지된 동안 계속해서 누적된다. 유효한 전체 시간을 구하려면 실제로 흐른 전체 시간에서 누적된 일시 정지 시간은 빼야 하므로, 누적 해 둘 필요가 있다. mStopTime은 타이머가 정지된 시점의 시간이다.
GameTime의 중요한 두 메서드로 Stop과 Start가 있다. 타이머를 일시 정지하거나 재개할 때 호출해야 한다.
void GameTimer::Stop()
{
// If we are already stopped, then don’t do anything.
if( !mStopped )
{
__int64 currTime;
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------->|
// ---------------*-----------------*------------> 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 = false;
}
}
마지막으로 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
// |<----------->|
// ---*------------*-------------*-------*-----------*------> 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
//
// |<--paused time-->|
// ----*---------------*-----------------*------------*------> time
// mBaseTime mStopTime startTime mCurrTime
else
{
return (float)(((mCurrTime-mPausedTime)-
mBaseTime)*mSecondsPerCount);
}
}
'그래픽스 > DirectX 12' 카테고리의 다른 글
[DirectX 12 3D게임 입문] Chapter 5.6: 렌더링 파이프라인 - 정점 셰이더 단계 (1) | 2023.11.13 |
---|---|
[DirectX 12 3D게임 입문] Chapter 5.5: 렌더링 파이프라인 - 입력 조립기 단계 (1) | 2023.11.11 |
[DirectX 12 3D게임 입문] Chapter 4.3: Direct3D의 초기화 - Direct3D의 초기화 (0) | 2023.11.07 |
[DirectX 12 3D게임 입문] Chapter 4.2: Direct3D의 초기화 - CPU와 GPU의 상호작용 (1) | 2023.11.06 |
[DirectX 12 3D게임 입문] Chapter 4.1: Direct3D의 초기화 - 기본지식 (0) | 2023.11.06 |