Three.js ๐Ÿš— Car ๋กœ ๋ณต์Šต

๊ฐ•์ •์šฐยท2025๋…„ 1์›” 15์ผ
0

three.js

๋ชฉ๋ก ๋ณด๊ธฐ
22/24
post-thumbnail

๐Ÿš— Car ๋กœ ๋ณต์Šต

1. ํด๋ž˜์Šค ์†์„ฑ & ์ƒ์„ฑ์ž ํ•จ์ˆ˜

class Car {
    dynamicBodies: [THREE.Object3D, RAPIER.RigidBody][] = []

    constructor() {}
        

์†์„ฑ

dynamicBodies
THREE.Mesh ์™€ Rapier.RigidBody ๋ฅผ ์Œ์œผ๋กœ ์ €์žฅํ•˜๋Š” 2์ฐจ์› ๋ฐฐ์—ด.
THREE.Mesh ์˜ position ์™€ quaternion ์„ ๋ฌผ๋ฆฌ ์—”์ง„์˜ ๋ฐ์ดํ„ฐ( translation ๊ณผ rotation ) ์™€ ๋™๊ธฐํ™”ํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋œ๋‹ค.
quaternion : 3D ๊ณต๊ฐ„์—์„œ ์–ด๋–ป๊ฒŒ ํšŒ์ „ํ–ˆ๋Š”์ง€๋ฅผ ํ‘œํ˜„ํ•˜๋Š” ์†์„ฑ๊ฐ’.

์ƒ์„ฑ์ž ํ•จ์ˆ˜

Car ๊ฐ์ฒด๋Š” glTF ๋กœ๋”๋กœ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ ๋•Œ๋ฌธ์— ์šฉ๋Ÿ‰์ด ํฌ๋ฉด ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ถˆํŽธํ•จ์„ ์ค„ ์ˆ˜ ์žˆ์–ด async ๋กœ ๋กœ๋“œํ•  ๊ฒƒ์ด๋‹ค.
์ด๋Š” ์ƒ์„ฑ์ž์—์„œ ํ˜ธ์ถœํ•˜๋ฉด async ๊ฐ€ ๋จนํžˆ์ง€ ์•Š์œผ๋ฏ€๋กœ ๋น„์›Œ์ค€๋‹ค.

2. loadCar()

์‹ค์งˆ์ ์œผ๋กœ Car ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•˜๋Š” ์ƒ์„ฑ์ž ํ•จ์ˆ˜ ์—ญํ• ์„ ํ•˜๋Š” ํ•จ์ˆ˜

Mesh

    async loadCar(scene: THREE.Scene, position: [number, number, number]) {
        await new GLTFLoader().loadAsync('models/sedanSports.glb').then((gltf) => {
            const carMesh = gltf.scene.getObjectByName('body') as THREE.Group
            carMesh.position.set(0, 0, 0)
            carMesh.traverse((o) => {
                o.castShadow = true
            })

            const wheelBLMesh = gltf.scene.getObjectByName('wheel_backLeft') as THREE.Group
            const wheelBRMesh = gltf.scene.getObjectByName('wheel_backRight') as THREE.Group
            const wheelFLMesh = gltf.scene.getObjectByName('wheel_frontLeft') as THREE.Group
            const wheelFRMesh = gltf.scene.getObjectByName('wheel_frontRight') as THREE.Group
            wheelBLMesh.position.set(0, 0, 0)
            wheelBRMesh.position.set(0, 0, 0)
            wheelFLMesh.position.set(0, 0, 0)
            wheelFRMesh.position.set(0, 0, 0)

            //scene.add(gltf.scene)
            scene.add(carMesh, wheelBLMesh, wheelBRMesh, wheelFLMesh, wheelFRMesh)

๋ชจ๋ธ ๋กœ๋“œ ํ›„ THREE.Scene ์— ์ถ”๊ฐ€

RigidBody

            const carBody = world.createRigidBody(
                RAPIER.RigidBodyDesc.dynamic()
                    .setTranslation(...position)
                    .setCanSleep(false)
            )
            const wheelBLBody = world.createRigidBody(
                RAPIER.RigidBodyDesc.dynamic()
                    .setTranslation(-1 + position[0], 1 + position[1], 1 + position[2])
                    .setCanSleep(false)
            )
            const wheelBRBody = world.createRigidBody(
                RAPIER.RigidBodyDesc.dynamic()
                    .setTranslation(1 + position[0], 1 + position[1], 1 + position[2])
                    .setCanSleep(false)
            )
            const wheelFLBody = world.createRigidBody(
                RAPIER.RigidBodyDesc.dynamic()
                    .setTranslation(-1 + position[0], 1 + position[1], -1 + position[2])
                    .setCanSleep(false)
            )
            const wheelFRBody = world.createRigidBody(
                RAPIER.RigidBodyDesc.dynamic()
                    .setTranslation(1 + position[0], 1 + position[1], -1 + position[2])
                    .setCanSleep(false)
            )

์ž๋™์ฐจ ๋ณธ์ฒด์™€ ๋ฐ”ํ€ด ๊ฐ๊ฐ์— ๋Œ€ํ•ด RigidBody ๋ฅผ ์ƒ์„ฑ
.setTranslation() ์—์„œ position ์„ ๊ท€์ฐฎ๊ฒŒ ํ•˜๋“œ์ฝ”๋”ฉ ํ•˜์ง€ ์•Š์€ ์ด์œ ๋Š” Car ๊ฐ์ฒด๋ฅผ ๋˜ ํ•˜๋‚˜ ์ƒ์„ฑํ•  ๋•Œ ๊ฐ™์€ ์œ„์น˜์— ๋ฐ”ํ€ด๊ฐ€ ์ƒ๊ธฐ๋ฉด์„œ ๋ฌผ๋ฆฌ์—”์ง„์˜ ์ถฉ๋Œ์„ ์ผ์œผ์ผœ ์˜ˆ๊ธฐ์น˜ ๋ชปํ•œ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ›์„ ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

maxrix ์„ค์ •

            const v = new THREE.Vector3()
            let positions: number[] = []
            // 1. ์›”๋“œ ๋งคํŠธ๋ฆญ์Šค๊ฐ€ ์ตœ์‹  ์ƒํƒœ์ธ์ง€ ํ™•์ธ
            carMesh.updateMatrixWorld(true) 
            carMesh.traverse((o) => {
                if (o.type === 'Mesh') {
                    const positionAttribute = (o as THREE.Mesh).geometry.getAttribute('position')
                    for (let i = 0, l = positionAttribute.count; i < l; i++) {
                        v.fromBufferAttribute(positionAttribute, i)
                      	// 2. ๋กœ์ปฌ ์ขŒํ‘œ๋ฅผ world ์ขŒํ‘œ๋กœ ๋ณ€ํ™˜
                        v.applyMatrix4((o.parent as THREE.Object3D).matrixWorld)
                        positions.push(...v)
                    }
                }
            })

carMesh ๊ทธ๋ฃน์— ์†ํ•œ ๋ชจ๋“  THREE.Mesh ์— ๋Œ€ํ•ด convexhull ์„ ์ƒ์„ฑ

๋งŒ์•ฝ ์œ„ ์ฝ”๋“œ์—์„œ 1๋ฒˆ์„ ๋„ฃ์ง€ ์•Š๋Š”๋‹ค๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํ˜„์ƒ์ด ๋‚˜ํƒ€๋‚œ๋‹ค. ๋”ฐ๋ผ์„œ Mesh ์™€ RigidBody ๋ฅผ ๋ณด์ด๋Š” ๋Œ€๋กœ ์ •ํ™•ํžˆ ์ผ์น˜ํ‚ค๋ ค๋ฉด world ๋ฅผ ์ตœ์‹ ํ™”ํ•˜์—ฌ carMesh ์— ์ ์šฉํ•ด์•ผํ•œ๋‹ค.

๋งŒ์•ฝ ์œ„ ์ฝ”๋“œ์—์„œ 2๋ฒˆ์„ ๋„ฃ์ง€ ์•Š๋Š”๋‹ค๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํ˜„์ƒ์ด ๋‚˜ํƒ€๋‚œ๋‹ค. object3D ์—์„œ ํฌ์ŠคํŒ…ํ•œ Hierarchy ๋ฅผ ์ ์šฉํ•œ local ์ขŒํ‘œ๋ฅผ wolrd ์ขŒํ‘œ๋กœ ๊ฐ€์ ธ์˜ด. ๊ทธ๋ž˜์„œ ๋ธ”๋žœ๋”์— body ์— child ๋กœ ๋“ค์–ด์žˆ๋Š” spoiler ๋„ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๊ฒŒ ๋˜๋Š” ๊ฑฐ์ž„.

updateMatrixWorld() ๋Š” ๋ฉ”์‹œ์˜ ์›”๋“œ ๋งคํŠธ๋ฆญ์Šค๋ฅผ ์—…๋ฐ์ดํŠธํ•˜์—ฌ, ๋ถ€๋ชจ ๊ฐ์ฒด์˜ ๋ณ€ํ™˜(์œ„์น˜, ํšŒ์ „, ํฌ๊ธฐ ๋“ฑ)์„ ๋ฐ˜์˜ํ•˜๋„๋ก ํ•œ๋‹ค. true ์ธ์ž๋Š” ๋ชจ๋“  ์ž์‹ ๊ฐ์ฒด๋„ ํ•จ๊ป˜ ์—…๋ฐ์ดํŠธํ•˜๋„๋ก ํ•œ๋‹ค.

geometry.getAttribute('position') ๋ฉ”์†Œ๋“œ๋ฅผ ํ†ตํ•ด ํ•ด๋‹น ๋ฉ”์‹œ์˜ ์ •์  ์œ„์น˜ ์†์„ฑ์„ ๊ฐ€์ ธ์˜จ๋‹ค.
์ด ์†์„ฑ์€ ์ •์ ์˜ ์œ„์น˜ ์ •๋ณด๋ฅผ ๋‹ด๊ณ  ์žˆ๋Š” BufferAttribute ๊ฐ์ฒด์ด๋‹ค.

for (let i = 0, l = positionAttribute.count; i < l; i++) ๊ฐ ์ •์ ์— ๋Œ€ํ•ด ๋ฐ˜๋ณต๋ฌธ์„ ์‹คํ–‰ํ•˜๋ฉฐ:
fromBufferAttribute(positionAttribute, i) ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ธ๋ฑ์Šค i ์— ํ•ด๋‹นํ•˜๋Š” ์ •์ ์˜ ์œ„์น˜๋ฅผ ๋ฒกํ„ฐ v ์— ๋กœ๋“œ
applyMatrix4((o.parent as Object3D).matrixWorld) ๋ฅผ ํ†ตํ•ด ๋ถ€๋ชจ ๊ฐ์ฒด์˜ ์›”๋“œ ๋งคํŠธ๋ฆญ์Šค๋ฅผ ์ ์šฉํ•˜์—ฌ ์ •์ ์˜ ์›”๋“œ ์ขŒํ‘œ๋ฅผ ๊ณ„์‚ฐ
์ตœ์ข…์ ์œผ๋กœ ๊ณ„์‚ฐ๋œ ์œ„์น˜๋ฅผ positions ๋ฐฐ์—ด์— ์ถ”๊ฐ€

convexhull ์ƒ์„ฑ

            const carShape = (RAPIER.ColliderDesc.convexHull(new Float32Array(positions)) as RAPIER.ColliderDesc).setMass(1).setRestitution(0.5)

            const wheelBLShape = RAPIER.ColliderDesc.cylinder(0.1, 0.3)
                .setRotation(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), -Math.PI / 2))
                .setTranslation(-0.2, 0, 0)
                .setRestitution(0.5)
            const wheelBRShape = RAPIER.ColliderDesc.cylinder(0.1, 0.3)
                .setRotation(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), Math.PI / 2))
                .setTranslation(0.2, 0, 0)
                .setRestitution(0.5)
            const wheelFLShape = RAPIER.ColliderDesc.cylinder(0.1, 0.3)
                .setRotation(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), Math.PI / 2))
                .setTranslation(-0.2, 0, 0)
                .setRestitution(0.5)
            const wheelFRShape = RAPIER.ColliderDesc.cylinder(0.1, 0.3)
                .setRotation(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), Math.PI / 2))
                .setTranslation(0.2, 0, 0)
                .setRestitution(0.5)

๋งŒ์•ฝ setRotation(), setTranslation() ์„ ์‹คํ–‰ํ•˜์ง€ ์•Š์œผ๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๊ฒฐ๊ณผ๊ฐ€ ๋‚˜์˜จ๋‹ค. ๋”ฐ๋ผ์„œ ์ž๋™์ฐจ์˜ body ์™€ ๋ฐ”ํ€ด์˜ convexHull ๋ฅผ ์ƒ์„ฑ ํ›„ THREE.Mesh ์™€ ์ผ์น˜ ์ž‘์—…์„ ํ•ด์ค˜์•ผํ•œ๋‹ค.

collider ์ƒ์„ฑ ๋ฐ ์—ฐ๊ฒฐ

            world.createCollider(carShape, carBody)
            world.createCollider(wheelBLShape, wheelBLBody)
            world.createCollider(wheelBRShape, wheelBRBody)
            world.createCollider(wheelFLShape, wheelFLBody)
            world.createCollider(wheelFRShape, wheelFRBody)

์ž๋™์ฐจ ๋ณธ์ฒด์™€ ๊ฐ ๋ฐ”ํ€ด์— ์ƒ์„ฑ๋œ ์ถฉ๋Œ์ฒด๋ฅผ ์—ฐ๊ฒฐํ•˜์—ฌ ๋ฌผ๋ฆฌ ์‹œ๋ฎฌ๋ ˆ์ด์…˜์„ ์ฒ˜๋ฆฌ.

๊ด€์ ˆ(Joints) ์ƒ์„ฑ

            // attach wheels to car using Rapier revolute joints
            world.createImpulseJoint(
                RAPIER.JointData.revolute(new RAPIER.Vector3(-0.55, 0, 0.63), new RAPIER.Vector3(0, 0, 0), new RAPIER.Vector3(-1, 0, 0)),
                carBody,
                wheelBLBody,
                true
            )
            world.createImpulseJoint(
                RAPIER.JointData.revolute(new RAPIER.Vector3(0.55, 0, 0.63), new RAPIER.Vector3(0, 0, 0), new RAPIER.Vector3(1, 0, 0)),
                carBody,
                wheelBRBody,
                true
            )
            world.createImpulseJoint(
                RAPIER.JointData.revolute(new RAPIER.Vector3(-0.55, 0, -0.63), new RAPIER.Vector3(0, 0, 0), new RAPIER.Vector3(-1, 0, 0)),
                carBody,
                wheelFLBody,
                true
            )
            world.createImpulseJoint(
                RAPIER.JointData.revolute(new RAPIER.Vector3(0.55, 0, -0.63), new RAPIER.Vector3(0, 0, 0), new RAPIER.Vector3(1, 0, 0)),
                carBody,
                wheelFRBody,
                true
            )

์ž๋™์ฐจ ๋ณธ์ฒด์™€ ๋ฐ”ํ€ด๋ฅผ Revolute Joint(ํšŒ์ „ ๊ด€์ ˆ) ๋กœ ์—ฐ๊ฒฐ. revolute Joint ๋Š” axis ๋ฅผ ๋‹จ ํ•œ๊ฐœ๋งŒ ๊ฐ–์„ ์ˆ˜ ์žˆ๋‹ค. ๋”ฐ๋ผ์„œ ์„ ํ’๊ธฐ๋‚˜ ๋ฐ”ํ€ด์— ์ ํ•ฉํ•œ ๊ด€์ ˆ์ด๋‹ค.

๋ฐ”ํ€ด๋Š” ํšŒ์ „๋งŒ ๊ฐ€๋Šฅํ•˜๋„๋ก ์„ค์ •ํ•˜๋ฉฐ, ์œ„์น˜์™€ ํšŒ์ „์ถ•์„ ์ •ํ™•ํžˆ ์ง€์ •.

revolute() ์˜ ์ฒซ์งธ ์ธ์ˆ˜๋Š” ๋ณธ์ฒด๋กœ๋ถ€ํ„ฐ ์œ„์น˜๋ฅผ ์ง€์ •ํ•˜๊ณ 

revolute() ์˜ ๋‘˜์งธ ์ธ์ˆ˜๋Š” ์ง€์ •๋œ ์กฐ์ธํŠธ ์ง€์ ์œผ๋กœ๋ถ€ํ„ฐ ์œ„์น˜๋ฅผ ์ง€์ •ํ•œ๋‹ค.

revolute() ์˜ ์„ธ๋ฒˆ์งธ ์ธ์ˆ˜๋Š” ์ง€์ •๋œ ์กฐ์ธํŠธ ์ง€์ ์œผ๋กœ๋ถ€ํ„ฐ ๊ฑฐ๋ฆฌ๋ฅผ ์ง€์ •ํ•œ๋‹ค.

dynamicBodies ์—…๋ฐ์ดํŠธ

            // update local dynamicBodies so mesh positions and quaternions are updated with the physics world info
            this.dynamicBodies.push([carMesh, carBody])
            this.dynamicBodies.push([wheelBLMesh, wheelBLBody])
            this.dynamicBodies.push([wheelBRMesh, wheelBRBody])
            this.dynamicBodies.push([wheelFLMesh, wheelFLBody])
            this.dynamicBodies.push([wheelFRMesh, wheelFRBody])
        })
    }

3. update()

    update() {
        for (let i = 0, n = this.dynamicBodies.length; i < n; i++) {
            this.dynamicBodies[i][0].position.copy(this.dynamicBodies[i][1].translation())
            this.dynamicBodies[i][0].quaternion.copy(this.dynamicBodies[i][1].rotation())
        }
    }
}

์ด๋•Œ animation ํ•จ์ˆ˜์—์„œ update ๋ฅผ ์‹คํ–‰ํ•ด์ฃผ์ง€ ์•Š๋Š”๋‹ค๋ฉด ์‹ค์ œ RigidBody ๋Š” ์—…๋ฐ์ดํŠธ ๋˜๊ณ  ์žˆ์ง€๋งŒ THREE.Mesh ๋Š” ์‹œ๊ฐ์ ์œผ๋กœ ๋ฉˆ์ถฐ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํ˜„์ƒ์ด ๋‚˜ํƒ€๋‚œ๋‹ค.


๐Ÿ’ป Car code (feat. Rapier)

import './style.css'
import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'
import { OBJLoader } from 'three/addons/loaders/OBJLoader.js'
import Stats from 'three/addons/libs/stats.module.js'
import { GUI } from 'three/addons/libs/lil-gui.module.min.js'
import RAPIER from '@dimforge/rapier3d-compat'

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
        }
    }
}

class Car {
    dynamicBodies: [THREE.Object3D, RAPIER.RigidBody][] = []

    constructor() {
        // now calling `loadCar` outside of constructor, since async await won't wait if called in a constructor.
    }

    async loadCar(scene: THREE.Scene, position: [number, number, number]) {
        await new GLTFLoader().loadAsync('models/sedanSports.glb').then((gltf) => {
            const carMesh = gltf.scene.getObjectByName('body') as THREE.Group
            carMesh.position.set(0, 0, 0)
            carMesh.traverse((o) => {
                o.castShadow = true
            })

            const wheelBLMesh = gltf.scene.getObjectByName('wheel_backLeft') as THREE.Group
            const wheelBRMesh = gltf.scene.getObjectByName('wheel_backRight') as THREE.Group
            const wheelFLMesh = gltf.scene.getObjectByName('wheel_frontLeft') as THREE.Group
            const wheelFRMesh = gltf.scene.getObjectByName('wheel_frontRight') as THREE.Group
            wheelBLMesh.position.set(0, 0, 0)
            wheelBRMesh.position.set(0, 0, 0)
            wheelFLMesh.position.set(0, 0, 0)
            wheelFRMesh.position.set(0, 0, 0)

            //scene.add(gltf.scene)
            scene.add(carMesh, wheelBLMesh, wheelBRMesh, wheelFLMesh, wheelFRMesh)

            const carBody = world.createRigidBody(
                RAPIER.RigidBodyDesc.dynamic()
                    .setTranslation(...position)
                    .setCanSleep(false)
            )
            const wheelBLBody = world.createRigidBody(
                RAPIER.RigidBodyDesc.dynamic()
                    .setTranslation(-1 + position[0], 1 + position[1], 1 + position[2])
                    .setCanSleep(false)
            )
            const wheelBRBody = world.createRigidBody(
                RAPIER.RigidBodyDesc.dynamic()
                    .setTranslation(1 + position[0], 1 + position[1], 1 + position[2])
                    .setCanSleep(false)
            )
            const wheelFLBody = world.createRigidBody(
                RAPIER.RigidBodyDesc.dynamic()
                    .setTranslation(-1 + position[0], 1 + position[1], -1 + position[2])
                    .setCanSleep(false)
            )
            const wheelFRBody = world.createRigidBody(
                RAPIER.RigidBodyDesc.dynamic()
                    .setTranslation(1 + position[0], 1 + position[1], -1 + position[2])
                    .setCanSleep(false)
            )

            // create a convexhull from all meshes in the carMesh group
            const v = new THREE.Vector3()
            let positions: number[] = []
            carMesh.updateMatrixWorld(true) // ensure world matrix is up to date
            carMesh.traverse((o) => {
                if (o.type === 'Mesh') {
                    const positionAttribute = (o as THREE.Mesh).geometry.getAttribute('position')
                    for (let i = 0, l = positionAttribute.count; i < l; i++) {
                        v.fromBufferAttribute(positionAttribute, i)
                        v.applyMatrix4((o.parent as THREE.Object3D).matrixWorld)
                        positions.push(...v)
                    }
                }
            })

            // create shapes for carBody and wheelBodies
            const carShape = (RAPIER.ColliderDesc.convexHull(new Float32Array(positions)) as RAPIER.ColliderDesc).setMass(1).setRestitution(0.5)
            const wheelBLShape = RAPIER.ColliderDesc.cylinder(0.1, 0.3)
                .setRotation(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), -Math.PI / 2))
                .setTranslation(-0.2, 0, 0)
                .setRestitution(0.5)
            const wheelBRShape = RAPIER.ColliderDesc.cylinder(0.1, 0.3)
                .setRotation(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), Math.PI / 2))
                .setTranslation(0.2, 0, 0)
                .setRestitution(0.5)
            const wheelFLShape = RAPIER.ColliderDesc.cylinder(0.1, 0.3)
                .setRotation(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), Math.PI / 2))
                .setTranslation(-0.2, 0, 0)
                .setRestitution(0.5)
            const wheelFRShape = RAPIER.ColliderDesc.cylinder(0.1, 0.3)
                .setRotation(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), Math.PI / 2))
                .setTranslation(0.2, 0, 0)
                .setRestitution(0.5)

            // create world collider
            world.createCollider(carShape, carBody)
            world.createCollider(wheelBLShape, wheelBLBody)
            world.createCollider(wheelBRShape, wheelBRBody)
            world.createCollider(wheelFLShape, wheelFLBody)
            world.createCollider(wheelFRShape, wheelFRBody)

            // attach wheels to car using Rapier revolute joints
            world.createImpulseJoint(
                RAPIER.JointData.revolute(new RAPIER.Vector3(-0.55, 0, 0.63), new RAPIER.Vector3(0, 0, 0), new RAPIER.Vector3(-1, 0, 0)),
                carBody,
                wheelBLBody,
                true
            )
            world.createImpulseJoint(
                RAPIER.JointData.revolute(new RAPIER.Vector3(0.55, 0, 0.63), new RAPIER.Vector3(0, 0, 0), new RAPIER.Vector3(1, 0, 0)),
                carBody,
                wheelBRBody,
                true
            )
            world.createImpulseJoint(
                RAPIER.JointData.revolute(new RAPIER.Vector3(-0.55, 0, -0.63), new RAPIER.Vector3(0, 0, 0), new RAPIER.Vector3(-1, 0, 0)),
                carBody,
                wheelFLBody,
                true
            )
            world.createImpulseJoint(
                RAPIER.JointData.revolute(new RAPIER.Vector3(0.55, 0, -0.63), new RAPIER.Vector3(0, 0, 0), new RAPIER.Vector3(1, 0, 0)),
                carBody,
                wheelFRBody,
                true
            )

            // update local dynamicBodies so mesh positions and quaternions are updated with the physics world info
            this.dynamicBodies.push([carMesh, carBody])
            this.dynamicBodies.push([wheelBLMesh, wheelBLBody])
            this.dynamicBodies.push([wheelBRMesh, wheelBRBody])
            this.dynamicBodies.push([wheelFLMesh, wheelFLBody])
            this.dynamicBodies.push([wheelFRMesh, wheelFRBody])
        })
    }

    update() {
        for (let i = 0, n = this.dynamicBodies.length; i < n; i++) {
            this.dynamicBodies[i][0].position.copy(this.dynamicBodies[i][1].translation())
            this.dynamicBodies[i][0].quaternion.copy(this.dynamicBodies[i][1].rotation())
        }
    }
}

await RAPIER.init() // This line is only needed if using the compat version
const gravity = new RAPIER.Vector3(0.0, -9.81, 0.0)
const world = new RAPIER.World(gravity)
const dynamicBodies: [THREE.Object3D, RAPIER.RigidBody][] = []

const scene = new THREE.Scene()

const rapierDebugRenderer = new RapierDebugRenderer(scene, world)

const light1 = new THREE.SpotLight(undefined, Math.PI * 10)
light1.position.set(2.5, 5, 5)
light1.angle = Math.PI / 1.8
light1.penumbra = 0.5
light1.castShadow = true
light1.shadow.blurSamples = 10
light1.shadow.radius = 5
scene.add(light1)

const light2 = light1.clone()
light2.position.set(-2.5, 5, 5)
scene.add(light2)

const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 100)
camera.position.set(0, 2, 5)

const renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setSize(window.innerWidth, window.innerHeight)
renderer.shadowMap.enabled = true
renderer.shadowMap.type = THREE.VSMShadowMap
document.body.appendChild(renderer.domElement)

window.addEventListener('resize', () => {
    camera.aspect = window.innerWidth / window.innerHeight
    camera.updateProjectionMatrix()
    renderer.setSize(window.innerWidth, window.innerHeight)
})

const controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true
controls.target.y = 1

// Cuboid Collider
const cubeMesh = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), new THREE.MeshNormalMaterial())
cubeMesh.castShadow = true
scene.add(cubeMesh)
const cubeBody = world.createRigidBody(RAPIER.RigidBodyDesc.dynamic().setTranslation(-5, 5, 0).setCanSleep(false))
const cubeShape = RAPIER.ColliderDesc.cuboid(0.5, 0.5, 0.5).setMass(1).setRestitution(0.5)
world.createCollider(cubeShape, cubeBody)
dynamicBodies.push([cubeMesh, cubeBody])

// Ball Collider
const sphereMesh = new THREE.Mesh(new THREE.SphereGeometry(), new THREE.MeshNormalMaterial())
sphereMesh.castShadow = true
scene.add(sphereMesh)
const sphereBody = world.createRigidBody(RAPIER.RigidBodyDesc.dynamic().setTranslation(-2.5, 5, 0).setCanSleep(false))
const sphereShape = RAPIER.ColliderDesc.ball(1).setMass(1).setRestitution(0.5)
world.createCollider(sphereShape, sphereBody)
dynamicBodies.push([sphereMesh, sphereBody])

// Cylinder Collider
const cylinderMesh = new THREE.Mesh(new THREE.CylinderGeometry(1, 1, 2, 16), new THREE.MeshNormalMaterial())
cylinderMesh.castShadow = true
scene.add(cylinderMesh)
const cylinderBody = world.createRigidBody(RAPIER.RigidBodyDesc.dynamic().setTranslation(0, 5, 0).setCanSleep(false))
const cylinderShape = RAPIER.ColliderDesc.cylinder(1, 1).setMass(1).setRestitution(0.5)
world.createCollider(cylinderShape, cylinderBody)
dynamicBodies.push([cylinderMesh, cylinderBody])

// ConvexHull Collider
const icosahedronMesh = new THREE.Mesh(new THREE.IcosahedronGeometry(1, 0), new THREE.MeshNormalMaterial())
icosahedronMesh.castShadow = true
scene.add(icosahedronMesh)
const icosahedronBody = world.createRigidBody(RAPIER.RigidBodyDesc.dynamic().setTranslation(2.5, 5, 0).setCanSleep(false))
const points = new Float32Array(icosahedronMesh.geometry.attributes.position.array)
const icosahedronShape = (RAPIER.ColliderDesc.convexHull(points) as RAPIER.ColliderDesc).setMass(1).setRestitution(0.5)
world.createCollider(icosahedronShape, icosahedronBody)
dynamicBodies.push([icosahedronMesh, icosahedronBody])

// Trimesh Collider
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])

// the floor (using a cuboid)
const floorMesh = new THREE.Mesh(new THREE.BoxGeometry(50, 1, 50), new THREE.MeshPhongMaterial())
floorMesh.receiveShadow = true
floorMesh.position.y = -1
scene.add(floorMesh)
const floorBody = world.createRigidBody(RAPIER.RigidBodyDesc.fixed().setTranslation(0, -1, 0))
const floorShape = RAPIER.ColliderDesc.cuboid(25, 0.5, 25)
world.createCollider(floorShape, floorBody)

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])
})

const car = new Car()
await car.loadCar(scene, [0, 2, 0]) // now calling `loadCar` outside of constructor, since async await won't wait if called in a constructor

// const car2 = new Car()
// await car2.loadCar(scene, [-2, 2, 0])

const raycaster = new THREE.Raycaster()
const mouse = new THREE.Vector2()

renderer.domElement.addEventListener('click', (e) => {
    mouse.set((e.clientX / renderer.domElement.clientWidth) * 2 - 1, -(e.clientY / renderer.domElement.clientHeight) * 2 + 1)

    raycaster.setFromCamera(mouse, camera)

    const intersects = raycaster.intersectObjects(
        dynamicBodies.flatMap((a) => a[0]),
        false
    )

    if (intersects.length) {
        dynamicBodies.forEach((b) => {
            b[0] === intersects[0].object && b[1].applyImpulse(new RAPIER.Vector3(0, 10, 0), true)
        })
    }
})

const stats = new Stats()
document.body.appendChild(stats.dom)

const gui = new GUI()
gui.add(rapierDebugRenderer, 'enabled').name('Rapier Degug Renderer')

const physicsFolder = gui.addFolder('Physics')
physicsFolder.add(world.gravity, 'x', -10.0, 10.0, 0.1)
physicsFolder.add(world.gravity, 'y', -10.0, 10.0, 0.1)
physicsFolder.add(world.gravity, 'z', -10.0, 10.0, 0.1)

const clock = new THREE.Clock()
let delta

function animate() {
    requestAnimationFrame(animate)

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

    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())
    }

    car.update()
    //car2.update()

    rapierDebugRenderer.update()

    controls.update()

    renderer.render(scene, camera)

    stats.update()
}

animate()
profile
ๆ™บ(์ง€)! ๅพท(๋•)! ้ซ”(์ฒด)!

0๊ฐœ์˜ ๋Œ“๊ธ€