최근 한 프로젝트에서 3D 건물 모델링 시각화 작업을 진행하면서 심각한 성능 이슈에 직면했습니다.
수천 개의 정점(vertex)과 면(face)으로 구성된 복잡한 건물 모델을 렌더링할 때 프레임 저하 현상이 발생했고, 특히 여러 건물을 동시에 표시할 때는 브라우저가 버벅거리는 현상이 심각했습니다.
초기 구현에서는 THREE.Geometry를 사용했는데, Chrome 개발자 도구의 Performance 패널을 통해 분석해본 결과 다음과 같은 문제점들이 발견되었습니다
이 문제를 해결하기 위해 THREE.BufferGeometry로의 전환을 결정했습니다.
BufferGeometry는 정점 데이터를 TypedArray 형태로 직접 GPU에 전달할 수 있어 메모리 효율성과 렌더링 성능이 훨씬 뛰어납니다.
class BuildingVisualizer {
constructor() {
this.buildings = [];
this.scene = new THREE.Scene();
}
createBuilding(buildingData) {
const geometry = new THREE.Geometry();
// 건물의 각 층 데이터를 순회하며 정점과 면 추가
buildingData.floors.forEach((floor, floorIndex) => {
const height = floorIndex * FLOOR_HEIGHT;
floor.points.forEach(point => {
geometry.vertices.push(
new THREE.Vector3(point.x, height, point.y)
);
});
// 면 추가
for (let i = 0; i < floor.points.length - 2; i++) {
geometry.faces.push(new THREE.Face3(
i, i + 1, i + 2
));
}
});
geometry.computeFaceNormals();
geometry.computeVertexNormals();
const material = new THREE.MeshPhongMaterial({
color: 0x156289,
emissive: 0x072534,
side: THREE.DoubleSide
});
const mesh = new THREE.Mesh(geometry, material);
this.buildings.push(mesh);
this.scene.add(mesh);
}
}
class OptimizedBuildingVisualizer {
constructor() {
this.buildings = [];
this.scene = new THREE.Scene();
this.geometryCache = new Map(); // geometry 재사용을 위한 캐시
}
createBufferedBuilding(buildingData) {
// 캐시된 geometry가 있는지 확인
const cacheKey = this.generateGeometryCacheKey(buildingData);
if (this.geometryCache.has(cacheKey)) {
return this.createInstancedBuilding(this.geometryCache.get(cacheKey), buildingData);
}
const vertices = [];
const normals = [];
const indices = [];
let vertexIndex = 0;
// 건물의 각 층 데이터를 순회하며 버퍼 데이터 생성
buildingData.floors.forEach((floor, floorIndex) => {
const height = floorIndex * FLOOR_HEIGHT;
// 각 층의 정점 데이터 생성
floor.points.forEach(point => {
vertices.push(point.x, height, point.y);
normals.push(0, 1, 0); // 기본 법선 벡터
});
// 면 인덱스 생성
for (let i = 0; i < floor.points.length - 2; i++) {
indices.push(
vertexIndex + i,
vertexIndex + i + 1,
vertexIndex + i + 2
);
}
vertexIndex += floor.points.length;
});
const geometry = new THREE.BufferGeometry();
// 버퍼 속성 설정
geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
geometry.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3));
geometry.setIndex(indices);
// 법선 벡터 재계산
geometry.computeVertexNormals();
// geometry를 캐시에 저장
this.geometryCache.set(cacheKey, geometry);
return this.createInstancedBuilding(geometry, buildingData);
}
createInstancedBuilding(geometry, buildingData) {
const material = new THREE.MeshPhongMaterial({
color: buildingData.color || 0x156289,
emissive: 0x072534,
side: THREE.DoubleSide
});
const mesh = new THREE.Mesh(geometry, material);
// 위치 및 회전 설정
mesh.position.set(buildingData.position.x, 0, buildingData.position.z);
mesh.rotation.y = buildingData.rotation || 0;
this.buildings.push(mesh);
this.scene.add(mesh);
return mesh;
}
generateGeometryCacheKey(buildingData) {
// 건물의 고유한 특성을 기반으로 캐시 키 생성
return `${buildingData.type}_${buildingData.floors.length}_${buildingData.floors[0].points.length}`;
}
// 메모리 정리
dispose() {
this.buildings.forEach(building => {
building.geometry.dispose();
building.material.dispose();
});
this.geometryCache.clear();
}
}
최적화 작업 후 다음과 같은 성능 향상을 확인할 수 있었습니다
이 최적화 기법을 실제 도시 계획 시각화 프로젝트에 적용했을 때의 결과입니다.
// 실제 프로젝트 사용 예시
const cityVisualizer = new OptimizedBuildingVisualizer();
// 도시 데이터 로드
fetch('/api/city-data')
.then(response => response.json())
.then(cityData => {
// 각 건물 데이터에 대해 시각화 수행
cityData.buildings.forEach(buildingData => {
cityVisualizer.createBufferedBuilding(buildingData);
});
// 성능 모니터링 시작
const stats = new Stats();
document.body.appendChild(stats.dom);
// 애니메이션 루프
function animate() {
requestAnimationFrame(animate);
stats.begin();
renderer.render(scene, camera);
stats.end();
}
animate();
});
class PerformanceMonitor {
constructor() {
this.frameCount = 0;
this.totalFrameTime = 0;
this.lastFrameTime = performance.now();
this.memoryUsage = [];
}
measure() {
const currentTime = performance.now();
const frameTime = currentTime - this.lastFrameTime;
this.lastFrameTime = currentTime;
this.frameCount++;
this.totalFrameTime += frameTime;
if (window.performance && window.performance.memory) {
this.memoryUsage.push(window.performance.memory.usedJSHeapSize / 1048576);
}
// 매 100프레임마다 통계 출력
if (this.frameCount % 100 === 0) {
this.printStats();
}
}
printStats() {
const avgFrameTime = this.totalFrameTime / this.frameCount;
const fps = 1000 / avgFrameTime;
const avgMemory = this.memoryUsage.reduce((a, b) => a + b, 0) / this.memoryUsage.length;
console.log(`
Performance Stats:
Average FPS: ${fps.toFixed(2)}
Average Frame Time: ${avgFrameTime.toFixed(2)}ms
Average Memory Usage: ${avgMemory.toFixed(2)}MB
`);
}
}
이러한 최적화 작업을 통해 복잡한 3D 시각화 프로젝트에서도 안정적인 성능을 확보할 수 있었습니다.
특히 BufferGeometry의 도입은 대규모 3D 데이터를 다루는 웹 애플리케이션에서 필수적인 최적화 전략임을 확인할 수 있었습니다.