1. 로딩 최적화를 위한 Progress Manager 구현

대규모 3D 프로젝트에서는 수십 개의 모델과 텍스처를 로드해야 하는 경우가 많습니다.
이런 상황에서 사용자에게 진행 상황을 보여주고 효율적으로 리소스를 관리하는 것이 중요합니다.

class AssetLoadingManager {
    private loadingManager: THREE.LoadingManager;
    private progressElement: HTMLElement;
    private loadingScreen: HTMLElement;
    private assets: Map<string, any>;
    private totalItems: number;
    private loadedItems: number;

    constructor() {
        this.assets = new Map();
        this.totalItems = 0;
        this.loadedItems = 0;
        this.setupLoadingUI();
        this.initializeLoadingManager();
    }

    private setupLoadingUI() {
        this.loadingScreen = document.createElement('div');
        this.loadingScreen.style.cssText = `
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0, 0, 0, 0.9);
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
            z-index: 1000;
        `;

        this.progressElement = document.createElement('div');
        this.progressElement.style.cssText = `
            color: white;
            font-size: 1.5em;
            margin-top: 20px;
        `;

        const progressBar = document.createElement('div');
        progressBar.style.cssText = `
            width: 300px;
            height: 5px;
            background: #333;
            border-radius: 5px;
            margin-top: 10px;
        `;

        this.loadingScreen.appendChild(this.progressElement);
        this.loadingScreen.appendChild(progressBar);
        document.body.appendChild(this.loadingScreen);
    }

    private initializeLoadingManager() {
        this.loadingManager = new THREE.LoadingManager();
        
        this.loadingManager.onProgress = (url, loaded, total) => {
            const progress = (loaded / total) * 100;
            this.progressElement.textContent = `로딩 중... ${Math.round(progress)}%`;
        };

        this.loadingManager.onLoad = () => {
            this.loadingScreen.style.display = 'none';
        };

        this.loadingManager.onError = (url) => {
            console.error(`에셋 로딩 실패: ${url}`);
            // 재시도 로직 구현
            this.retryLoading(url);
        };
    }

    private async retryLoading(url: string, maxRetries: number = 3) {
        let retryCount = 0;
        while (retryCount < maxRetries) {
            try {
                await new Promise(resolve => setTimeout(resolve, 1000 * (retryCount + 1)));
                // 재시도 로직
                // ... 구현 ...
                break;
            } catch (error) {
                retryCount++;
                console.warn(`재시도 ${retryCount}/${maxRetries} 실패: ${url}`);
            }
        }
    }
}

2. 메모리 관리 및 캐싱 시스템 구현

3D 에셋은 메모리를 많이 사용하므로, 효율적인 메모리 관리가 필수적입니다.

class AssetCache {
    private static instance: AssetCache;
    private cache: Map<string, {
        asset: any,
        lastAccessed: number,
        size: number
    }>;
    private maxCacheSize: number = 512 * 1024 * 1024; // 512MB
    private currentCacheSize: number = 0;

    private constructor() {
        this.cache = new Map();
        // 주기적으로 캐시 정리
        setInterval(() => this.cleanCache(), 60000);
    }

    static getInstance(): AssetCache {
        if (!AssetCache.instance) {
            AssetCache.instance = new AssetCache();
        }
        return AssetCache.instance;
    }

    async getAsset(key: string, loader: () => Promise<any>): Promise<any> {
        if (this.cache.has(key)) {
            const cacheItem = this.cache.get(key)!;
            cacheItem.lastAccessed = Date.now();
            return cacheItem.asset;
        }

        const asset = await loader();
        const size = this.estimateAssetSize(asset);

        // 캐시 공간 확보
        while (this.currentCacheSize + size > this.maxCacheSize) {
            this.removeOldestAsset();
        }

        this.cache.set(key, {
            asset,
            lastAccessed: Date.now(),
            size
        });
        this.currentCacheSize += size;

        return asset;
    }

    private estimateAssetSize(asset: any): number {
        // 여기서는 간단한 추정만 구현
        if (asset instanceof THREE.Texture) {
            return asset.image.width * asset.image.height * 4; // RGBA
        }
        if (asset instanceof THREE.BufferGeometry) {
            return asset.attributes.position.array.length * 4; // Float32
        }
        return 1024; // 기본 1KB
    }

    private removeOldestAsset() {
        let oldest: [string, any] | null = null;
        for (const [key, value] of this.cache.entries()) {
            if (!oldest || value.lastAccessed < oldest[1].lastAccessed) {
                oldest = [key, value];
            }
        }

        if (oldest) {
            const [key, value] = oldest;
            this.currentCacheSize -= value.size;
            this.cache.delete(key);
            // 메모리에서 실제로 제거
            if (value.asset.dispose) {
                value.asset.dispose();
            }
        }
    }

    private cleanCache() {
        const now = Date.now();
        const maxAge = 30 * 60 * 1000; // 30분

        for (const [key, value] of this.cache.entries()) {
            if (now - value.lastAccessed > maxAge) {
                this.currentCacheSize -= value.size;
                this.cache.delete(key);
                if (value.asset.dispose) {
                    value.asset.dispose();
                }
            }
        }
    }
}

3. LOD (Level of Detail) 시스템 구현

거리에 따라 모델의 디테일 수준을 조절하여 성능을 최적화합니다.

class ModelLODManager {
    private static readonly LOD_LEVELS = {
        HIGH: { distance: 10, vertexLimit: 50000 },
        MEDIUM: { distance: 30, vertexLimit: 20000 },
        LOW: { distance: 100, vertexLimit: 5000 }
    };

    private modelCache: Map<string, THREE.LOD>;
    private camera: THREE.Camera;

    constructor(camera: THREE.Camera) {
        this.modelCache = new Map();
        this.camera = camera;
    }

    async loadModelWithLOD(modelPath: string): Promise<THREE.LOD> {
        if (this.modelCache.has(modelPath)) {
            return this.modelCache.get(modelPath)!.clone();
        }

        const lod = new THREE.LOD();
        const loader = new GLTFLoader();

        // 각 LOD 레벨에 대해 모델 로드 및 최적화
        for (const [level, config] of Object.entries(ModelLODManager.LOD_LEVELS)) {
            const modelPathWithLOD = modelPath.replace('.glb', `_${level.toLowerCase()}.glb`);
            try {
                const gltf = await loader.loadAsync(modelPathWithLOD);
                const optimizedModel = this.optimizeModel(gltf.scene, config.vertexLimit);
                lod.addLevel(optimizedModel, config.distance);
            } catch (error) {
                console.warn(`LOD 레벨 ${level} 로드 실패:`, error);
            }
        }

        this.modelCache.set(modelPath, lod);
        return lod.clone();
    }

    private optimizeModel(model: THREE.Object3D, vertexLimit: number): THREE.Object3D {
        model.traverse((child) => {
            if (child instanceof THREE.Mesh) {
                const geometry = child.geometry;
                if (geometry.attributes.position.count > vertexLimit) {
                    // SimplifyModifier를 사용한 지오메트리 최적화
                    const modifier = new SimplifyModifier();
                    const simplified = modifier.modify(geometry, 
                        Math.floor(geometry.attributes.position.count * 0.5));
                    child.geometry = simplified;
                }
            }
        });
        return model;
    }
}

4. 실제 사용 예제

위의 모든 시스템을 통합하여 사용하는 코드입니다.

class SceneManager {
    private scene: THREE.Scene;
    private camera: THREE.PerspectiveCamera;
    private renderer: THREE.WebGLRenderer;
    private loadingManager: AssetLoadingManager;
    private assetCache: AssetCache;
    private lodManager: ModelLODManager;

    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.loadingManager = new AssetLoadingManager();
        this.assetCache = AssetCache.getInstance();
        this.lodManager = new ModelLODManager(this.camera);

        this.initialize();
    }

    private async initialize() {
        // 환경 맵 로드
        const envMap = await this.loadEnvironmentMap('environment.hdr');
        this.scene.environment = envMap;

        // 여러 자동차 모델 로드
        const carPositions = [
            { x: 0, y: 0, z: 0 },
            { x: 5, y: 0, z: 5 },
            { x: -5, y: 0, z: -5 }
        ];

        for (const position of carPositions) {
            await this.loadCarModel(position);
        }

        this.startRenderLoop();
    }

    private async loadEnvironmentMap(path: string): Promise<THREE.Texture> {
        return await this.assetCache.getAsset(path, async () => {
            const loader = new RGBELoader(this.loadingManager.getManager());
            const texture = await loader.loadAsync(path);
            texture.mapping = THREE.EquirectangularReflectionMapping;
            return texture;
        });
    }

    private async loadCarModel(position: { x: number, y: number, z: number }) {
        const carLOD = await this.lodManager.loadModelWithLOD('models/car.glb');
        carLOD.position.set(position.x, position.y, position.z);
        this.scene.add(carLOD);
    }

    private startRenderLoop() {
        const animate = () => {
            requestAnimationFrame(animate);
            this.renderer.render(this.scene, this.camera);
        };
        animate();
    }
}

// 사용 예제
const sceneManager = new SceneManager();

5. 성능 모니터링 및 디버깅

실제 프로덕션 환경에서는 성능 모니터링이 중요합니다.

class PerformanceMonitor {
    private stats: Stats;
    private memoryUsage: { geometries: number, textures: number, programs: number };
    private renderer: THREE.WebGLRenderer;

    constructor(renderer: THREE.WebGLRenderer) {
        this.stats = new Stats();
        this.renderer = renderer;
        this.memoryUsage = {
            geometries: 0,
            textures: 0,
            programs: 0
        };

        this.initialize();
    }

    private initialize() {
        document.body.appendChild(this.stats.dom);
        
        // 매 초마다 메모리 사용량 체크
        setInterval(() => this.checkMemoryUsage(), 1000);
    }

    private checkMemoryUsage() {
        const info = this.renderer.info;
        this.memoryUsage = {
            geometries: info.memory.geometries,
            textures: info.memory.textures,
            programs: info.programs.length
        };

        // 경고 임계값 체크
        if (this.memoryUsage.geometries > 1000) {
            console.warn('지오메트리 수가 너무 많습니다');
        }
        if (this.memoryUsage.textures > 100) {
            console.warn('텍스처 수가 너무 많습니다');
        }
    }

    update() {
        this.stats.update();
    }

    getMemoryUsage() {
        return this.memoryUsage;
    }
}

추천 URL

Three.js 공식 문서의 로더 섹션
Three.js 성능 최적화 가이드
WebGL 성능 모니터링 도구
Three.js LOD
메모리 최적화 기법

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

0개의 댓글