
게임에서 가장 기본이 되는 캐릭터 컨트롤러를 Rapier를 사용하여 구현해보겠습니다.
class CharacterController {
private characterBody: RAPIER.RigidBody;
private characterCollider: RAPIER.Collider;
private mesh: THREE.Mesh;
private isGrounded: boolean = false;
private jumpForce: number = 5;
private moveSpeed: number = 3;
constructor(world: RAPIER.World, position: THREE.Vector3) {
// 캐릭터 메시 생성
const geometry = new THREE.CapsuleGeometry(0.5, 1, 4, 8);
const material = new THREE.MeshPhongMaterial({ color: 0x00ff00 });
this.mesh = new THREE.Mesh(geometry, material);
this.mesh.castShadow = true;
// 물리 바디 생성
const bodyDesc = RAPIER.RigidBodyDesc.dynamic()
.setTranslation(position.x, position.y, position.z)
.setLinearDamping(0.1)
.setAngularDamping(0.5);
this.characterBody = world.createRigidBody(bodyDesc);
// 콜라이더 생성 (캡슐 형태)
const colliderDesc = RAPIER.ColliderDesc.capsule(0.75, 0.5)
.setFriction(0.2)
.setRestitution(0.0);
this.characterCollider = world.createCollider(colliderDesc, this.characterBody);
}
update() {
// 지면 검사
const position = this.characterBody.translation();
const rayDir = { x: 0, y: -1, z: 0 };
const ray = new RAPIER.Ray(position, rayDir);
const hit = world.castRay(ray, 1.0, true);
this.isGrounded = hit !== null && hit.toi <= 0.2;
// 메시 위치 동기화
this.mesh.position.copy(this.characterBody.translation());
this.mesh.quaternion.copy(this.characterBody.rotation());
}
jump() {
if (this.isGrounded) {
this.characterBody.applyImpulse(new RAPIER.Vector3(0, this.jumpForce, 0), true);
}
}
move(direction: THREE.Vector3) {
const moveImpulse = direction.multiplyScalar(this.moveSpeed);
this.characterBody.applyImpulse(
new RAPIER.Vector3(moveImpulse.x, 0, moveImpulse.z),
true
);
}
}
자동차나 기타 차량의 물리를 구현하는 예제입니다.
class VehicleSystem {
private chassis: RAPIER.RigidBody;
private wheels: Array<{
body: RAPIER.RigidBody;
joint: RAPIER.RevoluteJoint;
}> = [];
private engineForce: number = 20;
private brakingForce: number = 10;
private maxSteerAngle: number = 0.5;
constructor(world: RAPIER.World, position: THREE.Vector3) {
// 차체 생성
const chassisDesc = RAPIER.RigidBodyDesc.dynamic()
.setTranslation(position.x, position.y, position.z)
.setLinearDamping(0.1);
this.chassis = world.createRigidBody(chassisDesc);
// 차체 콜라이더
const chassisCollider = RAPIER.ColliderDesc.cuboid(2.0, 0.5, 4.0);
world.createCollider(chassisCollider, this.chassis);
// 바퀴 생성 및 조인트 연결
const wheelPositions = [
{ x: -1, y: -0.5, z: 1.5 }, // 앞왼쪽
{ x: 1, y: -0.5, z: 1.5 }, // 앞오른쪽
{ x: -1, y: -0.5, z: -1.5 }, // 뒤왼쪽
{ x: 1, y: -0.5, z: -1.5 }, // 뒤오른쪽
];
wheelPositions.forEach(pos => {
const wheelDesc = RAPIER.RigidBodyDesc.dynamic()
.setTranslation(
position.x + pos.x,
position.y + pos.y,
position.z + pos.z
);
const wheelBody = world.createRigidBody(wheelDesc);
const wheelCollider = RAPIER.ColliderDesc.cylinder(0.4, 0.2)
.setFriction(0.8)
.setDensity(1.0);
world.createCollider(wheelCollider, wheelBody);
// 회전 조인트 생성
const jointParams = RAPIER.JointParams.revolute(
{ x: 0, y: 0, z: 0 },
{ x: 1, y: 0, z: 0 }
);
const joint = world.createJoint(jointParams, this.chassis, wheelBody);
this.wheels.push({ body: wheelBody, joint: joint });
});
}
update(steering: number, throttle: number, brake: number) {
// 조향
const steerAngle = steering * this.maxSteerAngle;
this.wheels[0].joint.setMotorPosition(steerAngle, 0.1);
this.wheels[1].joint.setMotorPosition(steerAngle, 0.1);
// 가속/제동
const force = throttle * this.engineForce - brake * this.brakingForce;
this.wheels.forEach(wheel => {
wheel.body.applyForce(new RAPIER.Vector3(0, 0, force), true);
});
}
}
물체가 충격을 받았을 때 파괴되는 효과를 구현합니다.
class DestructibleSystem {
private pieces: Array<{
body: RAPIER.RigidBody;
mesh: THREE.Mesh;
health: number;
}> = [];
private breakForceThreshold: number = 50;
constructor(world: RAPIER.World, scene: THREE.Scene) {
// 파괴 가능한 벽 생성
const wallSize = { width: 4, height: 3, depth: 0.3 };
const pieceSize = 0.4;
for (let y = 0; y < wallSize.height; y += pieceSize) {
for (let x = 0; x < wallSize.width; x += pieceSize) {
const position = new THREE.Vector3(x, y, 0);
// 각 조각의 메시 생성
const geometry = new THREE.BoxGeometry(pieceSize, pieceSize, wallSize.depth);
const material = new THREE.MeshPhongMaterial({ color: 0x808080 });
const mesh = new THREE.Mesh(geometry, material);
mesh.position.copy(position);
scene.add(mesh);
// 물리 바디 생성
const bodyDesc = RAPIER.RigidBodyDesc.dynamic()
.setTranslation(position.x, position.y, position.z);
const body = world.createRigidBody(bodyDesc);
// 콜라이더 생성
const colliderDesc = RAPIER.ColliderDesc.cuboid(
pieceSize/2, pieceSize/2, wallSize.depth/2
);
world.createCollider(colliderDesc, body);
// 조각 정보 저장
this.pieces.push({
body,
mesh,
health: 100
});
}
}
// 충돌 이벤트 리스너 설정
world.addEventListener('collisionstart', (event) => {
this.handleCollision(event);
});
}
private handleCollision(event: any) {
const { collider1, collider2 } = event;
const impulse = event.impulse.norm();
if (impulse > this.breakForceThreshold) {
// 충돌한 조각 찾기
const piece = this.pieces.find(p =>
p.body === collider1.parent() || p.body === collider2.parent()
);
if (piece) {
piece.health -= impulse;
// 체력이 0 이하면 파괴
if (piece.health <= 0) {
this.destroyPiece(piece);
} else {
// 손상 시각화
const damage = 1 - (piece.health / 100);
(piece.mesh.material as THREE.MeshPhongMaterial).color.setRGB(
0.5 + damage * 0.5,
0.5 - damage * 0.3,
0.5 - damage * 0.3
);
}
}
}
}
private destroyPiece(piece: typeof this.pieces[0]) {
// 파괴 효과 생성
const fragments = this.createFragments(piece.mesh.position);
fragments.forEach(fragment => {
const direction = fragment.position.clone().sub(piece.mesh.position);
fragment.body.applyImpulse(
new RAPIER.Vector3(
direction.x * 2,
direction.y * 2,
direction.z * 2
),
true
);
});
// 원래 조각 제거
piece.mesh.parent?.remove(piece.mesh);
const index = this.pieces.indexOf(piece);
if (index > -1) {
this.pieces.splice(index, 1);
}
}
private createFragments(position: THREE.Vector3) {
const fragments = [];
const fragmentCount = 4;
const fragmentSize = 0.2;
for (let i = 0; i < fragmentCount; i++) {
const offset = new THREE.Vector3(
(Math.random() - 0.5) * 0.2,
(Math.random() - 0.5) * 0.2,
(Math.random() - 0.5) * 0.2
);
const fragmentPos = position.clone().add(offset);
const geometry = new THREE.TetrahedronGeometry(fragmentSize);
const material = new THREE.MeshPhongMaterial({ color: 0x606060 });
const mesh = new THREE.Mesh(geometry, material);
mesh.position.copy(fragmentPos);
const bodyDesc = RAPIER.RigidBodyDesc.dynamic()
.setTranslation(fragmentPos.x, fragmentPos.y, fragmentPos.z);
const body = world.createRigidBody(bodyDesc);
const colliderDesc = RAPIER.ColliderDesc.ball(fragmentSize);
world.createCollider(colliderDesc, body);
fragments.push({ body, mesh });
}
return fragments;
}
update() {
// 물리 상태를 메시에 동기화
this.pieces.forEach(piece => {
piece.mesh.position.copy(piece.body.translation());
piece.mesh.quaternion.copy(piece.body.rotation());
});
}
}
class PhysicsDebugger {
private debugMeshes: THREE.LineSegments[] = [];
private isEnabled: boolean = false;
constructor(private world: RAPIER.World, private scene: THREE.Scene) {}
toggle() {
this.isEnabled = !this.isEnabled;
if (!this.isEnabled) {
this.clearDebugMeshes();
}
}
update() {
if (!this.isEnabled) return;
this.clearDebugMeshes();
// 콜라이더 형상 시각화
const colliders = this.world.colliders();
colliders.forEach(collider => {
const shape = collider.shape;
const position = collider.translation();
const rotation = collider.rotation();
const geometry = this.createDebugGeometry(shape);
const material = new THREE.LineBasicMaterial({
color: collider.parent().isDynamic() ? 0xff0000 : 0x00ff00
});
const mesh = new THREE.LineSegments(geometry, material);
mesh.position.copy(position);
mesh.quaternion.copy(rotation);
this.scene.add(mesh);
this.debugMeshes.push(mesh);
});
// 성능 모니터링
const stats = {
bodies: this.world.bodies.len(),
colliders: this.world.colliders.len(),
contacts: this.world.contactPairs().length,
islands: this.world.islands().length
};
console.table(stats);
}
private createDebugGeometry(shape: RAPIER.Shape): THREE.BufferGeometry {
// 형상에 따른 와이어프레임 지오메트리 생성
switch(shape.type) {
case RAPIER.ShapeType.Cuboid:
const cuboid = shape as RAPIER.Cuboid;
return new THREE.BoxGeometry(
cuboid.halfExtents.x * 2,
cuboid.halfExtents.y * 2,
cuboid.halfExtents.z * 2
).toWireframe();
case RAPIER.ShapeType.Ball:
const ball = shape as RAPIER.Ball;
return new THREE.SphereGeometry(ball.radius)
.toWireframe();
// 다른 형상들에 대한 처리...
default:
return new THREE.BoxGeometry(1, 1, 1).toWireframe();
}
}
private clearDebugMeshes() {
this.debugMeshes.forEach(mesh => {
this.scene.remove(mesh);
mesh.geometry.dispose();
(mesh.material as THREE.Material).dispose();
});
this.debugMeshes = [];
}
}
// 물리 엔진 성능 최적화를 위한 매니저 클래스
class PhysicsOptimizationManager {
private activeRegion: THREE.Box3;
private sleepingBodies: Set<RAPIER.RigidBody> = new Set();
constructor(
private world: RAPIER.World,
private camera: THREE.Camera,
private regionSize: number = 20
) {
this.activeRegion = new THREE.Box3();
this.updateActiveRegion();
}
updateActiveRegion() {
const cameraPosition = this.camera.position;
this.activeRegion.setFromCenterAndSize(
cameraPosition,
new THREE.Vector3(
this.regionSize,
this.regionSize,
this.regionSize
)
);
}
update() {
this.updateActiveRegion();
// 활성 영역 기반 최적화
this.world.bodies.forEach(body => {
if (body.isFixed()) return;
const position = body.translation();
const bodyPosition = new THREE.Vector3(
position.x,
position.y,
position.z
);
if (this.activeRegion.containsPoint(bodyPosition)) {
// 활성 영역 내부의 바디는 깨우기
if (this.sleepingBodies.has(body)) {
body.wakeUp();
this.sleepingBodies.delete(body);
}
} else {
// 활성 영역 외부의 바디는 재우기
if (!this.sleepingBodies.has(body) && !body.isSleeping()) {
body.sleep();
this.sleepingBodies.add(body);
}
}
});
}
// 브로드페이즈 최적화를 위한 공간 분할
setupSpatialHashing() {
const cellSize = 5.0; // 그리드 셀 크기
this.world.setupSpatialHashing(cellSize, cellSize, cellSize);
}
}
// 물리 시뮬레이션 품질과 성능을 조정하는 설정 관리자
class PhysicsQualityManager {
private qualityPresets = {
low: {
substeps: 1,
maxVelocityIterations: 2,
maxPositionIterations: 1
},
medium: {
substeps: 2,
maxVelocityIterations: 4,
maxPositionIterations: 2
},
high: {
substeps: 4,
maxVelocityIterations: 8,
maxPositionIterations: 4
}
};
constructor(private world: RAPIER.World) {}
setQuality(preset: 'low' | 'medium' | 'high') {
const settings = this.qualityPresets[preset];
this.world.substeps = settings.substeps;
this.world.maxVelocityIterations = settings.maxVelocityIterations;
this.world.maxPositionIterations = settings.maxPositionIterations;
}
// 동적 품질 조정
adaptiveQuality(fps: number) {
if (fps < 30) {
this.setQuality('low');
} else if (fps < 50) {
this.setQuality('medium');
} else {
this.setQuality('high');
}
}
}
// 실제 사용 예시
class PhysicsWorld {
private world: RAPIER.World;
private debugger: PhysicsDebugger;
private optimizer: PhysicsOptimizationManager;
private qualityManager: PhysicsQualityManager;
private lastTime: number = 0;
private frameCount: number = 0;
private fpsUpdateInterval: number = 1000; // 1초마다 FPS 계산
constructor(scene: THREE.Scene, camera: THREE.Camera) {
this.world = new RAPIER.World(new RAPIER.Vector3(0, -9.81, 0));
this.debugger = new PhysicsDebugger(this.world, scene);
this.optimizer = new PhysicsOptimizationManager(this.world, camera);
this.qualityManager = new PhysicsQualityManager(this.world);
// 초기 설정
this.optimizer.setupSpatialHashing();
this.qualityManager.setQuality('medium');
}
update(currentTime: number) {
// FPS 계산 및 품질 조정
this.frameCount++;
if (currentTime - this.lastTime >= this.fpsUpdateInterval) {
const fps = (this.frameCount * 1000) / (currentTime - this.lastTime);
this.qualityManager.adaptiveQuality(fps);
this.frameCount = 0;
this.lastTime = currentTime;
}
// 최적화 및 디버깅 업데이트
this.optimizer.update();
this.debugger.update();
// 물리 시뮬레이션 스텝
this.world.step();
}
}
// GUI 컨트롤 추가
function setupPhysicsGUI(physicsWorld: PhysicsWorld) {
const gui = new GUI();
const debugFolder = gui.addFolder('Physics Debug');
debugFolder.add({
toggleDebug: () => physicsWorld.debugger.toggle()
}, 'toggleDebug').name('Toggle Debug View');
const qualityFolder = gui.addFolder('Physics Quality');
qualityFolder.add({
quality: 'medium'
}, 'quality', ['low', 'medium', 'high'])
.onChange((value) => physicsWorld.qualityManager.setQuality(value));
const performanceFolder = gui.addFolder('Performance');
performanceFolder.add(physicsWorld.optimizer, 'regionSize', 10, 50)
.name('Active Region Size');
}
천이나 로프와 같은 부드러운 물체를 시뮬레이션하는 시스템을 구현해보겠습니다.
class SoftBodySystem {
private particles: RAPIER.RigidBody[] = [];
private constraints: RAPIER.Joint[] = [];
private meshes: THREE.Mesh[] = [];
constructor(
private world: RAPIER.World,
private scene: THREE.Scene,
private config: {
width: number;
height: number;
spacing: number;
stiffness: number;
damping: number;
}
) {
this.initializeCloth();
}
private initializeCloth() {
const { width, height, spacing } = this.config;
// 파티클 생성
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const position = new RAPIER.Vector3(
x * spacing,
y * spacing,
0
);
// 물리 바디 생성
const bodyDesc = RAPIER.RigidBodyDesc.dynamic()
.setTranslation(position.x, position.y, position.z)
.setLinearDamping(this.config.damping);
const body = this.world.createRigidBody(bodyDesc);
// 콜라이더 생성
const colliderDesc = RAPIER.ColliderDesc.ball(spacing * 0.1)
.setDensity(1.0);
this.world.createCollider(colliderDesc, body);
// 상단 고정점 설정
if (y === height - 1) {
body.setBodyType(RAPIER.RigidBodyType.Fixed);
}
this.particles.push(body);
// 시각화를 위한 메시 생성
const geometry = new THREE.SphereGeometry(spacing * 0.1);
const material = new THREE.MeshPhongMaterial({
color: 0x2194ce
});
const mesh = new THREE.Mesh(geometry, material);
this.scene.add(mesh);
this.meshes.push(mesh);
}
}
// 스프링 제약조건 생성
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const i = y * width + x;
// 수평 제약조건
if (x < width - 1) {
this.createSpringConstraint(
this.particles[i],
this.particles[i + 1]
);
}
// 수직 제약조건
if (y < height - 1) {
this.createSpringConstraint(
this.particles[i],
this.particles[i + width]
);
}
// 대각선 제약조건
if (x < width - 1 && y < height - 1) {
this.createSpringConstraint(
this.particles[i],
this.particles[i + width + 1]
);
this.createSpringConstraint(
this.particles[i + 1],
this.particles[i + width]
);
}
}
}
}
private createSpringConstraint(body1: RAPIER.RigidBody, body2: RAPIER.RigidBody) {
const params = RAPIER.JointParams.spring(
{ x: 0, y: 0, z: 0 },
{ x: 0, y: 0, z: 0 },
this.config.stiffness
);
const joint = this.world.createJoint(params, body1, body2);
this.constraints.push(joint);
}
applyWind(direction: THREE.Vector3, strength: number) {
this.particles.forEach(particle => {
if (particle.bodyType() !== RAPIER.RigidBodyType.Fixed) {
const force = new RAPIER.Vector3(
direction.x * strength,
direction.y * strength,
direction.z * strength
);
particle.applyForce(force, true);
}
});
}
update() {
// 파티클 위치 업데이트
for (let i = 0; i < this.particles.length; i++) {
const position = this.particles[i].translation();
this.meshes[i].position.copy(position);
}
}
}
위에서 구현한 모든 시스템을 통합하여 사용하는 예시입니다.
// 메인 애플리케이션 클래스
class PhysicsApplication {
private scene: THREE.Scene;
private camera: THREE.PerspectiveCamera;
private renderer: THREE.WebGLRenderer;
private physicsWorld: PhysicsWorld;
private characterController: CharacterController;
private vehicleSystem: VehicleSystem;
private destructibleSystem: DestructibleSystem;
private softBodySystem: SoftBodySystem;
private clock: THREE.Clock;
constructor() {
// Three.js 초기화
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.renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(this.renderer.domElement);
// 물리 시스템 초기화
this.physicsWorld = new PhysicsWorld(this.scene, this.camera);
// 각 서브시스템 초기화
this.characterController = new CharacterController(
this.physicsWorld.world,
new THREE.Vector3(0, 5, 0)
);
this.vehicleSystem = new VehicleSystem(
this.physicsWorld.world,
new THREE.Vector3(10, 2, 0)
);
this.destructibleSystem = new DestructibleSystem(
this.physicsWorld.world,
this.scene
);
this.softBodySystem = new SoftBodySystem(
this.physicsWorld.world,
this.scene,
{
width: 10,
height: 10,
spacing: 0.5,
stiffness: 100,
damping: 0.1
}
);
// 이벤트 리스너 설정
this.setupEventListeners();
// 애니메이션 시작
this.clock = new THREE.Clock();
this.animate();
}
private setupEventListeners() {
// 키보드 입력 처리
document.addEventListener('keydown', (event) => {
switch(event.code) {
case 'Space':
this.characterController.jump();
break;
case 'ArrowUp':
this.vehicleSystem.update(0, 1, 0);
break;
case 'ArrowDown':
this.vehicleSystem.update(0, 0, 1);
break;
case 'ArrowLeft':
this.vehicleSystem.update(-1, 0, 0);
break;
case 'ArrowRight':
this.vehicleSystem.update(1, 0, 0);
break;
}
});
// 바람 효과
setInterval(() => {
const windDirection = new THREE.Vector3(
Math.sin(Date.now() * 0.001),
0,
Math.cos(Date.now() * 0.001)
);
this.softBodySystem.applyWind(windDirection, 0.5);
}, 100);
}
private animate() {
requestAnimationFrame(() => this.animate());
const delta = this.clock.getDelta();
const elapsedTime = this.clock.elapsedTime;
// 각 시스템 업데이트
this.physicsWorld.update(elapsedTime * 1000);
this.characterController.update();
this.vehicleSystem.update(0, 0, 0); // 입력이 없을 때는 0으로
this.destructibleSystem.update();
this.softBodySystem.update();
// 렌더링
this.renderer.render(this.scene, this.camera);
}
}
// 애플리케이션 시작
new PhysicsApplication();
실제 프로덕션에서 Rapier를 사용할 때 고려해야 할 성능 최적화 방법들입니다.
class MemoryManager {
private objectPool: Map<string, any[]> = new Map();
private maxPoolSize: number = 100;
// 객체 풀링 구현
acquire<T>(type: string, createFn: () => T): T {
if (!this.objectPool.has(type)) {
this.objectPool.set(type, []);
}
const pool = this.objectPool.get(type)!;
if (pool.length > 0) {
return pool.pop()!;
}
return createFn();
}
release(type: string, object: any) {
const pool = this.objectPool.get(type)!;
if (pool.length < this.maxPoolSize) {
pool.push(object);
}
}
// WASM 메모리 최적화
optimizeWasmMemory() {
// Rapier의 WASM 힙 메모리 정리
if (typeof global.gc === 'function') {
global.gc();
}
}
}
class CollisionOptimizer {
private collisionGroups: Map<number, number> = new Map();
constructor(private world: RAPIER.World) {
this.setupCollisionGroups();
}
private setupCollisionGroups() {
// 충돌 그룹 정의
const groups = {
STATIC: 0x0001,
CHARACTER: 0x0002,
VEHICLE: 0x0004,
DEBRIS: 0x0008,
SENSOR: 0x0010
};
// 충돌 매트릭스 설정
const collisionMatrix = [
{ group: groups.STATIC, collidesWith: groups.CHARACTER | groups.VEHICLE },
{ group: groups.CHARACTER, collidesWith: groups.STATIC | groups.VEHICLE },
{ group: groups.VEHICLE, collidesWith: groups.STATIC | groups.CHARACTER },
{ group: groups.DEBRIS, collidesWith: groups.STATIC },
{ group: groups.SENSOR, collidesWith: groups.CHARACTER }
];
collisionMatrix.forEach(entry => {
this.collisionGroups.set(entry.group, entry.collidesWith);
});
}
applyCollisionFilter(collider: RAPIER.Collider, groupType: string) {
const group = this.collisionGroups.get(groupType);
if (group) {
collider.setCollisionGroups(group);
}
}
}
class PhysicsWorkerManager {
private worker: Worker;
private messageQueue: Array<{
resolve: (value: any) => void;
reject: (reason: any) => void;
}> = [];
constructor() {
// 워커 생성 및 초기화
this.worker = new Worker(new URL('./physics.worker.ts', import.meta.url));
this.setupWorkerHandlers();
}
private setupWorkerHandlers() {
this.worker.onmessage = (event) => {
const { type, data, error } = event.data;
const promise = this.messageQueue.shift();
if (promise) {
if (error) {
promise.reject(error);
} else {
promise.resolve(data);
}
}
};
}
async simulatePhysics(bodies: any[], delta: number): Promise<any> {
return new Promise((resolve, reject) => {
this.messageQueue.push({ resolve, reject });
this.worker.postMessage({
type: 'simulate',
bodies,
delta
});
});
}
}
class PhysicsLODSystem {
private lodLevels: Map<RAPIER.RigidBody, number> = new Map();
private camera: THREE.Camera;
constructor(camera: THREE.Camera) {
this.camera = camera;
}
updateLOD(body: RAPIER.RigidBody, position: THREE.Vector3) {
const distance = this.camera.position.distanceTo(position);
let lodLevel = this.calculateLODLevel(distance);
if (this.lodLevels.get(body) !== lodLevel) {
this.applyLODSettings(body, lodLevel);
this.lodLevels.set(body, lodLevel);
}
}
private calculateLODLevel(distance: number): number {
if (distance < 10) return 0; // 높은 정밀도
if (distance < 30) return 1; // 중간 정밀도
return 2; // 낮은 정밀도
}
private applyLODSettings(body: RAPIER.RigidBody, level: number) {
switch (level) {
case 0:
// 높은 정밀도 설정
body.setAngularDamping(0.1);
body.setLinearDamping(0.1);
break;
case 1:
// 중간 정밀도 설정
body.setAngularDamping(0.5);
body.setLinearDamping(0.5);
break;
case 2:
// 낮은 정밀도 설정
body.setAngularDamping(1.0);
body.setLinearDamping(1.0);
body.sleep(); // 멀리 있는 객체는 슬립
break;
}
}
}
이러한 최적화 기법들을 적절히 조합하여 사용하면 대규모 물리 시뮬레이션에서도 안정적인 성능을 확보할 수 있습니다.
Rapier 물리 엔진은 강력한 기능을 제공하지만, 실제 프로덕션 환경에서는 성능과 안정성을 위한 세심한 최적화가 필요합니다.
위의 예제들을 기반으로 하여 프로젝트의 요구사항에 맞게 수정하고 확장하시면 됩니다.
특히 대규모 시뮬레이션이나 복잡한 상호작용이 필요한 경우, 위에서 다룬 최적화 기법들을 적절히 조합하여 사용하는 것이 중요합니다.
또한 항상 성능 모니터링을 통해 병목 지점을 파악하고 지속적으로 최적화하는 것이 좋습니다.