복잡한 3D 웹 애플리케이션에서는 수십 개의 애니메이션이 동시에 실행될 수 있습니다.
이를 효율적으로 관리하기 위한 AnimationManager 클래스를 구현해보겠습니다.
type AnimationState = 'idle' | 'running' | 'paused' | 'finished';
interface AnimationConfig {
duration: number;
easing: (t: number) => number;
delay?: number;
onUpdate?: (progress: number) => void;
onComplete?: () => void;
}
class AnimationManager {
private animations: Map<string, {
target: any;
state: AnimationState;
startTime: number;
config: AnimationConfig;
initialValues: Record<string, number>;
targetValues: Record<string, number>;
}> = new Map();
constructor() {
this.update = this.update.bind(this);
}
addAnimation(
id: string,
target: any,
targetValues: Record<string, number>,
config: AnimationConfig
) {
const initialValues: Record<string, number> = {};
// 초기값 저장
Object.keys(targetValues).forEach(key => {
initialValues[key] = target[key];
});
this.animations.set(id, {
target,
state: 'idle',
startTime: performance.now() + (config.delay || 0),
config,
initialValues,
targetValues
});
}
update() {
const currentTime = performance.now();
this.animations.forEach((animation, id) => {
if (animation.state === 'finished') return;
if (currentTime < animation.startTime) return;
if (animation.state === 'idle') {
animation.state = 'running';
}
const elapsed = currentTime - animation.startTime;
const progress = Math.min(elapsed / animation.config.duration, 1);
if (progress === 1) {
animation.state = 'finished';
}
const easedProgress = animation.config.easing(progress);
// 값 업데이트
Object.keys(animation.targetValues).forEach(key => {
animation.target[key] = animation.initialValues[key] +
(animation.targetValues[key] - animation.initialValues[key]) * easedProgress;
});
animation.config.onUpdate?.(progress);
if (animation.state === 'finished') {
animation.config.onComplete?.();
}
});
}
}
여러 애니메이션을 순차적으로 실행하는 체인 애니메이션 시스템을 구현해봅시다.
class AnimationChain {
private sequences: Array<{
target: any;
properties: Record<string, number>;
config: AnimationConfig;
}> = [];
private currentIndex = 0;
private manager: AnimationManager;
constructor(manager: AnimationManager) {
this.manager = manager;
}
add(
target: any,
properties: Record<string, number>,
config: AnimationConfig
): this {
this.sequences.push({ target, properties, config });
return this;
}
start() {
this.playNext();
}
private playNext() {
if (this.currentIndex >= this.sequences.length) return;
const current = this.sequences[this.currentIndex];
const id = `chain-${this.currentIndex}`;
this.manager.addAnimation(id, current.target, current.properties, {
...current.config,
onComplete: () => {
current.config.onComplete?.();
this.currentIndex++;
this.playNext();
}
});
}
}
메모리 사용량을 최적화하기 위한 오브젝트 풀링 시스템을 구현해봅시다.
class ObjectPool<T extends THREE.Object3D> {
private pool: T[] = [];
private active: Set<T> = new Set();
private factory: () => T;
constructor(
factory: () => T,
initialSize: number = 20
) {
this.factory = factory;
this.initialize(initialSize);
}
private initialize(size: number) {
for (let i = 0; i < size; i++) {
this.pool.push(this.factory());
}
}
acquire(): T {
let object: T;
if (this.pool.length > 0) {
object = this.pool.pop()!;
} else {
object = this.factory();
}
this.active.add(object);
return object;
}
release(object: T) {
if (this.active.has(object)) {
this.active.delete(object);
this.pool.push(object);
object.visible = false;
}
}
update() {
this.active.forEach(object => {
if ((object as any).update) {
(object as any).update();
}
});
}
}
프레임 드롭을 방지하기 위한 최적화 기법을 구현해봅시다.
class PerformanceMonitor {
private static instance: PerformanceMonitor;
private lastTime: number = 0;
private frameRates: number[] = [];
private readonly sampleSize = 60;
private warningThreshold = 45; // 45fps 이하면 경고
private constructor() {
this.update = this.update.bind(this);
}
static getInstance() {
if (!PerformanceMonitor.instance) {
PerformanceMonitor.instance = new PerformanceMonitor();
}
return PerformanceMonitor.instance;
}
update() {
const currentTime = performance.now();
const deltaTime = currentTime - this.lastTime;
this.lastTime = currentTime;
const fps = 1000 / deltaTime;
this.frameRates.push(fps);
if (this.frameRates.length > this.sampleSize) {
this.frameRates.shift();
}
const averageFps = this.frameRates.reduce((a, b) => a + b) / this.frameRates.length;
if (averageFps < this.warningThreshold) {
this.optimizeRendering();
}
}
private optimizeRendering() {
// LOD (Level of Detail) 조정
this.adjustLOD();
// 파티클 시스템 최적화
this.optimizeParticleSystems();
// 그림자 퀄리티 조정
this.adjustShadowQuality();
}
private adjustLOD() {
// LOD 구현
}
private optimizeParticleSystems() {
// 파티클 시스템 최적화 구현
}
private adjustShadowQuality() {
// 그림자 퀄리티 조정 구현
}
}
class EasingFunctions {
static elasticOut(t: number): number {
return Math.sin(-13.0 * (t + 1.0) * Math.PI / 2) * Math.pow(2.0, -10.0 * t) + 1.0;
}
static bounceOut(t: number): number {
if (t < 1 / 2.75) {
return 7.5625 * t * t;
} else if (t < 2 / 2.75) {
t -= 1.5 / 2.75;
return 7.5625 * t * t + 0.75;
} else if (t < 2.5 / 2.75) {
t -= 2.25 / 2.75;
return 7.5625 * t * t + 0.9375;
}
t -= 2.625 / 2.75;
return 7.5625 * t * t + 0.984375;
}
static customSpring(t: number): number {
const c4 = (2 * Math.PI) / 3;
return -Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c4) + 1;
}
}
interface KeyFrame {
time: number;
value: number;
easing?: (t: number) => number;
}
class KeyframeInterpolator {
private keyframes: KeyFrame[] = [];
addKeyframe(keyframe: KeyFrame) {
this.keyframes.push(keyframe);
this.keyframes.sort((a, b) => a.time - b.time);
}
getValue(time: number): number {
if (this.keyframes.length === 0) return 0;
if (time <= this.keyframes[0].time) return this.keyframes[0].value;
if (time >= this.keyframes[this.keyframes.length - 1].time) {
return this.keyframes[this.keyframes.length - 1].value;
}
let i = 0;
while (i < this.keyframes.length - 1 && time > this.keyframes[i + 1].time) {
i++;
}
const frame1 = this.keyframes[i];
const frame2 = this.keyframes[i + 1];
const t = (time - frame1.time) / (frame2.time - frame1.time);
const easingFunction = frame1.easing || ((x: number) => x);
return frame1.value + (frame2.value - frame1.value) * easingFunction(t);
}
}
class OptimizedParticleSystem {
private geometry: THREE.BufferGeometry;
private material: THREE.ShaderMaterial;
private particles: THREE.Points;
private positions: Float32Array;
private velocities: Float32Array;
private lifetimes: Float32Array;
private maxParticles: number;
constructor(maxParticles: number) {
this.maxParticles = maxParticles;
this.positions = new Float32Array(maxParticles * 3);
this.velocities = new Float32Array(maxParticles * 3);
this.lifetimes = new Float32Array(maxParticles);
this.initializeGeometry();
this.initializeMaterial();
this.createParticleSystem();
}
private initializeGeometry() {
this.geometry = new THREE.BufferGeometry();
this.geometry.setAttribute('position', new THREE.BufferAttribute(this.positions, 3));
this.geometry.setAttribute('velocity', new THREE.BufferAttribute(this.velocities, 3));
this.geometry.setAttribute('lifetime', new THREE.BufferAttribute(this.lifetimes, 1));
}
private initializeMaterial() {
this.material = new THREE.ShaderMaterial({
uniforms: {
time: { value: 0 }
},
vertexShader: `
attribute vec3 velocity;
attribute float lifetime;
uniform float time;
void main() {
vec3 pos = position + velocity * time;
float life = max(0.0, 1.0 - time / lifetime);
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
gl_PointSize = 2.0 * life;
}
`,
fragmentShader: `
void main() {
gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
}
`,
transparent: true
});
}
private createParticleSystem() {
this.particles = new THREE.Points(this.geometry, this.material);
}
update(deltaTime: number) {
this.material.uniforms.time.value += deltaTime;
// GPU에서 처리되므로 CPU 부하 최소화
this.geometry.attributes.position.needsUpdate = true;
}
}
class PhysicsAnimation {
private objects: THREE.Object3D[] = [];
private velocities: THREE.Vector3[] = [];
private accelerations: THREE.Vector3[] = [];
private forces: THREE.Vector3[] = [];
private gravity = new THREE.Vector3(0, -9.81, 0);
private damping = 0.98;
addObject(object: THREE.Object3D) {
this.objects.push(object);
this.velocities.push(new THREE.Vector3());
this.accelerations.push(new THREE.Vector3());
this.forces.push(new THREE.Vector3());
}
applyForce(index: number, force: THREE.Vector3) {
this.forces[index].add(force);
}
// PhysicsAnimation 클래스 update 메서드 이어서...
update(deltaTime: number) {
for (let i = 0; i < this.objects.length; i++) {
// 중력 적용
this.forces[i].add(this.gravity);
// 가속도 계산 (F = ma, a = F/m 여기서는 질량을 1로 가정)
this.accelerations[i].copy(this.forces[i]);
// 속도 업데이트 (v = v0 + at)
this.velocities[i].add(this.accelerations[i].multiplyScalar(deltaTime));
// 감쇠 적용
this.velocities[i].multiplyScalar(this.damping);
// 위치 업데이트 (x = x0 + vt)
this.objects[i].position.add(
this.velocities[i].clone().multiplyScalar(deltaTime)
);
// 충돌 검사 및 처리
this.handleCollisions(i);
// 힘 초기화
this.forces[i].set(0, 0, 0);
}
}
private handleCollisions(index: number) {
// 바닥 충돌 처리
if (this.objects[index].position.y < 0) {
this.objects[index].position.y = 0;
this.velocities[index].y = -this.velocities[index].y * 0.5; // 반발 계수
}
}
}
복잡한 3D 모델의 변형 애니메이션을 구현해보겠습니다.
class MorphAnimationController {
private mesh: THREE.Mesh;
private morphTargets: Array<{ name: string; weight: number }> = [];
private animations: Map<string, {
startWeight: number;
targetWeight: number;
duration: number;
startTime: number;
easing: (t: number) => number;
}> = new Map();
constructor(mesh: THREE.Mesh) {
this.mesh = mesh;
this.initialize();
}
private initialize() {
if (!this.mesh.morphTargetInfluences) return;
for (let i = 0; i < this.mesh.morphTargetInfluences.length; i++) {
this.morphTargets.push({
name: this.mesh.morphTargetDictionary?.[i] || `target${i}`,
weight: this.mesh.morphTargetInfluences[i]
});
}
}
animateMorph(targetName: string, weight: number, duration: number = 1000) {
const targetIndex = this.morphTargets.findIndex(t => t.name === targetName);
if (targetIndex === -1) return;
const currentWeight = this.mesh.morphTargetInfluences![targetIndex];
this.animations.set(targetName, {
startWeight: currentWeight,
targetWeight: weight,
duration: duration,
startTime: performance.now(),
easing: EasingFunctions.customSpring
});
}
update() {
const currentTime = performance.now();
this.animations.forEach((animation, targetName) => {
const elapsed = currentTime - animation.startTime;
const progress = Math.min(elapsed / animation.duration, 1);
if (progress === 1) {
this.animations.delete(targetName);
return;
}
const targetIndex = this.morphTargets.findIndex(t => t.name === targetName);
const easedProgress = animation.easing(progress);
this.mesh.morphTargetInfluences![targetIndex] =
animation.startWeight +
(animation.targetWeight - animation.startWeight) * easedProgress;
});
}
}
시네마틱한 카메라 움직임을 구현하는 컨트롤러입니다.
class CinematicCameraController {
private camera: THREE.PerspectiveCamera;
private target: THREE.Vector3;
private path: THREE.CatmullRomCurve3;
private lookAtPath: THREE.CatmullRomCurve3;
private progress = 0;
private speed = 0.001;
private tension = 0.5;
constructor(camera: THREE.PerspectiveCamera) {
this.camera = camera;
this.target = new THREE.Vector3();
this.initializePaths();
}
private initializePaths() {
// 카메라 이동 경로 설정
const cameraPoints = [
new THREE.Vector3(0, 2, 5),
new THREE.Vector3(5, 3, 0),
new THREE.Vector3(0, 4, -5),
new THREE.Vector3(-5, 2, 0)
];
// 카메라가 바라볼 지점들 설정
const lookAtPoints = [
new THREE.Vector3(0, 0, 0),
new THREE.Vector3(2, 0, 0),
new THREE.Vector3(0, 0, -2),
new THREE.Vector3(-2, 0, 0)
];
this.path = new THREE.CatmullRomCurve3(cameraPoints, true, 'centripetal', this.tension);
this.lookAtPath = new THREE.CatmullRomCurve3(lookAtPoints, true, 'centripetal', this.tension);
}
setKeyframes(cameraPoints: THREE.Vector3[], lookAtPoints: THREE.Vector3[]) {
this.path = new THREE.CatmullRomCurve3(cameraPoints, true, 'centripetal', this.tension);
this.lookAtPath = new THREE.CatmullRomCurve3(lookAtPoints, true, 'centripetal', this.tension);
}
update(deltaTime: number) {
// 경로를 따라 카메라 이동
const position = this.path.getPoint(this.progress);
const lookAt = this.lookAtPath.getPoint(this.progress);
this.camera.position.copy(position);
this.camera.lookAt(lookAt);
// 진행도 업데이트
this.progress += this.speed * deltaTime;
if (this.progress > 1) this.progress = 0;
}
// FOV 애니메이션
animateFOV(targetFOV: number, duration: number = 1000) {
const startFOV = this.camera.fov;
const startTime = performance.now();
const animate = () => {
const currentTime = performance.now();
const progress = Math.min((currentTime - startTime) / duration, 1);
const easedProgress = EasingFunctions.customSpring(progress);
this.camera.fov = startFOV + (targetFOV - startFOV) * easedProgress;
this.camera.updateProjectionMatrix();
if (progress < 1) {
requestAnimationFrame(animate);
}
};
animate();
}
}
위에서 구현한 시스템들을 실제로 활용하는 코드입니다.
class AnimationDemo {
private scene: THREE.Scene;
private camera: THREE.PerspectiveCamera;
private renderer: THREE.WebGLRenderer;
private animationManager: AnimationManager;
private particleSystem: OptimizedParticleSystem;
private physicsAnimation: PhysicsAnimation;
private cameraController: CinematicCameraController;
private performanceMonitor: PerformanceMonitor;
constructor() {
// 기본 설정
this.scene = new THREE.Scene();
this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
this.renderer = new THREE.WebGLRenderer({ antialias: true });
// 매니저 및 시스템 초기화
this.animationManager = new AnimationManager();
this.particleSystem = new OptimizedParticleSystem(10000);
this.physicsAnimation = new PhysicsAnimation();
this.cameraController = new CinematicCameraController(this.camera);
this.performanceMonitor = PerformanceMonitor.getInstance();
this.initialize();
}
private initialize() {
// 씬 설정
this.scene.add(this.particleSystem.particles);
// 예시 애니메이션 체인 생성
const chain = new AnimationChain(this.animationManager);
chain
.add(this.camera.position, { y: 5 }, {
duration: 1000,
easing: EasingFunctions.elasticOut
})
.add(this.camera.position, { z: 10 }, {
duration: 800,
easing: EasingFunctions.bounceOut
})
.start();
}
update(deltaTime: number) {
// 각 시스템 업데이트
this.animationManager.update();
this.particleSystem.update(deltaTime);
this.physicsAnimation.update(deltaTime);
this.cameraController.update(deltaTime);
this.performanceMonitor.update();
// 렌더링
this.renderer.render(this.scene, this.camera);
}
}
이러한 고급 애니메이션 테크닉들을 활용하면 더 복잡하고 세련된 3D 웹 애플리케이션을 구현할 수 있습니다.
성능과 사용자 경험을 모두 고려한 최적화된 애니메이션 시스템을 구축하는 것이 중요하다는것을 배운 계기가 되었습니다.