Three.js Rapier ImpulseJoint Motors

강정우·2025년 1월 14일
0

three.js

목록 보기
21/24
post-thumbnail

Rapier ImpulseJoint Motors

ImpulseJoint

바퀴나 pan 을 만들 때 주로 Body 에 revolute ImpulseJoint 를 사용하여 Joint 한다고 포스팅하였다.

Rapier v0.12.0 에서 우리가 사용할 수 있는 ImpulseJoints 의 타입

타입설명
Revolute두 강체 사이 한 축을 따라 회전하는 것 만 허용
Fixed두 강체를 단단히 고정
Prismatic한 축을 따라 이동을 제외한 모든 자유도를 제거
Spherical두 강체 사이에 모든 자유도를 부여

Revolute

world.createImpulseJoint(
  JointData.revolute(new Vector3(-0.55, 0, 0.63), new Vector3(0, 0, 0), new Vector3(-1, 0, 0)), 
  this.carBody, 
  wheelBLBody, 
  true
)
world.createImpulseJoint(
  JointData.revolute(new Vector3(0.55, 0, 0.63), new Vector3(0, 0, 0), new Vector3(-1, 0, 0)), 
  this.carBody, 
  wheelBRBody, 
  true
)
world.createImpulseJoint(
  JointData.revolute(new Vector3(-0.55, 0, -0.63), new Vector3(0, 0, 0), new Vector3(1, 0, 0)), 
  this.carBody, 
  wheelFLBody, 
  true
)
world.createImpulseJoint(
  JointData.revolute(new Vector3(0.55, 0, -0.63), new Vector3(0, 0, 0), new Vector3(1, 0, 0)), 
  this.carBody, 
  wheelFRBody, 
  true
)

하지만 joint 에 연결되 대상 객체에 velocity 설정이나 position 설정을 하려면 함수를 사용해야하는데, 이 함수를 사용하기 위하여 createInpulseJoint 후 return 되는 값을 각 바퀴에 담아줘야한다.

configureMotorVelocity()

뒷 바퀴를 지정한 속도로 돌릴 수 있도록 하는 함수이다.

    this.wheelBLMotor = world.createImpulseJoint(JointData.revolute(new Vector3(-0.55, 0, 0.63), new Vector3(0, 0, 0), new Vector3(-1, 0, 0)), this.carBody, wheelBLBody, true);
	this.wheelBRMotor = world.createImpulseJoint(JointData.revolute(new Vector3(0.55, 0, 0.63), new Vector3(0, 0, 0), new Vector3(-1, 0, 0)), this.carBody, wheelBRBody, true);

위 코드를 보면 기존의 createImpulseJoint() 와 동일하지만 다른 점은 class 의 속성값에 담아줬다는 것이다.

export default class Car {
    dynamicBodies: [Object3D, RigidBody][] = []
    followTarget = new Object3D()
    lightLeftTarget = new Object3D()
    lightRightTarget = new Object3D()
    carBody?: RigidBody
    wheelBLMotor?: ImpulseJoint
    wheelBRMotor?: ImpulseJoint
    wheelFLAxel?: ImpulseJoint
    wheelFRAxel?: ImpulseJoint
    v = new Vector3()
    keyMap: { [key: string]: boolean }
    pivot: Object3D
  
    ...

class 의 속성을 생성하여 해당 type을 ImpulseJoint 로 설정 후 createImpulseJoint() 함수 실행 후 return 값을 저장

    update(delta: number) {
      ...
      
      let targetVelocity = 0
      if (this.keyMap['KeyW']) {
        targetVelocity = 500
      }
      if (this.keyMap['KeyS']) {
        targetVelocity = -200
      }
      (this.wheelBLMotor as PrismaticImpulseJoint).configureMotorVelocity(targetVelocity, 2.0);
      (this.wheelBRMotor as PrismaticImpulseJoint).configureMotorVelocity(targetVelocity, 2.0);

그리고 update 함수에서 사용자가 누르는 방향을 매핑하여 configureMotorVelocity(목표속도, 모터 반응 속도) 함수로

  • configureMotorVelocity(targetVel: number, factor: number): void
    용도: 조인트의 모터 속도를 설정합니다. 이 함수는 조인트가 목표 속도로 이동하도록 조정하는 데 사용됨
    파라미터
    - targetVel: 조인트가 도달해야 하는 목표 속도
    - factor: 속도의 조정 비율로, 물리적 반응을 조절하는 데 사용됨 ( 값이 클수록 모터의 반응 속도가 빨라짐 )

configureMotorPosition()

앞 바퀴의 방향을 조절할 수 있도록 하는 함수이다.
이 방법은 조금 다르다.

뒷바퀴 모터와 다르게 axel 은 body 가 있어야한다.
마치 carBody, 네개의 각 바퀴처럼 RigidBody, ColliderShape(cuboid) 가 있어야하며 이를 기반으로 collider 도 있어야한다.
그리고 이를 자동차에 붙여야한다.

마지막으로 이를 animate 함수에서 계속하여 갱신해주면 된다.
이제 차근차근 알아보자.

1. createRigidBody()

const wheelFLBody = world.createRigidBody(
  RigidBodyDesc.dynamic()
  .setTranslation(position[0] - 0.55, position[1], position[2] - 0.63)
  .setCanSleep(false)
)
const wheelFRBody = world.createRigidBody(
  RigidBodyDesc.dynamic()
  .setTranslation(position[0] + 0.55, position[1], position[2] - 0.63)
  .setCanSleep(false)
)

const axelFLBody = world.createRigidBody(
  RigidBodyDesc.dynamic()
  .setTranslation(position[0] - 0.55, position[1], position[2] - 0.63)
  .setCanSleep(false)
)
const axelFRBody = world.createRigidBody(
  RigidBodyDesc.dynamic()
  .setTranslation(position[0] + 0.55, position[1], position[2] - 0.63)
  .setCanSleep(false)
)

위 코드를 보면 알 수 있듯 RigidBody 는 자식 Body 와 같은 위치에 생성해야한다.

2. createShape

const wheelFLShape = ColliderDesc.cylinder(0.1, 0.3)
	.setRotation(new Quaternion().setFromAxisAngle(new Vector3(0, 0, 1), Math.PI / 2))
	.setTranslation(-0.2, 0, 0)
	.setRestitution(0.5)
	.setFriction(2.5)
const wheelFRShape = ColliderDesc.cylinder(0.1, 0.3)
	.setRotation(new Quaternion().setFromAxisAngle(new Vector3(0, 0, 1), Math.PI / 2))
	.setTranslation(0.2, 0, 0)
	.setRestitution(0.5)
	.setFriction(2.5)
const axelFLShape = ColliderDesc.cuboid(0.1, 0.1, 0.1)
	.setRotation(new Quaternion().setFromAxisAngle(new Vector3(0, 0, 1), Math.PI / 2))
	.setMass(0.1)
const axelFRShape = ColliderDesc.cuboid(0.1, 0.1, 0.1)
	.setRotation(new Quaternion().setFromAxisAngle(new Vector3(0, 0, 1), Math.PI / 2))
	.setMass(0.1)

3. attach between carBody and Axel instead of Wheel

// attach steering axels to car. These will be configurable motors.
this.wheelFLAxel = world.createImpulseJoint(JointData.revolute(new Vector3(-0.55, 0, -0.63), new Vector3(0, 0, 0), new Vector3(0, 1, 0)), this.carBody, axelFLBody, true);
(this.wheelFLAxel as PrismaticImpulseJoint).configureMotorModel(MotorModel.ForceBased);
this.wheelFRAxel = world.createImpulseJoint(JointData.revolute(new Vector3(0.55, 0, -0.63), new Vector3(0, 0, 0), new Vector3(0, 1, 0)), this.carBody, axelFRBody, true);
(this.wheelFRAxel as PrismaticImpulseJoint).configureMotorModel(MotorModel.ForceBased);

기존엔 carBody 에 앞 바퀴를 바로 붙였는데 이제 wheel 아닌 Axel 을 붙인다. 위 사진에서 carBody 와 바퀴 강체 사이에 있는 네모(cuboid) 가 바로 Axel 역할을 하여 왼쪽으로 누르면 coboid 가 왼쪽으로 꺾이는 것이다.

  • configureMotorModel(model: MotorModel): void
    용도: 조인트의 모터 모델을 설정. 모터 모델은 조인트가 어떻게 작동할지를 결정하며, 한마디로 configureMotorPosition() 함수가 동작하는 알고리즘을 설정한다. ( 기본값: AccelerationBased )
    파라미터:
    - model: 사용할 모터 모델을 나타내는 MotorModel 객체.
// attach front wheel to steering axels
world.createImpulseJoint(JointData.revolute(new Vector3(0, 0, 0), new Vector3(0, 0, 0), new Vector3(1, 0, 0)), axelFLBody, wheelFLBody, true)
world.createImpulseJoint(JointData.revolute(new Vector3(0, 0, 0), new Vector3(0, 0, 0), new Vector3(1, 0, 0)), axelFRBody, wheelFRBody, true)

이제 Axel 와 wheel 을 붙여 최종적으로 Body 와 wheel 이 붙도록 하고 중간에 Axel 이 방향을 조절하는 구조를 만든다.

4. 강체와 메쉬 연결하기

...
world.createCollider(wheelFLShape, wheelFLBody)
world.createCollider(wheelFRShape, wheelFRBody)
world.createCollider(axelFLShape, axelFLBody)
world.createCollider(axelFRShape, axelFRBody)

...
this.dynamicBodies.push([wheelFLMesh, wheelFLBody])
this.dynamicBodies.push([wheelFRMesh, wheelFRBody])
this.dynamicBodies.push([new Object3D(), axelFRBody])
this.dynamicBodies.push([new Object3D(), axelFLBody])

앞서 작성한 shape 와 강체를 연결하여 Collider 를 만들고 마지막으로 Mesh 와 RidgidBody 를 연결한다.

5. configureMotorPosition()

update(delta: number) {
  
  ...

  let targetSteer = 0
  if (this.keyMap['KeyA']) {
    targetSteer += 0.6
  }
  if (this.keyMap['KeyD']) {
    targetSteer -= 0.6
  }

  (this.wheelFLAxel as PrismaticImpulseJoint).configureMotorPosition(targetSteer, 100, 10);
  (this.wheelFRAxel as PrismaticImpulseJoint).configureMotorPosition(targetSteer, 100, 10);
}

드디어 마지막으로 animate 함수에 configureMotorPosition(방향, ) 를 넣어 동작시킨다.

  • configureMotorPosition(targetPos: number, stiffness: number, damping: number): void
    용도: 조인트의 모터 위치를 설정. 이 함수는 조인트가 목표 위치에 도달하도록 조정하는 데 사용됨.
    파라미터:
    - targetPos: 조인트가 도달해야 하는 목표 위치.
    - stiffness: 목표 위치에 대한 강성. ( 강성이 높을수록 조인트가 목표 위치에 더 단단히 유지됨 )
    - damping: 조인트가 목표 위치에 도달하는 과정에서의 감쇠 정도. ( 감쇠가 높을수록 진동이 줄어듬 )

setCollisionGroups()

지금까지 설정했어도 A, D 를 눌러서 방향키를 눌러도 움찔움찔만 하고 실제로 회전되진 않는다. 그 이유는 바퀴가 너무 커서 돌려도 차체와 부딪혀 더 이상 돌아가지 않는다.
따라서 이를 막기위해 우리는 CollisionGroups 을 설정할 수 있다. 그래서 오직 plane 과면 collide 할 수 있도록 말이다.

Rapier Collision Group Calculator

// collision groups
floorShape = 0
carShape = 1
wheelShape = 2
axelShape = 3

따라서 우리는 setCollisionGroups() 함수 안에 어떠한 값을 넣어서 어떤 Collider 와 상호작용할 지 설정할 수 있다.
그리고 우리는 위 변수들 처럼 본인이 해당 값을 마음대로 정할 수 있다. 하지만 이때 항상 일관성 있게 본인이 설정한 값(규칙) 대로 계산해야한다는 것이다.

자, 그래서 예를 들어
바닥 collides 은 차와 바퀴만 상호작용 함. membership=0, filter=[1,2] => 65542

차 collides 는 바닥과만 상호작용함. membership=1, filter=[0] => 131073

바퀴 collides 는 바닥과만 상호작용함. membership=2, filter=[0] => 262145

axels collide 는 어떠한 것과 상호작용 하지 않음. membership=3, filter=[] => 589823


🚗 car code

import {Group, Mesh, Object3D, Quaternion, Scene, SpotLight, TextureLoader, Vector3} from 'three'
import {
    RigidBody,
    ImpulseJoint,
    World,
    RigidBodyDesc,
    ColliderDesc,
    JointData,
    MotorModel,
    PrismaticImpulseJoint
} from '@dimforge/rapier3d-compat'
import {GLTFLoader} from 'three/addons/loaders/GLTFLoader.js'
import {Lensflare, LensflareElement} from 'three/addons/objects/Lensflare.js'

// collision groups
// floorShape = 0
// carShape = 1
// wheelShape = 2
// axelShape = 3

export default class Car {
    dynamicBodies: [Object3D, RigidBody][] = []
    followTarget = new Object3D()
    lightLeftTarget = new Object3D()
    lightRightTarget = new Object3D()
    carBody?: RigidBody
    wheelBLMotor?: ImpulseJoint
    wheelBRMotor?: ImpulseJoint
    wheelFLAxel?: ImpulseJoint
    wheelFRAxel?: ImpulseJoint
    v = new Vector3()
    keyMap: { [key: string]: boolean }
    pivot: Object3D

    constructor(keyMap: { [key: string]: boolean }, pivot: Object3D) {
        this.followTarget.position.set(0, 1, 0)
        this.lightLeftTarget.position.set(-0.35, 1, -10)
        this.lightRightTarget.position.set(0.35, 1, -10)
        this.keyMap = keyMap
        this.pivot = pivot
    }

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

            carMesh.add(this.followTarget)

            const textureLoader = new TextureLoader()
            const textureFlare0 = textureLoader.load('img/lensflare0.png')
            const textureFlare3 = textureLoader.load('img/lensflare3.png')

            const lensflareLeft = new Lensflare()
            lensflareLeft.addElement(new LensflareElement(textureFlare0, 1000, 0))
            lensflareLeft.addElement(new LensflareElement(textureFlare3, 500, 0.2))
            lensflareLeft.addElement(new LensflareElement(textureFlare3, 250, 0.8))
            lensflareLeft.addElement(new LensflareElement(textureFlare3, 125, 0.6))
            lensflareLeft.addElement(new LensflareElement(textureFlare3, 62.5, 0.4))

            const lensflareRight = new Lensflare()
            lensflareRight.addElement(new LensflareElement(textureFlare0, 1000, 0))
            lensflareRight.addElement(new LensflareElement(textureFlare3, 500, 0.2))
            lensflareRight.addElement(new LensflareElement(textureFlare3, 250, 0.8))
            lensflareRight.addElement(new LensflareElement(textureFlare3, 125, 0.6))
            lensflareRight.addElement(new LensflareElement(textureFlare3, 62.5, 0.4))

            const headLightLeft = new SpotLight(undefined, Math.PI * 20)
            headLightLeft.position.set(-0.4, 0.5, -1.01)
            headLightLeft.angle = Math.PI / 4
            headLightLeft.penumbra = 0.5
            headLightLeft.castShadow = true
            headLightLeft.shadow.blurSamples = 10
            headLightLeft.shadow.radius = 5

            const headLightRight = headLightLeft.clone()
            headLightRight.position.set(0.4, 0.5, -1.01)

            carMesh.add(headLightLeft)
            headLightLeft.target = this.lightLeftTarget
            headLightLeft.add(lensflareLeft)
            carMesh.add(this.lightLeftTarget)

            carMesh.add(headLightRight)
            headLightRight.target = this.lightRightTarget
            headLightRight.add(lensflareRight)
            carMesh.add(this.lightRightTarget)

            const wheelBLMesh = gltf.scene.getObjectByName('wheel_backLeft') as Group
            const wheelBRMesh = gltf.scene.getObjectByName('wheel_backRight') as Group
            const wheelFLMesh = gltf.scene.getObjectByName('wheel_frontLeft') as Group
            const wheelFRMesh = gltf.scene.getObjectByName('wheel_frontRight') as Group

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

            // create bodies for car, wheels and axels
            this.carBody = world.createRigidBody(
                RigidBodyDesc.dynamic()
                    .setTranslation(...position)
                    .setCanSleep(false)
            )

            const wheelBLBody = world.createRigidBody(
                RigidBodyDesc.dynamic()
                    .setTranslation(position[0] - 0.55, position[1], position[2] + 0.63)
                    .setCanSleep(false)
            )
            const wheelBRBody = world.createRigidBody(
                RigidBodyDesc.dynamic()
                    .setTranslation(position[0] + 0.55, position[1], position[2] + 0.63)
                    .setCanSleep(false)
            )

            const wheelFLBody = world.createRigidBody(
                RigidBodyDesc.dynamic()
                    .setTranslation(position[0] - 0.55, position[1], position[2] - 0.63)
                    .setCanSleep(false)
            )
            const wheelFRBody = world.createRigidBody(
                RigidBodyDesc.dynamic()
                    .setTranslation(position[0] + 0.55, position[1], position[2] - 0.63)
                    .setCanSleep(false)
            )

            const axelFLBody = world.createRigidBody(
                RigidBodyDesc.dynamic()
                    .setTranslation(position[0] - 0.55, position[1], position[2] - 0.63)
                    .setCanSleep(false)
            )
            const axelFRBody = world.createRigidBody(
                RigidBodyDesc.dynamic()
                    .setTranslation(position[0] + 0.55, position[1], position[2] - 0.63)
                    .setCanSleep(false)
            )

            // create a convexhull from all meshes in the carMesh group
            const v = new 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 Mesh).geometry.getAttribute('position')
                    for (let i = 0, l = positionAttribute.count; i < l; i++) {
                        v.fromBufferAttribute(positionAttribute, i)
                        v.applyMatrix4((o.parent as Object3D).matrixWorld)
                        positions.push(...v)
                    }
                }
            })

            // create shapes for carBody, wheelBodies and axelBodies
            const carShape = (ColliderDesc.convexMesh(new Float32Array(positions)) as ColliderDesc).setMass(1).setRestitution(0.0).setFriction(3)
            .setCollisionGroups(131073)
            const wheelBLShape = ColliderDesc.cylinder(0.1, 0.3)
                .setRotation(new Quaternion().setFromAxisAngle(new Vector3(0, 0, 1), -Math.PI / 2))
                .setTranslation(-0.2, 0, 0)
                .setRestitution(0.5)
                .setFriction(3)
            .setCollisionGroups(262145)
            const wheelBRShape = ColliderDesc.cylinder(0.1, 0.3)
                .setRotation(new Quaternion().setFromAxisAngle(new Vector3(0, 0, 1), Math.PI / 2))
                .setTranslation(0.2, 0, 0)
                .setRestitution(0.5)
                .setFriction(3)
            .setCollisionGroups(262145)
            const wheelFLShape = ColliderDesc.cylinder(0.1, 0.3)
                .setRotation(new Quaternion().setFromAxisAngle(new Vector3(0, 0, 1), Math.PI / 2))
                .setTranslation(-0.2, 0, 0)
                .setRestitution(0.5)
                .setFriction(2.5)
            .setCollisionGroups(262145)
            const wheelFRShape = ColliderDesc.cylinder(0.1, 0.3)
                .setRotation(new Quaternion().setFromAxisAngle(new Vector3(0, 0, 1), Math.PI / 2))
                .setTranslation(0.2, 0, 0)
                .setRestitution(0.5)
                .setFriction(2.5)
            .setCollisionGroups(262145)
            const axelFLShape = ColliderDesc.cuboid(0.1, 0.1, 0.1)
                .setRotation(new Quaternion().setFromAxisAngle(new Vector3(0, 0, 1), Math.PI / 2))
                .setMass(0.1)
              .setCollisionGroups(589823)
            const axelFRShape = ColliderDesc.cuboid(0.1, 0.1, 0.1)
                .setRotation(new Quaternion().setFromAxisAngle(new Vector3(0, 0, 1), Math.PI / 2))
                .setMass(0.1)
              .setCollisionGroups(589823)

            // attach back wheel to cars. These will be configurable motors.
            this.wheelBLMotor = world.createImpulseJoint(JointData.revolute(new Vector3(-0.55, 0, 0.63), new Vector3(0, 0, 0), new Vector3(-1, 0, 0)), this.carBody, wheelBLBody, true)
            this.wheelBRMotor = world.createImpulseJoint(JointData.revolute(new Vector3(0.55, 0, 0.63), new Vector3(0, 0, 0), new Vector3(-1, 0, 0)), this.carBody, wheelBRBody, true)

            // attach steering axels to car. These will be configurable motors.
            this.wheelFLAxel = world.createImpulseJoint(JointData.revolute(new Vector3(-0.55, 0, -0.63), new Vector3(0, 0, 0), new Vector3(0, 1, 0)), this.carBody, axelFLBody, true);
            (this.wheelFLAxel as PrismaticImpulseJoint).configureMotorModel(MotorModel.ForceBased);
            this.wheelFRAxel = world.createImpulseJoint(JointData.revolute(new Vector3(0.55, 0, -0.63), new Vector3(0, 0, 0), new Vector3(0, 1, 0)), this.carBody, axelFRBody, true);
            (this.wheelFRAxel as PrismaticImpulseJoint).configureMotorModel(MotorModel.ForceBased);

            // attach front wheel to steering axels
            world.createImpulseJoint(JointData.revolute(new Vector3(0, 0, 0), new Vector3(0, 0, 0), new Vector3(1, 0, 0)), axelFLBody, wheelFLBody, true)
            world.createImpulseJoint(JointData.revolute(new Vector3(0, 0, 0), new Vector3(0, 0, 0), new Vector3(1, 0, 0)), axelFRBody, wheelFRBody, true)

            // create world collider
            world.createCollider(carShape, this.carBody)
            world.createCollider(wheelBLShape, wheelBLBody)
            world.createCollider(wheelBRShape, wheelBRBody)
            world.createCollider(wheelFLShape, wheelFLBody)
            world.createCollider(wheelFRShape, wheelFRBody)
            world.createCollider(axelFLShape, axelFLBody)
            world.createCollider(axelFRShape, axelFRBody)

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

    update(delta: number) {
        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())
        }

        this.followTarget.getWorldPosition(this.v)
        this.pivot.position.lerp(this.v, delta * 5)

        let targetVelocity = 0
        if (this.keyMap['KeyW']) {
            targetVelocity = 500
        }
        if (this.keyMap['KeyS']) {
            targetVelocity = -200
        }
        (this.wheelBLMotor as PrismaticImpulseJoint).configureMotorVelocity(targetVelocity, 2.0);
        (this.wheelBRMotor as PrismaticImpulseJoint).configureMotorVelocity(targetVelocity, 2.0);

        let targetSteer = 0
        if (this.keyMap['KeyA']) {
            targetSteer += 0.6
        }
        if (this.keyMap['KeyD']) {
            targetSteer -= 0.6
        }

        (this.wheelFLAxel as PrismaticImpulseJoint).configureMotorPosition(targetSteer, 100, 10);
        (this.wheelFRAxel as PrismaticImpulseJoint).configureMotorPosition(targetSteer, 100, 10);
    }
}
profile
智(지)! 德(덕)! 體(체)!

0개의 댓글