일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 절두체 컬링
- 디퍼드 렌더링
- DirectX
- DirectX12
- 동적 색인화
- TCP/IP
- Direct3D12
- 장치 초기화
- 조명 처리
- Frustum Culling
- C++
- 게임 디자인 패턴
- 노멀 맵핑
- effective C++
- Dynamic Indexing
- 게임 클래스
- gitscm
- 큐브 매핑
- 네트워크
- 입방체 매핑
- FrameResource
- Render Target
- InputManager
- Deferred Rendering
- 게임 프로그래밍
- gitlab
- 직교 투영
- direct3d
- light
- 네트워크 게임 프로그래밍
- Today
- Total
코승호딩의 메모장
[시간 측정] 본문
컴퓨터의 성능은 하드웨어마다 다르기 때문에 게임의 물체들을 경과 시간에 기초하여 적절히 갱신하기 위해서는
애니메이션의 프레임들을 렌더링할 때는 프레임들 사이에 시간이 얼마나 흘렀는지 알아야 한다. 이를 위해 Windows.h가 제공하는 성능 타이머(성능 카운터)를 사용한다. 성능 타이머의 시간 측정 단위는 '지나간 클럭 틱들의 개수' 이다.
- QueryPerformanceCounter 함수 : 고성능 타이머 값(1us 이하)을 반환하는 함수로 성능 타이머로부터 틱 수 단위의 현재 시간을 얻을 때 사용된다.
- QueryPerformanceFrequency 함수 : 현재 성능 카운터의 빈도 즉, 성능 타이머의 주파수(초당 틱 수)를 얻을 때 사용된다.
시간 측정을 위해 필요한 함수는 이 두 개뿐이다. QueryPerformanceFrequency 함수를 통해 초당 틱 수를 얻어 이에 역수를 취해 틱당 초수를 얻는다. 이 틱당 초수에 틱 수를 곱하게 되면 초 단위 시간이 나오게 된다. 그런데 여기서 중요한 점은 애니메이션에 필요한 것은 두 측정치의 차이이다. 즉, 성능 타이머가 돌려준 값들이 아닌 두 시간 값 사이의 차이이다.
따라서 다음으로 프레임들 사이에서 시간이 얼마나 흘렀는지를 구해야 한다. 만약 i 번째 프레임을 렌더링할 때 성능 타이머 값이 t(i)이고 이전 프레임의 성능 타이머 값이 t(i-1)이라고 할 때 프레임 사이의 경과 시간 △t 는 t(i) - t(i-1)일 것이다. 게임과 같은 응용 프로그램에서 1초당 경과한 프레임의 수는 일반적으로 60프레임 즉 60fps정도여야 한다. 다음 코드는 △t를 계산하는 코드이다.
void GameTimer::Tick()
{
if( mStopped )
{
mDeltaTime = 0.0;
return;
}
__int64 currTime;
QueryPerformanceCounter((LARGE_INTEGER*)&currTime);
mCurrTime = currTime;
mDeltaTime = (mCurrTime - mPrevTime)*mSecondsPerCount;
mPrevTime = mCurrTime;
if(mDeltaTime < 0.0)
{
mDeltaTime = 0.0;
}
}
Tick 함수는 응용 프로그램의 Run 혹은 Update 메시지 루프에서 메시지가 없다면 계속 호출되는 방식으로 처리된다. 우선 QueryPerformanceCounter 함수를 사용해 현재 프레임 시간을 구한다. 그리고 이전 현재 프레임에서 이전 프레임 시간의 차이를 구한다. 만약 프로세스가 절전 모드가 되거나 엉키면 mDeltaTime의 값이 음수가 될 수 있기 때문에 음수가 되지 않도록 한다. 이렇게 △t 를 계산하여 사용하면 애니메이션이 이전 프레임으로부터 흐른 시간에 기초하여 장면을 적절히 갱신할 수 있다. 만약 △t 의 값이 크다면 그 크기 만큼 애니메이션의 이동에 곱해준다면 △t 가 크든 작든 애니메이션이 같은 결과를 낼 수 있을 것이다.
다음으로 이를 응용하여 Reset, Start, Stop 그리고 전체 시간을 구하도록 하겠다. 우선 Reset 함수는 매우 간단한데 다음과 같다.
void GameTimer::Reset()
{
__int64 currTime;
QueryPerformanceCounter((LARGE_INTEGER*)&currTime);
mBaseTime = currTime;
mPrevTime = currTime;
mStopTime = 0;
mStopped = false;
}
여기서 살펴볼 점은 mPrevTime의 값을 현재 시간으로 설정하였다는 것이다. 에니메이션의 첫 프레임에서는 이전 프레임 값이 없기 때문에 t(i-1)의 값이 없다. 따라서 Reset 함수에서는 이전 시간 값을 초기화해 주는 것이다.
전체 시간은 유용하게 사용할 수 있는 중요한 시간 측정치이다. 일시 정시된 시간을 제외하고 응용 프로그램이 시작된 후 흐른 시간이다. 전체 시간을 이용한다면 x = 10cost, y = 0, z = 10sint와 같이 전체 시간을 다음과 같은 매개변수 방정식에 넣어 준다면 xz 평면에 반지름 10인 원을 따라서 움직이게 될 것이다. 앞서 봤듯이 중요한 점은 일시 정시된 시간은 누적 시간에 포함시키지 말아야 한다는 것이다.
INT64 mBaseTime; // 응용 프로그램이 시작된 시간
INT64 mPausedTime; // 일시 정지된 시간동안 누적된 시간
INT64 mStopTime; // 타이머가 정지된 시점의 시간
전체 시간을 구하기 위해서 GameTimer 클래스에 다음과 같은 멤버 변수를 사용한다. 다음은 Stop 함수와 Start 함수이다.
void GameTimer::Stop()
{
if( !mStopped )
{
__int64 currTime;
QueryPerformanceCounter((LARGE_INTEGER*)&currTime);
mStopTime = currTime;
mStopped = true;
}
}
void GameTimer::Start()
{
__int64 startTime;
QueryPerformanceCounter((LARGE_INTEGER*)&startTime);
if( mStopped )
{
mPausedTime += (startTime - mStopTime);
mPrevTime = startTime;
mStopTime = 0;
mStopped = false;
}
}
코드를 살펴보면 Stop 함수에서는 타이머가 정지된 시점의 시간(현재 시간)을 mStopTime(정지된 시점의 시간)에 저장한다. 이후 Start 함수에서 Stop 값이 true라면 mPausedTime(정지되는 동안 누적된 시간)값에 현재 흐르고 있는 시간에서 정지된 시점의 시간을 뺄셈하여 얼마만큼 정지했는지를 넣어준다. 이후 이전 프레임을 현재 값으로 초기화해줌으로써 앞서 정의한 멤버 변수들에 값을 채워준다.
마지막으로 전체 시간을 구하는 TotalTime 함수이다. 이 함수에서는 Reset이 호출된 이후 흐른 시간에서 정지 시간을 제외한 시간을 반환한다.
float GameTimer::TotalTime()const
{
if( mStopped )
{
return (float)(((mStopTime - mPausedTime)-mBaseTime)*mSecondsPerCount);
}
else
{
return (float)(((mCurrTime-mPausedTime)-mBaseTime)*mSecondsPerCount);
}
}
만약 정지된 상태라면 정지된 시점의 시간부터 이전 누적된 정지 시간을 뺄셈하여 초당 틱당 초수를 곱하여 결과를 반환하며 정지된 상태가 아니라면 현재 흐르고 있는 시간에서 누적된 정지 시간을 뺄셈한다. 위 함수들을 적절히 사용하면 다양한 곳에 적용할 수 있을 것이다. 예를 들어 캐릭터가 동굴에 입장하여 30초 내에 동굴을 빠져 나가야만 할 때, 새로운 GameTimer 인스턴스를 생성하고, 이후 TotalTime이 30초를 넘으면 실패하는 등으로 활용할 수 있을 것이다.
이처럼 어렵지 않은 코드와 단 두개의 Windows.h의 함수로 타이머를 구현할 수 있다. 한가지 걸리는 점은 QueryPerformanceFrequency를 통해 얻은 초당 틱수는 프레임마다 항상 똑같은 것은 아니라는 것이다. 책의 내용과 예제 코드에서는 타이머의 틱당 초수에 해당하는 mSecondsPerCount 변수를 생성자에서 값을 만들어 이후에도 계속 사용하는데 CPU 클럭 속도는 시스템의 부하에 따라 변할 수 있어 만약 시스템이 CPU를 많이 사용하고 있는 작업을 수행한다면 CPU이 클럭 속도가 낮아져 얻은 초당 틱수도 낮아질 것이다. 그렇다면 틱당 초수도 Update에서 계속 초기화를 해줘야 하는 것이 아닌지 의문이다. 혹시라도 틀린점이 있거나 Update를 해주는 것이 맞다면 피드백 주시면 감사드리겠습니다.
'DirectX12 > DirectX12 입문' 카테고리의 다른 글
[Frame Resource와 Render Item] (0) | 2023.09.19 |
---|---|
[Direct3D 렌더링] (0) | 2023.09.18 |
[전체 화면 모드 전환] (0) | 2023.09.11 |
[Direct3D 개요와 초기화] (0) | 2023.09.11 |