Three.js Rapier Debug Renderer

강정우·2025년 1월 13일
0

three.js

목록 보기
19/24
post-thumbnail

Rapier Debug Renderer

말 그대로 디버깅 하기위해 존재한다.

Geometry에 맞게 물리 도형이나 colliders를 만들려고 할 때, dimensions, positions, rotations 을 실제로 시각화할 수 없기 때문에 만들기가 까다롭다.

다행히 Rapier 에는 그래픽 렌더러가 없지만 색상을 사용하여 선을 그리는 데 사용할 수 있는 shape data 를 제공해준다.

const { vertices, colors } = world.debugRender()

vertices : 디버그 선을 나타내는 정점 데이터 (Float32Array)
colors : 각 정점의 색상 데이터 (Float32Array).

RapierDebugRenderer

Rapier.js와 Three.js를 사용하여 물리 시뮬레이션의 디버그 시각화를 처리하는 커스텀 클래스를 만들어보자.

class RapierDebugRenderer {
    mesh
    world
    enabled = true

    constructor(scene: THREE.Scene, world: RAPIER.World) {
        this.world = world
        this.mesh = new THREE.LineSegments(new THREE.BufferGeometry(), new THREE.LineBasicMaterial({ color: 0xffffff, vertexColors: true }))
        this.mesh.frustumCulled = false
        scene.add(this.mesh)
    }

    update() {
        if (this.enabled) {
            const { vertices, colors } = this.world.debugRender()
            this.mesh.geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3))
            this.mesh.geometry.setAttribute('color', new THREE.BufferAttribute(colors, 4))
            this.mesh.visible = true
        } else {
            this.mesh.visible = false
        }
    }
}

Properties

mesh : Rapier.js의 디버그 데이터를 시각화하기 위해 넣음. 추후 Line 을 상속받은 LineSegments 가 들어갈 예정.

world : Rapier.js의 RAPIER.World 객체. 물리 시뮬레이션 데이터를 가져오기 위해 참조

enabled : 디버그 렌더러의 활성화 여부를 나타내는 플래그. 기본값은 true.

Constructor

mesh : Rapier.js 에서 가져온 디버그 데이터를 렌더링하기 위한 LineSegments 객체. BufferGeometry: 디버그 데이터를 담는 3D 기하학적 데이터 구조. (default 값임)
LineBasicMaterial: 기본 선 재질로, 흰색(0xffffff)이며 꼭짓점 색상(vertexColors)을 활성화.

mesh.frustumCulled : 이 메쉬는 카메라의 시야(frustum) 밖에 있어도 항상 렌더링 됨.

scene.add(this.mesh) : Constructor 에서 TREE.Scene 에 디버그 메쉬를 추가.

update()

world.debugRender() : Rapier.js에서 디버그 데이터를 가져오는 이 포스팅의 핵심 메서드.

mesh.geometry.setAttribute() : 가져온 데이터를 THREE.BufferGeometry 에 설정.
position: 선 정점 위치 (3차원 좌표), color: 각 정점의 색상 (RGBA 값)

mesh.visible : 디버그 메쉬의 표출 유무.

즉, 커스텀 DebugRender 는 Rapier.js의 디버그 데이터를 Three.js로 렌더링하고,
Rapier.js에서 제공하는 충돌체 등의 디버그 데이터를 시각적으로 확인할 수 있도록 변환할 수 있도록 코드를 작성하였다.

RapierDebugRenderer class instantiate

// 레이피어 초기화
await RAPIER.init()
const gravity = new RAPIER.Vector3(0.0, -9.81, 0.0)
const world = new RAPIER.World(gravity)
// three.Object3D 와 rapier.RigidBody 동기화를 위한 배열
const dynamicBodies: [THREE.Object3D, RAPIER.RigidBody][] = []

const scene = new THREE.Scene()

// Scene 과 world 생성 후 RapierDebugRenderer 생성
const rapierDebugRenderer = new RapierDebugRenderer(scene, world)

...

function animate() {
    requestAnimationFrame(animate)

    delta = clock.getDelta()
    world.timestep = Math.min(delta, 0.1)
    world.step()

    rapierDebugRenderer.update()
    controls.update()
    renderer.render(scene, camera)
    stats.update()
}

animate()

RapierDebugRenderer 에서 작성한 .update 함수를 각 animate 마다 동작하여 RigidBody 데이털를 Line 에 업데이트 해줘야함.

convexHull vs trimesh

위 사진을 보면 convexHull vs trimesh 의 차이를 알 수 있다. 점, 선, 면을 봐도 오른쪽(triemsh) 가 훨씩 복잡하다는 것을 알 수 있다. 이는 용량을 보다 많이 먹는 것을 뜻한다.

왼쪽 원숭이 ( convexHull )

new OBJLoader().loadAsync('models/suzanne.obj').then((object) => {
    scene.add(object)
    const suzanneMesh = object.getObjectByName('Suzanne') as THREE.Mesh
    suzanneMesh.material = new THREE.MeshNormalMaterial()
    suzanneMesh.castShadow = true

    const suzanneBody = world.createRigidBody(RAPIER.RigidBodyDesc.dynamic().setTranslation(-1, 10, 0).setCanSleep(false))
    const points = new Float32Array(suzanneMesh.geometry.attributes.position.array)
    const suzanneShape = (RAPIER.ColliderDesc.convexHull(points) as RAPIER.ColliderDesc).setMass(1).setRestitution(0.5)
    world.createCollider(suzanneShape, suzanneBody)
    dynamicBodies.push([suzanneMesh, suzanneBody])
})

토러스 매듭 ( trimesh )

const torusKnotMesh = new THREE.Mesh(new THREE.TorusKnotGeometry(), new THREE.MeshNormalMaterial())
torusKnotMesh.castShadow = true
scene.add(torusKnotMesh)
const torusKnotBody = world.createRigidBody(RAPIER.RigidBodyDesc.dynamic().setTranslation(5, 5, 0))
const vertices = new Float32Array(torusKnotMesh.geometry.attributes.position.array)
let indices = new Uint32Array((torusKnotMesh.geometry.index as THREE.BufferAttribute).array)
const torusKnotShape = (RAPIER.ColliderDesc.trimesh(vertices, indices) as RAPIER.ColliderDesc).setMass(1).setRestitution(0.5)
world.createCollider(torusKnotShape, torusKnotBody)
dynamicBodies.push([torusKnotMesh, torusKnotBody])

trimesh 는 정점(Vertex) 배열과 인덱스(Index) 배열을 제공해야 한다. 이를 통해 정밀한 충돌 감지가 가능해지며, 특히 복잡한 모양의 객체에 적합하다.
다만 역시 복잡하면 성능이 더 낮을 수 있다.

그럼 원숭이를 trimesh 해보고 싶은데?
모델 중 geometry.index 가 없는 모델들이 있다 앞서 설명했듯 trimesh 는 정점과 인덱스가 필요한데 geometry.attributes.position 는 있지만 인덱스가 없는 경우가 있다.
이때 강제로 삼각형으로 분할(Triangulate)하는 과정을 거칠 수도 있고 혹은 그냥 인덱스도 함께 제공하는 모델을 사용하면 된다.

왼쪽 원숭이 ( trimesh )

new OBJLoader().loadAsync('models/suzanne.obj').then((object) => {
    scene.add(object);
    const suzanneMesh = object.getObjectByName('Suzanne') as THREE.Mesh;
    suzanneMesh.material = new THREE.MeshNormalMaterial();
    suzanneMesh.castShadow = true;

    // geometry를 가져오기
    let geometry = suzanneMesh.geometry;

    // geometry.index가 없는 경우 처리
    if (!geometry.index) {
        geometry = BufferGeometryUtils.mergeVertices(geometry); // 중복 정점 병합
        geometry = geometry.toNonIndexed(); // 비인덱스 -> 인덱스 변환
    }

    // 정점(Vertex) 및 인덱스(Index) 배열 추출
    const vertices = new Float32Array(geometry.attributes.position.array);
    const indexArray = geometry.index?.array as Uint32Array;

    if (!indexArray) {
        console.error("Failed to generate indexArray from geometry.");
        return;
    }

    // 물리 바디 생성
    const suzanneBody = world.createRigidBody(
        RAPIER.RigidBodyDesc.dynamic()
            .setTranslation(-1, 10, 0)
            .setCanSleep(false)
    );

    // Trimesh 생성
    const suzanneShape = RAPIER.ColliderDesc.trimesh(vertices, indexArray)
        .setMass(1)
        .setRestitution(0.5);

    // 콜라이더 생성
    world.createCollider(suzanneShape, suzanneBody);

    // 동기화용 배열에 추가
    dynamicBodies.push([suzanneMesh, suzanneBody]);
});
profile
智(지)! 德(덕)! 體(체)!

0개의 댓글