바퀴나 pan 을 만들 때 주로 Body 에 revolute ImpulseJoint 를 사용하여 Joint 한다고 포스팅하였다.
Rapier v0.12.0
에서 우리가 사용할 수 있는 ImpulseJoints
의 타입
타입 | 설명 |
---|---|
Revolute | 두 강체 사이 한 축을 따라 회전하는 것 만 허용 |
Fixed | 두 강체를 단단히 고정 |
Prismatic | 한 축을 따라 이동을 제외한 모든 자유도를 제거 |
Spherical | 두 강체 사이에 모든 자유도를 부여 |
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 되는 값을 각 바퀴에 담아줘야한다.
뒷 바퀴를 지정한 속도로 돌릴 수 있도록 하는 함수이다.
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
앞 바퀴의 방향을 조절할 수 있도록 하는 함수이다.
이 방법은 조금 다르다.
뒷바퀴 모터와 다르게 axel 은 body 가 있어야한다.
마치 carBody, 네개의 각 바퀴처럼 RigidBody, ColliderShape(cuboid) 가 있어야하며 이를 기반으로 collider 도 있어야한다.
그리고 이를 자동차에 붙여야한다.
마지막으로 이를 animate 함수에서 계속하여 갱신해주면 된다.
이제 차근차근 알아보자.
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 와 같은 위치에 생성해야한다.
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)
// 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
)// 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 이 방향을 조절하는 구조를 만든다.
...
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 를 연결한다.
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
지금까지 설정했어도 A, D 를 눌러서 방향키를 눌러도 움찔움찔만 하고 실제로 회전되진 않는다. 그 이유는 바퀴가 너무 커서 돌려도 차체와 부딪혀 더 이상 돌아가지 않는다.
따라서 이를 막기위해 우리는 CollisionGroups 을 설정할 수 있다. 그래서 오직 plane 과면 collide 할 수 있도록 말이다.
// 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
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);
}
}