
대규모 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}`);
}
}
}
}
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();
}
}
}
}
}
거리에 따라 모델의 디테일 수준을 조절하여 성능을 최적화합니다.
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;
}
}
위의 모든 시스템을 통합하여 사용하는 코드입니다.
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();
실제 프로덕션 환경에서는 성능 모니터링이 중요합니다.
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;
}
}
Three.js 공식 문서의 로더 섹션
Three.js 성능 최적화 가이드
WebGL 성능 모니터링 도구
Three.js LOD
메모리 최적화 기법