기존 코드에서는 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();
}
}
}
물리 시뮬레이션이 실행되는 동안 매 프레임마다 새로운 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;
}
}
특정 유형의 물리 객체만 선택적으로 렌더링할 수 있는 필터 시스템을 구현해보겠습니다.
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;
}
}
물리 객체의 상세 정보를 표시하는 오버레이 시스템을 구현해보겠습니다.
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
`;
});
}
}
디버그 렌더러의 성능을 모니터링하고 최적화하기 위한 시스템을 구현해보겠습니다.
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);
}
}
}
위에서 구현한 모든 기능들을 통합하여 사용하는 예제를 살펴보겠습니다.
// 초기화
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();
이러한 심화 구현을 통해 다음과 같은 이점을 얻을 수 있습니다.
이러한 기능들은 특히 대규모 물리 시뮬레이션이나 게임 개발에서 매우 유용하게 활용될 수 있습니다.