1. 디버그 렌더러 성능 최적화

1.1 Frustum Culling 최적화

기존 코드에서는 frustumCulled = false로 설정하여 모든 디버그 라인을 항상 렌더링했습니다. 하지만 복잡한 물리 시뮬레이션에서는 이는 성능 저하의 원인이 될 수 있습니다.

다음과 같이 최적화된 방식을 구현해보겠습니다.

class RapierDebugRenderer {
    private chunks: THREE.LineSegments[] = [];
    private chunkSize = 1000; // 청크당 최대 라인 수
    
    constructor(scene: THREE.Scene, world: RAPIER.World) {
        this.world = world;
        this.setupChunks(scene);
    }

    private setupChunks(scene: THREE.Scene) {
        // 청크별로 LineSegments 생성
        const chunk = new THREE.LineSegments(
            new THREE.BufferGeometry(),
            new THREE.LineBasicMaterial({ 
                color: 0xffffff, 
                vertexColors: true,
                transparent: true,
                opacity: 0.7 
            })
        );
        
        // 청크별 바운딩 박스 설정
        chunk.geometry.computeBoundingBox();
        chunk.geometry.computeBoundingSphere();
        
        this.chunks.push(chunk);
        scene.add(chunk);
    }

    update() {
        if (!this.enabled) return;

        const { vertices, colors } = this.world.debugRender();
        
        // 청크별로 데이터 분할
        for (let i = 0; i < vertices.length; i += this.chunkSize * 3) {
            const chunkIndex = Math.floor(i / (this.chunkSize * 3));
            
            if (!this.chunks[chunkIndex]) {
                this.setupChunks(this.scene);
            }

            const chunk = this.chunks[chunkIndex];
            const vertexSlice = vertices.slice(i, i + this.chunkSize * 3);
            const colorSlice = colors.slice(i / 3 * 4, (i + this.chunkSize * 3) / 3 * 4);

            chunk.geometry.setAttribute('position', new THREE.BufferAttribute(vertexSlice, 3));
            chunk.geometry.setAttribute('color', new THREE.BufferAttribute(colorSlice, 4));
            
            // 바운딩 정보 업데이트
            chunk.geometry.computeBoundingBox();
            chunk.geometry.computeBoundingSphere();
        }
    }
}

1.2 메모리 최적화를 위한 BufferGeometry 재사용

물리 시뮬레이션이 실행되는 동안 매 프레임마다 새로운 BufferGeometry를 생성하는 대신, 기존 버퍼를 재사용하여 메모리 사용량을 최적화할 수 있습니다.

class RapierDebugRenderer {
    private geometryCache: Map<number, THREE.BufferGeometry> = new Map();
    
    private updateGeometry(vertexCount: number, vertices: Float32Array, colors: Float32Array) {
        let geometry = this.geometryCache.get(vertexCount);
        
        if (!geometry) {
            geometry = new THREE.BufferGeometry();
            this.geometryCache.set(vertexCount, geometry);
        }
        
        // 버퍼 속성 업데이트
        const positionAttribute = geometry.getAttribute('position') as THREE.BufferAttribute;
        const colorAttribute = geometry.getAttribute('color') as THREE.BufferAttribute;
        
        if (!positionAttribute || positionAttribute.array.length !== vertices.length) {
            geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
        } else {
            positionAttribute.set(vertices);
            positionAttribute.needsUpdate = true;
        }
        
        if (!colorAttribute || colorAttribute.array.length !== colors.length) {
            geometry.setAttribute('color', new THREE.BufferAttribute(colors, 4));
        } else {
            colorAttribute.set(colors);
            colorAttribute.needsUpdate = true;
        }
        
        return geometry;
    }
}

2. 확장 기능 구현

2.1 디버그 필터 시스템

특정 유형의 물리 객체만 선택적으로 렌더링할 수 있는 필터 시스템을 구현해보겠습니다.

interface DebugRenderFilter {
    showDynamicBodies: boolean;
    showStaticBodies: boolean;
    showColliders: boolean;
    showJoints: boolean;
    showAABB: boolean;
    bodyFilter?: (body: RAPIER.RigidBody) => boolean;
}

class RapierDebugRenderer {
    private filters: DebugRenderFilter = {
        showDynamicBodies: true,
        showStaticBodies: true,
        showColliders: true,
        showJoints: true,
        showAABB: false,
        bodyFilter: undefined
    };

    setFilter(filter: Partial<DebugRenderFilter>) {
        this.filters = { ...this.filters, ...filter };
    }

    private filterDebugData(vertices: Float32Array, colors: Float32Array): [Float32Array, Float32Array] {
        const bodies = this.world.bodies;
        let filteredVertices: number[] = [];
        let filteredColors: number[] = [];
        
        bodies.forEach((body, i) => {
            if (!this.shouldRenderBody(body)) return;
            
            // 바디에 해당하는 버텍스와 컬러 데이터의 인덱스 계산
            const startIdx = i * 24; // 예시: 바디당 8개의 버텍스
            const vertexData = vertices.slice(startIdx, startIdx + 24);
            const colorData = colors.slice(startIdx / 3 * 4, (startIdx + 24) / 3 * 4);
            
            filteredVertices.push(...vertexData);
            filteredColors.push(...colorData);
        });
        
        return [
            new Float32Array(filteredVertices),
            new Float32Array(filteredColors)
        ];
    }

    private shouldRenderBody(body: RAPIER.RigidBody): boolean {
        if (this.filters.bodyFilter && !this.filters.bodyFilter(body)) {
            return false;
        }
        
        if (body.isStatic() && !this.filters.showStaticBodies) {
            return false;
        }
        
        if (body.isDynamic() && !this.filters.showDynamicBodies) {
            return false;
        }
        
        return true;
    }
}

2.2 디버그 정보 오버레이

물리 객체의 상세 정보를 표시하는 오버레이 시스템을 구현해보겠습니다.

class PhysicsDebugOverlay {
    private container: HTMLDivElement;
    private labels: Map<number, HTMLDivElement> = new Map();
    private camera: THREE.Camera;
    private scene: THREE.Scene;
    
    constructor(camera: THREE.Camera, scene: THREE.Scene) {
        this.camera = camera;
        this.scene = scene;
        this.setupContainer();
    }
    
    private setupContainer() {
        this.container = document.createElement('div');
        this.container.style.position = 'absolute';
        this.container.style.top = '0';
        this.container.style.left = '0';
        this.container.style.pointerEvents = 'none';
        document.body.appendChild(this.container);
    }
    
    update(bodies: RAPIER.RigidBody[]) {
        bodies.forEach(body => {
            const position = body.translation();
            const velocity = body.linvel();
            const angularVel = body.angvel();
            
            // 3D 위치를 2D 스크린 좌표로 변환
            const screenPosition = new THREE.Vector3(
                position.x,
                position.y,
                position.z
            ).project(this.camera);
            
            let label = this.labels.get(body.handle);
            if (!label) {
                label = document.createElement('div');
                label.className = 'physics-debug-label';
                this.container.appendChild(label);
                this.labels.set(body.handle, label);
            }
            
            // 라벨 위치 업데이트
            const x = (screenPosition.x + 1) * window.innerWidth / 2;
            const y = (-screenPosition.y + 1) * window.innerHeight / 2;
            
            label.style.transform = `translate(${x}px, ${y}px)`;
            label.textContent = `
                속도: ${velocity.length().toFixed(2)} m/s
                각속도: ${angularVel.length().toFixed(2)} rad/s
                질량: ${body.mass().toFixed(2)} kg
            `;
        });
    }
}

2.3 성능 모니터링 시스템

디버그 렌더러의 성능을 모니터링하고 최적화하기 위한 시스템을 구현해보겠습니다.

class DebugPerformanceMonitor {
    private updateTimes: number[] = [];
    private maxSamples = 60; // 1초간의 샘플 (60fps 기준)
    
    private lastUpdateTime = 0;
    private frameCount = 0;
    
    update() {
        const currentTime = performance.now();
        const deltaTime = currentTime - this.lastUpdateTime;
        this.lastUpdateTime = currentTime;
        
        this.updateTimes.push(deltaTime);
        if (this.updateTimes.length > this.maxSamples) {
            this.updateTimes.shift();
        }
        
        this.frameCount++;
    }
    
    getStats() {
        const averageUpdateTime = this.updateTimes.reduce((a, b) => a + b, 0) / this.updateTimes.length;
        const maxUpdateTime = Math.max(...this.updateTimes);
        
        return {
            averageFrameTime: averageUpdateTime.toFixed(2),
            maxFrameTime: maxUpdateTime.toFixed(2),
            fps: Math.round(1000 / averageUpdateTime),
            frameCount: this.frameCount
        };
    }
}

// RapierDebugRenderer에 성능 모니터링 통합
class RapierDebugRenderer {
    private performanceMonitor = new DebugPerformanceMonitor();
    
    update() {
        const startTime = performance.now();
        
        // 기존 업데이트 로직
        if (this.enabled) {
            const { vertices, colors } = this.world.debugRender();
            // ... 렌더링 로직 ...
        }
        
        this.performanceMonitor.update();
        
        // 성능 정보 로깅 (개발 모드에서만)
        if (process.env.NODE_ENV === 'development') {
            const stats = this.performanceMonitor.getStats();
            console.debug('Debug Renderer Performance:', stats);
        }
    }
}

3. 실제 사용 예제

위에서 구현한 모든 기능들을 통합하여 사용하는 예제를 살펴보겠습니다.

// 초기화
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer();

// Rapier 월드 설정
const world = new RAPIER.World(new RAPIER.Vector3(0, -9.81, 0));

// 디버그 렌더러 초기화
const debugRenderer = new RapierDebugRenderer(scene, world);
const debugOverlay = new PhysicsDebugOverlay(camera, scene);

// 필터 설정
debugRenderer.setFilter({
    showStaticBodies: false,
    showAABB: true,
    bodyFilter: (body) => body.mass() > 1.0 // 1kg 이상의 물체만 표시
});

// 애니메이션 루프
function animate() {
    requestAnimationFrame(animate);
    
    // 물리 시뮬레이션 스텝
    world.step();
    
    // 디버그 정보 업데이트
    debugRenderer.update();
    debugOverlay.update(Array.from(world.bodies));
    
    renderer.render(scene, camera);
}

animate();

이러한 심화 구현을 통해 다음과 같은 이점을 얻을 수 있습니다.

1. 성능 최적화

  • Frustum Culling을 통한 렌더링 최적화
  • 메모리 사용량 감소
  • 효율적인 버퍼 관리

2. 개발 편의성 향상

  • 선택적 디버그 정보 표시
  • 실시간 성능 모니터링
  • 직관적인 물리 객체 정보 확인

3. 확장성

  • 필요에 따른 필터링 시스템
  • 커스텀 디버그 정보 표시
  • 성능 모니터링 및 최적화 도구

이러한 기능들은 특히 대규모 물리 시뮬레이션이나 게임 개발에서 매우 유용하게 활용될 수 있습니다.

profile
꾸준히, 의미있는 사이드 프로젝트 경험과 문제해결 과정을 기록하기 위한 공간입니다.

0개의 댓글