정말 오랜만의 포스팅입니다~!


이번 포스트에선 프레임이 무엇인지와 프레임을 계산하는 방법에 대해 소개를 해볼까 합니다.


먼저, 게임이나 동영상 등에서의 프레임(Frame)은 "정지된 화면"을 말합니다.

더 자세하게 설명드리면 동영상은 여러 개의 정지된 화면을 연속적으로 표시함으로써 사용자에게 화면이 움직인다는 착각을 주게 됩니다.

게임도 마찬가지입니다. 매 순간마다 화면을 그리고 사용자에게 표시해주는데 이 과정을 반복적으로 수행함으로써 사용자에게 화면이 움직이고 있다는 착각을 주는 것이지요.


이제 프레임이 뭔지 알았습니다. 게임이나 동영상에서 자주 보이는 fps는 무엇일까요?

fps는 Frame per second의 약자로서 초당 프레임이 몇 번 표시되는지를 나타내는 단위입니다.


이 포스트에서 사용하는 계산 방법은 초당 호출 횟수를 계산합니다. 따라서, 프레임 계산 목적에만 사용되는 것이 아니라 다른 용도로도 사용될 수 있습니다!



흔히 검색을 통해 가장 쉽게 접하게 되는 fps 계산 방법은 값을 누적시키고 매 초마다 fps 값을 갱신해주는 방식입니다.

초당 연산 횟수를 계산하는 코드 (C#)
int currentTick, previousTick;
int calculation = 0;

// 시작 전에 초기값을 담아주기 위함
previousTick = Environment.TickCount;
while (true) {
    // 현재 틱을 가져오고
    currentTick = Environment.TickCount;

    // 현재의 틱 값이 이전 틱 값 + 1000보다 클 경우
    // 즉, 마지막 갱신 시간으로부터 1초가 지난 경우
    if (currentTick >= previousTick + 1000) {
        // 이전 시간을 현재 시간으로 설정하고 
        // 계산량을 구한다.
        previousTick = currentTick;
        Console.WriteLine("Calculation per second (Calc/s): {0:N0}", calculation);
        calculation = 0;
    }
    // 계산 수를 1 증가시킨다.
    calculation++;
}
예제 코드 (.NET Fiddle)


위 코드는 "프레임"이 "연산"으로만 바뀌었을 뿐이지 동작 원리는 같습니다.

게임에 적용한다면 계산 수를 1 증가시키는 코드 전후로 렌더링 코드를 넣어주면 되겠죠?


위 코드의 실행 결과는 아래처럼 나타납니다. 초당 연산 횟수는 1초마다 콘솔 화면에 출력됩니다.




또 다른 방법은 초당 결과를 보여주는 것이 아니라 이전 작업으로부터 현재 작업이 완료되기 까지의 시간 차를 이용하여 연산 횟수를 계산하는 방법입니다.

.NET 프레임워크에 기본으로 포함되어 있는 Environment.TickCount 속성을 사용하여 초당 연산 횟수를 구하려고 했더니 정밀도가 많이 떨어져서 정확한 계산이 불가능했습니다. 따라서 내부에서 정밀도 높은 타이머를 사용하는 System.Diagnostics.Stopwatch 클래스를 사용하였습니다.

초당 연산 횟수를 계산하는 코드 (고정밀도) (C#)
// 표시 용도로 사용
Console.Write("Calculation per second (Calc/s): ");
int curLeft = Console.CursorLeft, curTop = Console.CursorTop;

// 최대로 샘플링될 수 있는 틱의 갯수를 정해놓는다.
// 100개의 샘플 틱이 채워지면 이후엔 가장 먼저 들어왔던 틱을 제거하고 
// 현재 틱을 추가하여 계속 정해진 샘플 갯수 내에서 평균을 계산하게 된다.
int MaximumSamplingTickCount = 10000;
Queue<long> sampledTicks = new Queue<long>(MaximumSamplingTickCount);

// 샘플링된 틱의 합계를 저장할 변수
long sampleTotal = 0;

// 현재 틱과 이전 틱을 저장할 변수
long currentTick, previousTick;

// 고정밀 타이머를 사용하기 위한 Stopwatch 클래스를 초기화하고
// 타이머를 시작한다.
Stopwatch sw = new Stopwatch();
sw.Start();

// 시작 전에 초기값을 담아주기 위함
previousTick = sw.ElapsedTicks;

while (true) {
    // 현재 틱을 가져오고
    currentTick = sw.ElapsedTicks;

    // 델타값을 구한다.
    long deltaTick = currentTick - previousTick;

    // 샘플 큐의 갯수를 확인하고 MaximumSamplingTickCount개 이상이면
    // 하나를 지우면서 합계 값에서도 뺀다.
    if (sampledTicks.Count >= MaximumSamplingTickCount) sampleTotal -= sampledTicks.Dequeue();

    // 샘플 큐에 델타값을 추가하고
    // 합계 값에도 더한다.
    sampledTicks.Enqueue(deltaTick);
    sampleTotal += deltaTick;

    // 샘플 큐에 포함된 값의 평균을 계산하고
    // 초당 연산 횟수를 계산한다.
    float average = (float) sampleTotal / sampledTicks.Count;
    float cps = Stopwatch.Frequency / average;
    
    Console.SetCursorPosition(curLeft, curTop);
    Console.WriteLine("{0:N2}     ", cps);

    // 시간을 업데이트한다.
    previousTick = currentTick;
}
예제 코드 (.NET Fiddle)


* 이 예제는 .NET Fiddle에서 실행이 안되기 때문에 개인 컴퓨터에서 실행하셔야 합니다. 양해 부탁드립니다.


이 코드는 샘플링되는 틱 수가 많으면 많을수록 초당 연산 횟수가 더욱 안정되게 나옵니다. (갑작스런 값의 변화가 적어짐) 하지만 샘플링할 틱 수가 많아지면 많아질수록 필요로하는 메모리 공간 또한 증가됩니다.

게임에서의 경우 렌더링 작업 자체가 단순 연산이 아닌 고비용의 작업이므로 연산 횟수의 값이 높아봐야 1~2천대정도로 낮게 나옵니다. 따라서 이 경우엔 샘플링할 틱 수의 값을 위 예제처럼 1만정도로 설정할 필요 없이 100~300정도로도 충분하게 됩니다.

* 수직 동기화를 켜서 초당 프레임 수를 모니터의 주사율에 맞출 경우 해당 모니터의 주사율 ± 20 정도로 설정하는 것도 좋습니다.


그리고, 위 코드의 실행 결과는 아래 그림처럼 나타납니다. 초당 연산 횟수는 매 반복마다 갱신됩니다.




여기까지입니다!

게임에서 자주 보이는 fps는 어떻게 계산하는지, 그리고 fps에서 f(frame, 프레임)는 무슨 의미인지에 대해 짧게나마 작성해보았습니다.


긴 글 읽어주셔서 감사합니다.

+ Recent posts