
먼저 기존 코드의 메모리 사용량을 최적화하기 위한 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);
}
}
}
카메라와의 거리에 따라 애니메이션 품질을 조절하는 시스템을 구현해보겠습니다.
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;
}
}
여러 애니메이션을 동시에 실행하고 블렌딩하는 고급 시스템을 구현해보겠습니다.
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);
}
}
더 복잡한 애니메이션 전환을 관리하기 위한 상태 머신 구현
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);
}
});
}
}
애니메이션 성능을 모니터링하고 디버깅하기 위한 도구
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;
}
}
}
위의 모든 시스템을 통합한 실제 사용 예시
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 웹 애플리케이션에서 안정적이고 효율적인 애니메이션 구현을 가능하게 합니다.