개요
혹시 플립북(flip book)을 아시나요? 이름은 몰라도 한 번쯤은 보신 적이 있을 겁니다. 종이에 한 장씩 그림을 그리고 빠르게 넘기면 마치 그림이 움직이는 것처럼 보이죠? 이게 바로 애니메이션의 기본 원리입니다.
- 예시
https://www.youtube.com/watch?v=uMky_iMPVM4
이 원리는 컴퓨터 애니메이션에도 그대로 적용됩니다. 컴퓨터는 매 순간 장면을 한 프레임씩 계싼해 화면에 그려냅니다. 다만, 사람이 손으로 넘기는 것보다 훨씬 빠르게, 초당 60프레임(60FPS) 혹은 144프레임(144FPS) 속도로 그리기 때문에 부드럽고 생동감 있게 느껴지는 것이죠.
- requestAnimationFrame
https://developer.mozilla.org/ko/docs/Web/API/Window/requestAnimationFrame
Window: requestAnimationFrame() method - Web API | MDN
window.requestAnimationFrame() 메서드는 브라우저에게 수행하기를 원하는 애니메이션을 알리고 다음 리페인트 바로 전에 브라우저가 애니메이션을 업데이트할 지정된 함수를 호출하도록 요청합니다.
developer.mozilla.org
requestAnimationFrame은 브라우저에게 “다음 프레임이 준비되면 이 함수를 실행해줘” 라고 요청하는 메서드입니다. 이를 활용하면 부드럽고 효율적인 애니메이션 루프를 만들 수 있습니다.
- 작동 흐름
- 애니메이션을 갱신할 함수(tick)를 정의합니다.
- requestAnimationFrame(tick)을 호출해, 다음 프레임에서 tick을 실행하도록 예약합니다.
- tick 함수 안에서 다시 requestAnimationFrame(tick)을 호출하면,
- 이 과정이 반복되면서 프레임마다 자동으로 tick이 호출되는 애니메이션 루프가 만들어집니다.
function tick() {
// 프레임마다 실행할 코드
console.log('tick function!');
// 다음 프레임 예약
requestAnimationFrame(tick);
}
requestAnimationFrame(tick); // 루프 시작
tick 함수가 매 프레임 호출되는 이유는 애니메이션을 부드럽게 만들기 위함입니다. 플립북처럼 그림을 한 장씩 넘기며 움직임을 표현하듯, 컴퓨터는 tick 함수가 한 프레임에 한 번씩 실행되면서 매 순간 변화를 적용하죠.
이제, 해당 함수를 이용해 플립 북과 비슷한 애니메이션의 개념을 학습해보도록 하겠습니다.
- x축으로 물체를 움직이기
function tick() {
// x축으로 이동
mesh.position.x += 0.01;
// 다음 프레임 예약
requestAnimationFrame(tick);
}
requestAnimationFrame(tick); // 루프 시작
위 코드는 매 프레임마다 mesh의 x 위치를 0.01만큼 증가시켜, 물체가 오른쪽으로 천천히 움직이는 효과를 줍니다. 반면, -= 0.01로 간다면 물체가 왼쪽으로 가는 것을 확인할 수 있습니다.
- 다른 축도 똑같이 움직일 수 있음
x축 말고 y축, z축도 마찬가지로 위치를 바꿀 수 있습니다.
- y축으로 기준으로 물체를 회전하기
function tick() {
// y축으로 회전시키기
mesh.rotation.y += 0.01;
// 다음 프레임 예약
requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
위 코드를 실행하면, FPS 게임에서 캐릭터가 좌우로 천천히 돌 듯이 물체가 y축을 중심으로 부드럽게 회전하는 걸 볼 수 있습니다.
- 이 방식에서 생기는 문제점
tick 함수가 컴퓨터 성능마다 다르게 실행되기 때문에, 어떤 컴퓨터는 빠르게, 어떤 컴퓨터는 느리게 움직일 수 있습니다. 즉, 프레임 속도(FPS)가 다르면 애니메이션 속도도 달라지는 문제가 생기는 거죠. 그래서 시간 차이를 이용해 애니메이션 속도를 보정하게 됩니다.
- 보정 방법: Date와 Clock
let time = Date.now();
function tick() {
// Time
const currentTime = Date.now();
const deltaTime = currentTime - time;
time = currentTime;
console.log(deltaTime);
// Update objects
mesh.rotation.y += 0.001 * deltaTime;
// Render
renderer.render(scene, camera);
// next frame
requestAnimationFrame(tick);
}
이 코드는 Date 객체를 사용해 이전 프레임과 현재 프레임 간 시간 차이(deltaTime)를 구하고, 그 값을 애니메이션 속도에 곱해주는 방식입니다. 오른쪽 사진을 통해 실제 프레임 차이(deltaTime)를 확인할 수 있습니다.
// Clock
const clock = new THREE.Clock();
function tick() {
// Clock
const elapsedTime = clock.getElapsedTime();
console.log(elapsedTime);
// Update objects
mesh.rotation.y = elapsedTime;
// Render
renderer.render(scene, camera);
// next frame
requestAnimationFrame(tick);
}
THREE.Clcok은 직접 프레임마다 시간을 계산하는 대신, 시작 시간을 기준으로 경과 시간을 쉽게 추적할 수 있게 도와주는 객체입니다. 처음에 Clock을 만들고, getElapsedTime() 메서드를 사용하면, 시작 시점부터 얼마나 시간이 흘렀는지를 초 단위로 바로 가져올 수 있습니다.
- 결과는 안바뀌는데 이렇게 하면 뭐가 좋아지나요?
빠른 컴퓨터는 deltaTime이 작아지고, 느린 컴퓨터는 커지면서 애니메이션 속도가 일정하게 유지됩니다. 즉, 모든 사용자에게 동일한 속도를 보장하여 자연스러운 움직임을 보여줄 수 있는거죠.
- clock.getDelta를 쓰면 될 것 같은데 왜 이렇게 번거롭게 하나요?
clock.getDelta()는 프레임 간 시간 차를 계산해 프레임 보정된 애니메이션을 만들 수 있지만, 프레임 손실이나 외부 엔진 간 충돌이 발생하면 적용이 까다로울 수 있습니다. 반면 clock.getElapsedTime()은 절대 시간 기반으로, 모든 환경에서 보다 안정적으로 사용할 수 있어 Three.js의 학습이나 일반적인 애니메이션 구성에 더 적합합니다.
- Math를 활용한 큐브 애니메이션
// Clock
const clock = new THREE.Clock();
function tick() {
// Clock
const elapsedTime = clock.getElapsedTime();
// Update objects
mesh.position.y = Math.sin(elapsedTime);
// Render
renderer.render(scene, camera);
// next frame
requestAnimationFrame(tick);
}
Math.sin() 함수와 getElapsedTime()의 조합은 파형처럼 부드럽게 위아래로 움직이는 움직임을 만들어줍니다. 마치 큐브가 둥실둥실 떠다디는 듯한 느낌이죠.
// Clock
const clock = new THREE.Clock();
function tick() {
// Clock
const elapsedTime = clock.getElapsedTime();
// Update objects
mesh.position.y = Math.sin(elapsedTime);
mesh.position.x = Math.cos(elapsedTime);
// Render
renderer.render(scene, camera);
// next frame
requestAnimationFrame(tick);
}
이번엔 Math.sin()과 Math.cos()를 함께 사용해서 2차원 회전을 구현해봤습니다.
이 공식은 기본적인 원 운동 공식으로, (x, y) = ( cos(t), sin(t) ) 형태를 따라 큐브가 부드럽게 원을 그리며 움직이게 됩니다.
- Camera 사용 : 자연스럽게 모델을 바라보게 하기
const clock = new THREE.Clock();
function tick() {
// Clock
const elapsedTime = clock.getElapsedTime();
// 카메라 위치를 원형으로 이동
camera.position.y = Math.sin(elapsedTime);
camera.position.x = Math.cos(elapsedTime);
// 항상 mesh를 바라보게 설정
camera.lookAt(mesh.position);
// Render
renderer.render(scene, camera);
// next frame
requestAnimationFrame(tick);
}
카메라의 위치를 sin, cos를 활용해 원형으로 회전시키고, lookAt(mesh.position)을 통해 항상 중앙의 mesh를 바라보게 만들었습니다.
이렇게 하면 마치 카메라가 중심을 돌며 피사체를 관찰하는 듯한 연출이 가능해지죠. 이 방법은 단순하지만, 모델 전시나 인터랙티브 웹에서 시선을 자연스럽게 이끄는 데 매우 유용합니다.
핵심은 카메라의 위치를 수학적으로 움직이게 만들고,
lookAt으로 항상 대상(mesh)을 주시하게 만드는 것이죠,
이 구조만 잘 응용해도 꽤 다채로운 비주얼을 만들 수 있습니다.
결론
애니메이션의 기본은 플립북처럼 프레임마다 장면을 갱신하는 것입니다. requestAnimationFrame() 은 이를 효율적으로 구현할 수 있도록 도와주는 메서드이며, THREE.Clock 이나 Date를 활용해 시간 보정까지 하면 어떤 환경에서도 부드러운 애니메이션을 구현할 수 있습니다.
또한, Math.sin, Math.cos와 같은 삼각함수를 이용하면 객체나 카메라에 자연스럽고 반복적인 움직임을 줄 수 있습니다. 특히 카메라의 lookAt()과 함께 사용하면 인터랙티브하고 몰입감 있는 장면 연출이 가능하죠.
- 시간을 잘 다루고, 수학을 잘 쓰는 것!
이 두 가지만 익혀두면, Three.js에서 훨씬 다양한 연출을 구현할 수 있습니다.
'Three.js > Intro' 카테고리의 다른 글
[threejs-journey 1-6] Control (0) | 2025.06.10 |
---|---|
[threejs-journey 1-6] Camera (4) | 2025.06.10 |
[threejs-journey 1-4 부록] 좌표계, 벡터 연산, 디버깅 도구, Group 구조 (0) | 2025.06.03 |
[threejs-journey 1-4] : Object Transform(position, rotation, scale) (0) | 2025.06.01 |
[threejs-journey 1-3] : Scene 구성(깊게 통찰해보자!) (0) | 2025.05.30 |