[Web Sculpting] Delta Time

JINBOK LEE·2023년 7월 29일
1

Web Sculpting

목록 보기
1/4
post-thumbnail

웹 상에서 애니메이션을 구현하기 위해서는 다양한 방법이 있지만, 그 중에서도 requestAnimationFrame(callback) 라는 Javascript 메서드를 이용하면서 Delta Time의 개념과 사용 목적에 대해 알게 되었다.

본 글을 통해 이에 대한 내용을 차근차근 서술해보고자 한다.


1. Delta Time이란 ?

Delta Time이란, 간단히 말하면 '프레임간의 시간차' 라고 할 수 있다.

매 프레임마다 호출되는 requestAnimationFrame(callback) 메서드 콜백함수 입장에서 본다면, 콜백 함수가 호출되는 시간의 차이라고 볼 수 있다.

requestAnimationFrame Method ?

requestAnimationFrame(callback)

브라우저는 이 메서드를 이용하여 1프레임마다 지정된 콜백 함수를 호출한다

아래 이미지는 각 주사율에 맞춰 requestAnimationFrame(callback) 메서드의 콜백함수가 호출되는 다이어그램이다.

DeltaTime 이미지 출처 : Mastering Delta Time to Ensure Consistent Frame Rates! / KIRUPA

60Hz 환경에서는 16.667ms, 120Hz 환경에서는 8.333ms의 delta Time이 발생한다.

이제 각 디스플레이의 주사율에 따라 서로 다른 delta Time을 가진다는 것을 알게 되었다.


그런데, 이게 왜 필요할까?

delta time을 왜 알고 있어야 하고, 왜 활용하는지에 대해 알아보자.


2. 디스플레이에 따라 달라지는 애니메이션

일반적으로 많이 사용하는 디스플레이의 주사율은 60Hz 이고, 게이밍, 영상 작업 등에 사용되는 디스플레이는 120~144Hz 정도를 많이 사용한다. (360Hz 고주사율 디스플레이도 있다고 한다)

이는 달리 말하자면, 사용자의 디스플레이 주사율 환경에 따라 requestAnimationFrame(callback) 메서드가 콜백 함수를 호출하는 횟수가 달라진다는 의미이다.

예를 들어, requestAnimationFrame(callback) 이 호출되는 시점인 매 프레임마다 1px씩 원을 움직이는 애니메이션이 있다고 가정하자.

motionDiagram

위 다이어그램과 같이,
60hz의 디스플레이에서는 1초간 60px을 이동할 것 이고,
144hz의 디스플레이에서는 1초간 144px을 이동할 것 이다.

  • 사용자마다 디스플레이의 주사율이 다르거나
  • 브라우저가 다른 작업으로 바빠서 repaint 작업이 후순위로 밀려나거나
  • 브라우저가 최소화 되거나, 탭 변경으로 인해 포커스를 잃은 상태가 되거나
  • PC,Laptop 등의 기기가 배터리 부족 등으로 성능을 제한 한다거나
  • Safari 브라우저와 같이 높은 프레임 레이트를 지원하지 않는 브라우저를 사용한다거나

위와 같은 여러 요인들로 인해 나의 애니메이션이 의도와는 다르게 동작할 수 있다는 것이다.


3. 모든 디스플레이에서 동일한 애니메이션을 구현하려면?

이를 달성하기 위해서는 크게 2가지 방식이 있다.

1. 최대 FPS를 제한하는 방식

아래 코드는 최대 FPS를 제한하여 구현한 애니메이션 코드이다.

const canvas = document.querySelector("canvas"); // DOM에서 캔버스를 선택한다
const ctx = canvas.getContext("2d"); // 캔버스의 2D 렌더링 컨텍스트를 가져온다
const dpr = window.devicePixelRatio; // 장치의 픽셀 비율을 저장한다

let canvasWidth, canvasHeight;

function init() {
  // 윈도우 내부의 너비와 높이로 캔버스의 사이즈를 설정한다.
  canvasWidth = window.innerWidth;
  canvasHeight = window.innerHeight;

  // DPR로 스케일을 조정한 캔버스 너비와 높이를 설정한다.
  canvas.width = canvasWidth * dpr;
  canvas.height = canvasHeight * dpr;
  ctx.scale(dpr, dpr); // 2D 랜더링 컨텍스트를 DPR을 사용하여 스케일을 맞춘다.

  // 캔버스의 CSS너비와 높이를 윈도우 내부의 너비와 높이로 설정한다.
  canvas.style.width = canvasWidth + "px";
  canvas.style.height = canvasHeight + "px";
}
  
let frameInterval = 1000 / 60; // 프레임을 60 FPS로 제한한다.
let currentTime, deltaTime;
let previousTime = performance.now(); // Date.now()보다 더 정밀한 performance.now()를 사용한다.

let xpos = 0; // 초기 시작 위치
let speed = 1; // 이동 속도

function render() {
  requestAnimationFrame(render); // 다음 프레임을 요청한다.
  currentTime = performance.now(); // current time을 저장한다.
  deltaTime = currentTime - previousTime; // delta time을 계산한다.

  // 시간 차이가 프레임 간격보다 작은 경우, 함수를 종료한다. (실질적인 프레임 제한)
  if (deltaTime < frameInterval) return;

  // 이전 프레임의 시간을 현재 시간으로 업데이트 한다.
  previousTime = currentTime - (deltaTime % frameInterval);

  // 캔버스를 지운다. (매 프레임마다 xpos가 업데이트된 사각형을 그리도록)
  ctx.clearRect(0, 0, canvasWidth, canvasHeight);

  // xpos를 속도와 시간 차이의 곱만큼 증가시킨다.
  // 이렇게 하면 프레임 비율이 변동해도 움직임이 부드럽게 유지된다.
  xpos += speed * deltaTime;

  // xpos가 캔버스 너비를 초과하면 0으로 설정한다. (캔버스 내에서 반복하도록)
  if (xpos > canvasWidth) xpos = 0;

  // 현재 xpos에 사각형을 그린다
  ctx.beginPath();
  ctx.fillStyle = "green";
  ctx.strokeRect(xpos, 50, 50, 50);
  ctx.fillRect(xpos, 50, 50, 50);
  ctx.closePath();
}

// 윈도우가 로드되면, 캔버스를 초기화하고 첫 애니메이션 프레임을 요청한다.
window.addEventListener("load", () => {
  init();
  render();
});

그렇지만, 144hz의 디스플레이를 사용하는 유저에게는 144fps의 애니메이션을 보여주고 싶다면? 즉, 프레임의 제한 없이, 가능한 높은 프레임레이트의 애니메이션을 구현하고 싶다면 어떻게 해야할까?

이에 대해서는 아래와 같은 방법이 있었다.

2. 실제 시간 기반의 애니메이션 방식

const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
const dpr = window.devicePixelRatio;

let canvasWidth, canvasHeight;

function init() {
  canvasWidth = window.innerWidth;
  canvasHeight = window.innerHeight;

  canvas.width = canvasWidth * dpr;
  canvas.height = canvasHeight * dpr;
  ctx.scale(dpr, dpr);

  canvas.style.width = canvasWidth + "px";
  canvas.style.height = canvasHeight + "px";
}

let deltaTime;
let previousTime = performance.now();

let xpos = 0;
let speed = 1; // 여기까지 1번의 방식과 동일하다.

// requestAnimationFrame 메서드는 'DOMHighResTimeStamp'를 인수로 전달한다.
// 아래 render함수에서는 이것을 currentTime이라는 이름으로 지정하였다.
// 이는 render라는 콜백함수가 실행된 시간 값이며, performance.now()의 반환값과 동일하다.
function render(currentTime) { 
  requestAnimationFrame(render); // 다음 프레임을 요청한다.

  // delta time을 설정한다.
  deltaTime = (currentTime - previousTime); 

  // 이전 프레임의 시간을 현재 시간으로 업데이트 한다.
  previousTime = currentTime;

  // 캔버스를 지운다. (매 프레임마다 xpos가 업데이트된 사각형을 그리도록)
  ctx.clearRect(0, 0, canvasWidth, canvasHeight);

  //  xpos를 속도와 시간 차이의 곱만큼 증가시킨다.
  xpos += speed * deltaTime;

  // xpos가 캔버스 너비를 초과하면 0으로 설정한다. (캔버스 내에서 반복하도록)
  if (xpos > canvasWidth) xpos = 0;

  ctx.beginPath();
  ctx.fillStyle = "green";
  ctx.strokeRect(xpos, 50, 50, 50);
  ctx.fillRect(xpos, 50, 50, 50);
  ctx.closePath();
}

window.addEventListener("load", () => {
  init(); // 윈도우가 로드되면, 캔버스를 초기화한다.
  
  // 그리고 첫 애니메이션 프레임을 요청한다.
  // 1번의 방식처럼 render() 함수 자체를 실행하지 않은 이유는,
  // 위 render 콜백함수에서 인자로 받을 currentTime의 값을 받아오기 위함이다.
  // (1번 방식처럼 render() 함수를 실행할 시, 초기 currentTime이 undefiend 상태)
  requestAnimationFrame(render);
});

위와 같은 방식은 1번의 FPS를 제한하는 방식과는 다르게, 이동량을 "pixel/프레임" 이 아니라 "pixel/시간" 으로 고려한 방식이다. 이를 통해 프레임 속도가 느려지더라도 화면에 표시되는 애니메이션 속도는 일정하게 유지될 수 있는 것이다.

이러한 방식은 아래와 같은 장점을 가진다

⭕ 애니메이션의 속도가 FPS에 의존하지 않고 실제 경과 시간에 기반하게 되어, 다양한 환경에서 일관된 결과를 보장한다.
⭕ 각 사용자의 프레임레이트에 맞는 최고 성능의 애니메이션을 구현할 수 있다.

그러나, FPS를 제한하지 않기에 고주사율의 디스플레이에서는 1번의 방식보다 더 많은 연산을 해야한다는 점을 고려해야한다.

이것은 내 개인적인 생각이지만, 고주사율의 디스플레이를 쓰는 유저는 그만큼 고성능의 하드웨어를 사용하고 있을 것이고, 이를 활용하여 최상의 그래픽 모션을 원할 것이라는 생각을 한다.

따라서, 개발자의 입장에서도 '굳이 144hz 디스플레이를 쓰는 유저에게 애니메이션 프레임을 제한하여 60프레임의 애니메이션을 보여주는 것이 과연 좋을까' 라는 생각을 하면서 2번의 방식을 더 선호한다.
: 프레임이 순간적으로 드랍되어 Delta Time의 값이 커지고, 이로 인해 애니메이션이 의도치않게 빨라지는 등의 이슈를 겪었다. 이를 해결하기 위한 과정에 대한 포스트를 추가적으로 작성해보았다.


4. 마치며

웹 상에서 애니메이션과 3D 인터렉션을 구현하기 위해 이제 막 공부를 시작하면서, 처음 듣는 Delta Time의 개념에 대해 공부했던 시간이었다. 몰랐던 개념이지만, 알면 알수록 애니메이션에서는 당연하게 알고 있어야 하는 개념이라고 느껴졌다.

현재 듣고 있는 강의에서는 최대 FPS를 제한하는 방법을 사용하고 있었는데, '고주사율 디스플레이를 사용하는 사람들도 애니메이션은 60FPS로 봐야하는 것인가?' 라는 의구심이 들었고, 역시나 모든 환경에서 각자 최고의 퍼포먼스로 대응할 수 있는 방법이 있다는 것을 알게 되었다.

앞으로 계속 공부를 해 나가면서 프론트엔드 개발자로서, 그 중에서도 3D 그래픽과 사용자 인터렉션에 큰 가치를 두고 성장해나가는 프론트엔드 개발자로 성장하기 위해 열심히 노력해야겠다!

profile
깔끔한 비즈니스 로직 설계를 위해 공부하는 FE 개발자

4개의 댓글

comment-user-thumbnail
2023년 7월 29일

많은 도움이 되었습니다, 감사합니다.

1개의 답글
comment-user-thumbnail
2023년 7월 29일

잘 읽고갑니당~ 후속 시리즈도 기대할께요

1개의 답글