1. 복잡한 애니메이션 시스템 설계

1.1 상태 기반 애니메이션 관리자 구현

복잡한 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?.();
            }
        });
    }
}

1.2 체인 애니메이션 구현

여러 애니메이션을 순차적으로 실행하는 체인 애니메이션 시스템을 구현해봅시다.

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();
            }
        });
    }
}

2. 성능 최적화 전략

2.1 Object Pooling 구현

메모리 사용량을 최적화하기 위한 오브젝트 풀링 시스템을 구현해봅시다.

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();
            }
        });
    }
}

2.2 프레임 최적화

프레임 드롭을 방지하기 위한 최적화 기법을 구현해봅시다.

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() {
        // 그림자 퀄리티 조정 구현
    }
}

3. 고급 보간 테크닉

3.1 커스텀 Easing 함수 구현

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;
    }
}

3.2 복합 보간 시스템

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);
    }
}

4. 실무 사례 연구

4.1 대규모 파티클 시스템 최적화

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;
    }
}

4.2 고성능 물리 기반 애니메이션

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; // 반발 계수
        }
    }
}

4.3 인터랙티브 변형 애니메이션

복잡한 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;
        });
    }
}

4.4 고급 카메라 컨트롤

시네마틱한 카메라 움직임을 구현하는 컨트롤러입니다.

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);
    }
}

최적화 팁

1. 메모리 관리

  • Object Pooling 활용
  • 불필요한 객체 제거
  • WeakMap, WeakSet 활용

2. 렌더링 최적화

  • Level of Detail (LOD) 구현
  • Frustum Culling 활용
  • 적절한 그림자 설정

3. 애니메이션 최적화

  • RequestAnimationFrame 사용
  • 델타 타임 보정
  • GPU 가속 활용

4. 로직 최적화

  • Web Workers 활용
  • 복잡한 계산 캐싱
  • 이벤트 디바운싱/쓰로틀링

이러한 고급 애니메이션 테크닉들을 활용하면 더 복잡하고 세련된 3D 웹 애플리케이션을 구현할 수 있습니다.

성능과 사용자 경험을 모두 고려한 최적화된 애니메이션 시스템을 구축하는 것이 중요하다는것을 배운 계기가 되었습니다.

profile
꾸준히, 의미있는 사이드 프로젝트 경험과 문제해결 과정을 기록하기 위한 공간입니다.

0개의 댓글