- Udemy의 Three.js and TypeScript 강의를 듣고 정리한 내용을 기록한다.
- 31. GLTF DRACO 부터 33. Rapier Debug Renderer 까지.
DRACO loader
- 3D Mesh를 압축하거나 압축을 풀기 위해 사용하는 오픈소스 라이브러리.
- 그동안 사용해온 GLTF 파일도 압축될 수 있으며, GLTFLoader를 통해 불러오는 것도 가능하다.
gltf-transform 설치
- gltf 파일 형식을 읽고, 수정할 수 있게 한다(참고)
- 강의에서는 모델 파일을 압축하기 위해 사용한다.
// 설치
yarn global add @gltf-transform/cli
// 예제 파일을 압축한다
gltf-transform optimize './public/models/eve$@walk.glb' './public/models/eve$@walk_compressed.glb' --compress draco --texture-compress webp
- 위 명령어는 Input으로 주어진 모델 파일을 Draco를 사용해서 압축한다.
- ‘draco —texture-compress webp’ 는 가장 흔하게 사용되는 옵션이라고.
- 명령어 실행 후 압축된 파일이 지정한 이름으로 생성되며, 용량을 확인해보면 확연히 차이가 나는 걸 확인할 수 있음(15 → 1mb)
- DRACO 옵션으로 압축된 모델은 DRACOLoader를 통해서만 가져와 사용할 수 있다.
DRACO loader 사용하기
- addons 에서 가져와 사용하는 것이 가능하다.
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('https://www.gstatic.com/draco/v1/decoders/');
const glTFLoader = new GLTFLoader();
glTFLoader.setDRACOLoader(dracoLoader);
- Draco로 압축된 모델을 three.js의 gltfLoader가 가져올 수 있게 하기 위해, DRACOLoader를 선언하고 이를 setDRACOLoader로 설정한다.
- setDecoderPath 메서드를 통해 압축을 풀 때 사용할 디코더 라이브러리의 경로를 설정한다.
모델을 압축한다고 해서 Scene이 더 빨라지는 것은 아니다. 압축된 파일을 Scene에 띄우기 위해서는 모델을 압축 해제하는 과정이 필요하기에 압축된 모델이 더 늦게 띄워질 가능성도 있기 때문이다.
따라서 모든 파일을 압축하기 보단, 압축 시의 이점이 클지 아닐지를 따져가며 압축 여부를 결정하는 것이 좋다.
Physics with Rapier
Rapier란?
- Rigid Body(리지드 바디, 강체)의 속도나 힘 등을 계산할 때 사용하는 물리 엔진이다.
- 이러한 조작은 게임이나 시뮬레이션 등에 유용하게 쓰일 수 있다.
- Three.js와는 별도의 엔진이지만 Three.js에 적용하는 것이 가능하다.
보통 Three.js 의 물리엔진 하면 cannon.js을 많이 쓰는 것 같았는데, 알아보니 아주 오래 전부터 업데이트가 멈춘 상태라 다른 물리 엔진을 적용하려는 사례가 많은 듯 하다. 이번 강의에서 다루는 Rapier는 비교적 새로운 물리 엔진 라이브러리라고.
설치
yarn add -D @dimforge/rapier3d-compat
@dimforge/rapier3d 을 설치하는 것도 가능한데, Vite에서 0.12.0 버전의 @dimforge/rapier3d가 잘 작동하지 않아서 대신 Combat 버전을 사용한다고.
Rapier 적용하기
import RAPIER from '@dimforge/rapier3d-compat';
// 일부 코드 생략
// combat 버전에서만 init 메서드가 필요하다.
await RAPIER.init();
const world = new RAPIER.World(gravity);
const dynamicBodies: [THREE.Object3D, RAPIER.RigidBody][] = [];
const cubeBody = world.createRigidBody(RAPIER.RigidBodyDesc.dynamic().setTranslation(0, 5, 0).setCanSleep(false));
const cubeShape = RAPIER.ColliderDesc.cuboid(0.5, 0.5, 0.5).setMass(1).setRestitution(1.1);
world.createCollider(cubeShape, cubeBody);
dynamicBodies.push([cubeMesh, cubeBody]);
// 중략
function animate() {
requestAnimationFrame(animate);
for (let i = 0, n = dynamicBodies.length; i < n; i++) {
dynamicBodies[i][0].position.copy(dynamicBodies[i][1].translation());
dynamicBodies[i][0].quaternion.copy(dynamicBodies[i][1].rotation());
}
controls.update();
renderer.render(scene, camera);
}
- RAPIER.World 생성자를 통해 리지드 바디들을 포함할 World 객체를 만든다.
- World.createRigidBody 메서드를 통해 생성한 World에 새 리지드 바디를 추가한다.
- 이 때, createRigidBody 메서드에 RigidBodyDesc 객체를 넣어야 한다.
- RigidBodyDesc.dynamic() 메서드를 통해 해당 리지드 바디가 외부의 힘의 영향을 받을 수 있게 한다(즉, 힘을 받으면 움직일 수 있다).
- 반대로, RigidBodyDesc.fixed() 를 적용하면 외부의 힘이 가해져도 영향을 받지 않게 된다(힘을 받아도 움직이지 않는다).
- setCanSleep 메서드는 물리 관련 설정이 바뀌었을 때, 이것이 바로 적용될지 아닐지를 나타낸다. false로 설정할 경우 변경값이 바로 적용된다.
- RAPIER.ColliderDesc 를 통해 Collider를 설정한다.
- Collider란, 해당 물체로의 접촉을 감지할 수 있도록 만들어진 기하학적 모양 혹은 형상(shape)을 의미한다.
- 이걸 설정하면 외부의 힘에 의해 리지드 바디가 영향 받을 수 있게 한다.
- cuboid를 통해 Collider의 모양을 육면체로 설정한다. 이 외에도 다양한 모양이 존재한다.
- setMass를 통해 질량을 설정한다.
- setRestitution 메서드를 통해 얼마나 튕길지 그 정도를 결정한다.
- 위에서 만든 RigidBody와 Collider를 world.createCollider로 결합하여 world에 추가한다.
- Animation Loop에서 Rapier에 의해 변화하는 위치 좌표와 회전값을 Three.js의 Object3D 객체에 적용시키며 물리 엔진이 적용된 움직임을 만들어낸다.
+) Collider를 복잡한 Mesh에 적용하기 위해서는?
- Collider를 적용하고자 하는 Mesh를 이루는 각 정점 정보를 사용해 Collider를 만들 수 있다.
import RAPIER from '@dimforge/rapier3d-compat';
import * as THREE from 'three';
const torusKnotMesh = new THREE.Mesh(new THREE.TorusKnotGeometry(), new THREE.MeshNormalMaterial());
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(1.1);
- 위 코드에서는 정점 정보를 받아 이를 TriMesh 형태로 그려낸다.
- TriMesh는 모델의 폴리곤을 삼각형 형태로 그려내는 것을 의미한다.
- TriMesh 외에도 다른 옵션을 선택할 수 있다.
- 예를 들어 convexHull은, 해당 정점들 중 일부를 이용해 만들 수 있는 모든 점을 포함하는 다각형을 의미한다. 다소 정밀도는 떨어지지만 가장 간단한 방법이다.
Rapier Debug Renderer
- Rapier를 사용할 때, 시각적으로 보이지 않기 때문에 개발에 어려움을 겪거나 실수를 할 수 있다.
- 이러한 실수를 방지하기 위해 Collider나 Shape를 이루는 정점 배열 내 점들을 연결하여 시각적으로 보이게 할 수 있다.
Rapier 공식 문서에 따르면 Rapier 내부적으로 구현된 렌더링 기능이 없기 때문에, 개발자가 알아서 구현해야 한다. 다만, World 객체를 통해 내부에 존재하는 물체 혹은 Collider 들의 정점 및 색상 정보를 알 수는 있다. 이 때 debugRender() 메서드를 사용한다.
import RAPIER from '@dimforge/rapier3d-compat';
import * as THREE from 'three';
const mesh = new THREE.LineSegments(new THREE.BufferGeometry(), new THREE.LineBasicMaterial({ color: 0xffffff, vertexColors: true }));
const world = new RAPIER.World(gravity);
const { vertices, colors } = world.debugRender();
mesh.geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
mesh.geometry.setAttribute('color', new THREE.BufferAttribute(colors, 4));
- world.debugRender 메서드를 통해 정점 및 색상 정보를 가져온다.
- mesh에 LineSegments 객체를 생성한다.
- LineSegments 객체는 주어진 정점들 사이를 연결하는 선들의 집합을 나타낸다.
- 생성자는 인자로 Geometry 객체와 Material 객체를 받는다.
- 생성한 LineSegments mesh에 setAttribute로 world에서 받은 정점 및 색상 정보를 할당한다.
+) OBJLoader
- 그동안 강의에서 사용한 GLTFLoader처럼 모델을 로딩하는데 사용하는 로더 클래스이다.
- obj 파일 형식의 모델을 로딩할 때 사용한다.
.obj : 사람이 읽을 수 있는 형태로 3D 기하학 구조의 각 정점, 텍스처 좌표의 UV 위치 등을 표현한 간단한 데이터 포맷이라고 한다. Three.js에서는 addons를 통해 사용한다.
- OBJLoader를 통해 불러온 모델은 기본적으로 MeshPhoneMaterial로 불러와진다.
- 강의에서 gltf-transform을 글로벌로 설치하는데, 어째선지 내 노트북에 yarn global 관련 설정이 되어있지 않아 약간 애먹었다.
- 아래의 공식 문서를 보고 해결. yarn global의 경로를 PATH에 추가하여 해결했다.
'공부 > etc' 카테고리의 다른 글
Three.js and TypeScript (10) (0) | 2024.11.25 |
---|---|
Three.js and TypeScript (9) (0) | 2024.11.22 |
Three.js and TypeScript (8) (1) | 2024.11.21 |
Three.js and TypeScript (7) (2) | 2024.11.20 |
Three.js and TypeScript (6) (0) | 2024.11.19 |