[three.js] GLTF 애니메이션 최적화 하기

궁금하면 500원·2024년 12월 23일

1. 애니메이션 최적화 전략

1.1 메모리 관리와 캐싱

먼저 기존 코드의 메모리 사용량을 최적화하기 위한 AnimationCache 클래스를 구현해보겠습니다.

class AnimationCache {
    private static instance: AnimationCache;
    private cache: Map<string, THREE.AnimationClip> = new Map();
    
    private constructor() {}
    
    static getInstance(): AnimationCache {
        if (!AnimationCache.instance) {
            AnimationCache.instance = new AnimationCache();
        }
        return AnimationCache.instance;
    }
    
    async preloadAnimation(url: string): Promise<THREE.AnimationClip> {
        if (this.cache.has(url)) {
            return this.cache.get(url)!;
        }
        
        const loader = new GLTFLoader();
        const gltf = await loader.loadAsync(url);
        const clip = gltf.animations[0];
        this.cache.set(url, clip);
        return clip;
    }
    
    getAnimation(url: string): THREE.AnimationClip | undefined {
        return this.cache.get(url);
    }
    
    clearCache(): void {
        this.cache.clear();
    }
}

이를 활용한 최적화된 로딩 시스템

class AnimationManager {
    private mixer: THREE.AnimationMixer;
    private cache: AnimationCache;
    private activeActions: Map<string, THREE.AnimationAction> = new Map();
    
    constructor(scene: THREE.Scene) {
        this.mixer = new THREE.AnimationMixer(scene);
        this.cache = AnimationCache.getInstance();
    }
    
    async loadAnimation(name: string, url: string): Promise<void> {
        try {
            const clip = await this.cache.preloadAnimation(url);
            const action = this.mixer.clipAction(clip);
            this.activeActions.set(name, action);
        } catch (error) {
            console.error(`Failed to load animation ${name}:`, error);
        }
    }
}

1.2 LOD (Level of Detail) 애니메이션 시스템

카메라와의 거리에 따라 애니메이션 품질을 조절하는 시스템을 구현해보겠습니다.

class AnimationLODSystem {
    private static readonly LOD_LEVELS = {
        NEAR: { distance: 0, fps: 60 },
        MEDIUM: { distance: 10, fps: 30 },
        FAR: { distance: 30, fps: 15 }
    };
    
    private mixer: THREE.AnimationMixer;
    private camera: THREE.Camera;
    private target: THREE.Object3D;
    
    constructor(mixer: THREE.AnimationMixer, camera: THREE.Camera, target: THREE.Object3D) {
        this.mixer = mixer;
        this.camera = camera;
        this.target = target;
    }
    
    update(): void {
        const distance = this.camera.position.distanceTo(this.target.position);
        let timeScale = 1;
        
        if (distance > AnimationLODSystem.LOD_LEVELS.FAR.distance) {
            timeScale = AnimationLODSystem.LOD_LEVELS.FAR.fps / 60;
        } else if (distance > AnimationLODSystem.LOD_LEVELS.MEDIUM.distance) {
            timeScale = AnimationLODSystem.LOD_LEVELS.MEDIUM.fps / 60;
        }
        
        this.mixer.timeScale = timeScale;
    }
}

2. 복합 애니메이션 시스템 구현

여러 애니메이션을 동시에 실행하고 블렌딩하는 고급 시스템을 구현해보겠습니다.

interface AnimationLayer {
    name: string;
    weight: number;
    actions: THREE.AnimationAction[];
}

class LayeredAnimationController {
    private layers: AnimationLayer[] = [];
    private mixer: THREE.AnimationMixer;
    
    constructor(mixer: THREE.AnimationMixer) {
        this.mixer = mixer;
    }
    
    addLayer(name: string, weight: number = 1): void {
        this.layers.push({
            name,
            weight,
            actions: []
        });
    }
    
    addActionToLayer(layerName: string, action: THREE.AnimationAction): void {
        const layer = this.layers.find(l => l.name === layerName);
        if (layer) {
            action.weight = layer.weight;
            layer.actions.push(action);
        }
    }
    
    setLayerWeight(layerName: string, weight: number): void {
        const layer = this.layers.find(l => l.name === layerName);
        if (layer) {
            layer.weight = weight;
            layer.actions.forEach(action => {
                action.weight = weight;
            });
        }
    }
    
    update(delta: number): void {
        this.mixer.update(delta);
    }
}

3. 상태 머신을 활용한 애니메이션 전환

더 복잡한 애니메이션 전환을 관리하기 위한 상태 머신 구현

interface AnimationState {
    name: string;
    action: THREE.AnimationAction;
    transitions: Map<string, AnimationTransition>;
}

interface AnimationTransition {
    toState: string;
    duration: number;
    conditions?: (() => boolean)[];
}

class AnimationStateMachine {
    private states: Map<string, AnimationState> = new Map();
    private currentState?: AnimationState;
    
    constructor(private mixer: THREE.AnimationMixer) {}
    
    addState(name: string, action: THREE.AnimationAction): void {
        this.states.set(name, {
            name,
            action,
            transitions: new Map()
        });
    }
    
    addTransition(
        fromState: string,
        toState: string,
        duration: number,
        conditions?: (() => boolean)[]
    ): void {
        const state = this.states.get(fromState);
        if (state) {
            state.transitions.set(toState, { toState, duration, conditions });
        }
    }
    
    setState(stateName: string): void {
        const newState = this.states.get(stateName);
        if (!newState) return;
        
        if (this.currentState) {
            const transition = this.currentState.transitions.get(stateName);
            if (transition) {
                this.currentState.action.fadeOut(transition.duration);
                newState.action.reset().fadeIn(transition.duration).play();
            }
        } else {
            newState.action.play();
        }
        
        this.currentState = newState;
    }
    
    update(): void {
        if (!this.currentState) return;
        
        // 전환 조건 체크
        this.currentState.transitions.forEach((transition, toState) => {
            if (transition.conditions?.every(condition => condition())) {
                this.setState(toState);
            }
        });
    }
}

4. 퍼포먼스 모니터링과 디버깅

애니메이션 성능을 모니터링하고 디버깅하기 위한 도구

class AnimationPerformanceMonitor {
    private frameCount: number = 0;
    private frameTime: number = 0;
    private stats: {
        averageFrameTime: number;
        activeAnimations: number;
        memoryUsage: number;
    } = {
        averageFrameTime: 0,
        activeAnimations: 0,
        memoryUsage: 0
    };
    
    constructor(private mixer: THREE.AnimationMixer) {
        this.setupDebugGUI();
    }
    
    private setupDebugGUI(): void {
        const gui = new dat.GUI();
        const folder = gui.addFolder('Animation Performance');
        
        folder.add(this.stats, 'averageFrameTime')
            .listen()
            .name('Frame Time (ms)');
        folder.add(this.stats, 'activeAnimations')
            .listen()
            .name('Active Animations');
        folder.add(this.stats, 'memoryUsage')
            .listen()
            .name('Memory Usage (MB)');
            
        folder.open();
    }
    
    update(delta: number): void {
        this.frameCount++;
        this.frameTime += delta;
        
        if (this.frameCount >= 60) {
            this.stats.averageFrameTime = (this.frameTime / this.frameCount) * 1000;
            this.stats.activeAnimations = this.mixer.stats.animations;
            this.stats.memoryUsage = performance.memory?.usedJSHeapSize / (1024 * 1024) || 0;
            
            this.frameCount = 0;
            this.frameTime = 0;
        }
    }
}

5. 실전 활용 사례

위의 모든 시스템을 통합한 실제 사용 예시

class AdvancedCharacterController {
    private animationManager: AnimationManager;
    private lodSystem: AnimationLODSystem;
    private layeredController: LayeredAnimationController;
    private stateMachine: AnimationStateMachine;
    private performanceMonitor: AnimationPerformanceMonitor;
    
    constructor(scene: THREE.Scene, camera: THREE.Camera, character: THREE.Object3D) {
        const mixer = new THREE.AnimationMixer(character);
        
        this.animationManager = new AnimationManager(scene);
        this.lodSystem = new AnimationLODSystem(mixer, camera, character);
        this.layeredController = new LayeredAnimationController(mixer);
        this.stateMachine = new AnimationStateMachine(mixer);
        this.performanceMonitor = new AnimationPerformanceMonitor(mixer);
        
        this.setupAnimations();
        this.setupStateMachine();
    }
    
    private async setupAnimations(): Promise<void> {
        await Promise.all([
            this.animationManager.loadAnimation('idle', 'models/eve$@idle.glb'),
            this.animationManager.loadAnimation('walk', 'models/eve@walk.glb'),
            this.animationManager.loadAnimation('run', 'models/eve@run.glb')
        ]);
        
        // 레이어 설정
        this.layeredController.addLayer('base', 1);
        this.layeredController.addLayer('upper', 0.5);
        
        // 기본 애니메이션 설정
        this.stateMachine.setState('idle');
    }
    
    private setupStateMachine(): void {
        // 걷기 -> 뛰기 전환 조건
        const runningCondition = () => keyMap['ShiftLeft'] && keyMap['KeyW'];
        
        this.stateMachine.addTransition('walk', 'run', 0.2, [runningCondition]);
        this.stateMachine.addTransition('run', 'walk', 0.2, [
            () => !runningCondition()
        ]);
    }
    
    update(delta: number): void {
        this.lodSystem.update();
        this.layeredController.update(delta);
        this.stateMachine.update();
        this.performanceMonitor.update(delta);
    }
}

결론

이번 포스트에서는 기본적인 GLTF 애니메이션을 넘어서 실제 프로덕션 환경에서 활용할 수 있는 고급 기능들을 살펴보았습니다.

메모리 최적화, LOD 시스템, 복합 애니메이션 관리, 상태 머신을 통한 전환 관리, 그리고 성능 모니터링까지 구현해보았습니다.

이러한 시스템들은 대규모 3D 웹 애플리케이션에서 안정적이고 효율적인 애니메이션 구현을 가능하게 합니다.

profile
에러가 나도 괜찮아 — 그건 내가 배우고 있다는 증거야.

0개의 댓글