three.js 지형 데이터를 물리 엔진 트라이메시로 변환하기

요약

three.js场景의 Mesh를 순회하면서 geometry에서 index와 position 데이터를 추출하고, 이 두 배열을 기반으로 물리 엔진의 Trimesh를 생성한다.

배경

현재 웹 기반 MMORPG를 개발 중이며, three.js를 그래픽内核으로 사용하고 있다. 씬 asset은 glTF Binary(.glb) 파일 형식으로 로드한다.

요구사항

three.js는 OimoPhysics와 Ammo.js의 래퍼를 제공하지만, 이 래퍼들은 BoxGeometry, SphereGeometry, IcosahedronGeometry gibi 세 가지 특정 기하학만을 지원한다. 그러나 GLTFLoader로 가져오는 모델은 BufferGeometry이므로, 별도의 처리 없이는 물리 엔진과 호환되지 않는다. 따라서 물리 엔진에서对应的 Collision body를 생성할 방법이 필요했다.

개발 환경

  • three.js r147
  • 물리 엔진 (cannon-es 0.20.0 또는 @dimforge/rapier3d-compat 0.10.0 사용)

구현 과정

1. 기본 기하학 조합

最初的 시도는 기본 기하학들을 조합하여 지형을 만드는 것이었으나, ア트팀의否决를 받았다. 나 스스로도 이 방식을 선호하지 않았다.

2. Heightfield 방식

물리 엔진 문서와 데모를 찾아보니, 기본 기하학을 지면으로 사용하는 경우를 제외하면 대부분 Heightfield를 사용하여 지형을 구현하고 있다.

그러나 이 방식은 추가적인 높이 데이터가 필요하며, 생성, 저장, 읽기 과정이 필요하다. 또한 将来的에 멀티 레벨 Indoor 지형을 사용할 경우, 이를 지원하기 위해 추가 작업이 요구된다.

결국 이 방식은 채택하지 않았다.

3. Trimesh 방식

이 방식이 유일하게 적합한 해결책으로 보였다. 그러나 Trimesh를 구성하는 매개변수를 어디서 가져오는지가 오랜 기간 문제가 되었다.最終적으로 three.js 공식 문서와 glTF 레퍼런스 매뉴얼을 참고하여 해결책을 찾았다.

물리 엔진의 Trimesh는顶点 좌표 배열(vertices)과 vertex 인덱스 배열(indices)이 필요하다. three.js의 BufferGeometry에这两項이 포함되어 있다.

glTF 파일에서 로드한 BufferGeometry의 경우, BufferGeometry.attributes에 "position"이라는 이름의 BufferAttribute가 있는데,これが顶点 좌표 배열이다. BufferGeometry.getAttribute("position")으로 가져올 수 있다.

顶点 인덱스 배열은 BufferGeometry.attributes에 없으며, BufferGeometry.index 속성에 있다. 이 역시 BufferAttribute 타입이다.

위치, 회전, 크기 정보는 BufferGeometry가 속한 Mesh의 .getWorldPosition(), .getWorldQuaternion(), .getWorldScale() 메서드로 구할 수 있다. (정확히 말하면 이 세 메서드는 Object3D의 메서드다)

import { Quaternion, Vector3 } from "three";

let terrainAssetRoot; // 지형 asset의 루트 노드

// 모델 asset 로드......

terrainAssetRoot.traverse(node => {
  if (node.isMesh && node.geometry.index && node.geometry.hasAttribute("position")) {
    // 기하학 정보
    const geo = node.geometry;
    // vertex 인덱스 배열
    const indexArray = geo.index.array;
    // vertex 좌표 배열
    const vertexArray = geo.getAttribute("position").array;
    // 크기
    const scale = node.getWorldScale(new Vector3());
    // 위치
    const location = node.getWorldPosition(new Vector3());
    // 회전 (사원수)
    const rotation = node.getWorldQuaternion(new Quaternion());

    // 물리 세계의 Trimesh Collision body 구성......

  }
});

주의할 점: Rapier3D 같은 일부 물리 엔진은 크기 조정을 지원하지 않는다. 이러한 경우 vertex 좌표 배열에 먼저 크기 조정을 적용한 후 Trimesh를 생성해야 한다.

const posAttr = geometry.getAttribute("position");
const scaledVertices = new Float32Array(posAttr.array.length);
const worldScale = node.getWorldScale(new Vector3());

for (let i = 0; i < posAttr.count; i++) {
  scaledVertices[3 * i] = posAttr.array[3 * i] * worldScale.x;
  scaledVertices[(3 * i) + 1] = posAttr.array[(3 * i) + 1] * worldScale.y;
  scaledVertices[(3 * i) + 2] = posAttr.array[(3 * i) + 2] * worldScale.z;
}

태그: Three.js glTF Trimesh WebGL cannon-es

7월 23일 05:40에 게시됨