diff --git a/examples/public/draco-gltf/draco_decoder.js b/examples/public/draco-gltf/draco_decoder.js
index 6b38f1c6..78b524d7 100755
--- a/examples/public/draco-gltf/draco_decoder.js
+++ b/examples/public/draco-gltf/draco_decoder.js
@@ -295,8 +295,7 @@ var DracoDecoderModule = (function () {
},
Module: function (binary) {},
Instance: function (module, info) {
- this.exports = // EMSCRIPTEN_START_ASM
- (function instantiate(Vn, Wn) {
+ this.exports = (function instantiate(Vn, Wn) { // EMSCRIPTEN_START_ASM
function Mn(Xn) {
Xn.set = function (T, Yn) {
this[T] = Yn
diff --git a/examples/src/demos/Vehicle.js b/examples/src/demos/Vehicle.js
new file mode 100644
index 00000000..e41294c8
--- /dev/null
+++ b/examples/src/demos/Vehicle.js
@@ -0,0 +1,327 @@
+import React, { forwardRef, useEffect, useRef, useState } from 'react'
+import { Canvas, extend, useFrame, useThree } from 'react-three-fiber'
+import { Physics, useBox, useCylinder, usePlane, useRaycastVehicle } from '@react-three/cannon'
+import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
+
+// Extend will make OrbitControls available as a JSX element called orbitControls for us to use.
+extend({ OrbitControls })
+
+const CameraControls = () => {
+ // Get a reference to the Three.js Camera, and the canvas html element.
+ // We need these to setup the OrbitControls component.
+ // https://threejs.org/docs/#examples/en/controls/OrbitControls
+ const {
+ camera,
+ gl: { domElement },
+ } = useThree()
+ // Ref to the controls, so that we can update them on every frame using useFrame
+ const controls = useRef()
+ useFrame((state) => controls.current.update())
+ return
+}
+
+function Plane(props) {
+ const [ref] = usePlane(() => ({
+ type: 'Static',
+ material: 'ground',
+ ...props,
+ }))
+ return (
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+// The vehicle chassis
+const Chassis = forwardRef((props, ref) => {
+ const boxSize = [1.2, 1, 4]
+ // eslint-disable-next-line
+ const [_, api] = useBox(
+ () => ({
+ // type: 'Kinematic',
+ mass: 500,
+ rotation: props.rotation,
+ angularVelocity: props.angularVelocity,
+ allowSleep: false,
+ args: boxSize,
+ ...props,
+ }),
+ ref
+ )
+ return (
+
+
+
+
+
+ )
+})
+
+// A Wheel
+const Wheel = forwardRef((props, ref) => {
+ const wheelSize = [0.7, 0.7, 0.5, 16]
+ useCylinder(
+ () => ({
+ mass: 1,
+ type: 'Kinematic',
+ material: 'wheel',
+ collisionFilterGroup: 0, // turn off collisions !!
+ // rotation: [0,0,Math.PI/2], // useless -> the rotation should be applied to the shape (not the body)
+ args: wheelSize,
+ ...props,
+ }),
+ ref
+ )
+ // useCompoundBody(
+ // () => ({
+ // mass: 1,
+ // type: 'Kinematic',
+ // material: 'wheel',
+ // collisionFilterGroup: 0, // turn off collisions
+ // ...props,
+ // shapes: [{ type: 'Cylinder', args: wheelSize, rotation: [Math.PI / 2, 0, 0] }],
+ // }),
+ // ref
+ // )
+ return (
+
+
+
+
+
+
+ )
+})
+
+function Pilar(props) {
+ const args = [0.7, 0.7, 5, 16]
+ const [ref] = useCylinder(() => ({
+ mass: 10,
+ args: args,
+ ...props,
+ }))
+ return (
+
+
+
+
+ )
+}
+
+const wheelInfo = {
+ radius: 0.7,
+ directionLocal: [0, -1, 0], // same as Physics gravity
+ suspensionStiffness: 30,
+ suspensionRestLength: 0.3,
+ maxSuspensionForce: 1e4,
+ maxSuspensionTravel: 0.3,
+ dampingRelaxation: 2.3,
+ dampingCompression: 4.4,
+ frictionSlip: 5,
+ rollInfluence: 0.01,
+ axleLocal: [1, 0, 0], // wheel rotates around X-axis
+ chassisConnectionPointLocal: [1, 0, 1],
+ isFrontWheel: false,
+ useCustomSlidingRotationalSpeed: true,
+ customSlidingRotationalSpeed: -30,
+}
+
+function Vehicle(props) {
+ // chassisBody
+ const chassis = useRef()
+ // wheels
+ const wheels = []
+ const wheelInfos = []
+
+ // chassis - wheel connection helpers
+ var chassisWidth = 2
+ var chassisHeight = 0
+ var chassisFront = 1
+ var chassisBack = -1
+
+ // FrontLeft [-X,Y,Z]
+ const wheel_1 = useRef()
+ wheels.push(wheel_1)
+ const wheelInfo_1 = { ...wheelInfo }
+ wheelInfo_1.chassisConnectionPointLocal = [-chassisWidth / 2, chassisHeight, chassisFront]
+ wheelInfo_1.isFrontWheel = true
+ wheelInfos.push(wheelInfo_1)
+ // FrontRight [X,Y,Z]
+ const wheel_2 = useRef()
+ wheels.push(wheel_2)
+ const wheelInfo_2 = { ...wheelInfo }
+ wheelInfo_2.chassisConnectionPointLocal = [chassisWidth / 2, chassisHeight, chassisFront]
+ wheelInfo_2.isFrontWheel = true
+ wheelInfos.push(wheelInfo_2)
+ // BackLeft [-X,Y,-Z]
+ const wheel_3 = useRef()
+ wheels.push(wheel_3)
+ const wheelInfo_3 = { ...wheelInfo }
+ wheelInfo_3.chassisConnectionPointLocal = [-chassisWidth / 2, chassisHeight, chassisBack]
+ wheelInfo_3.isFrontWheel = false
+ wheelInfos.push(wheelInfo_3)
+ // BackRight [X,Y,-Z]
+ const wheel_4 = useRef()
+ wheels.push(wheel_4)
+ const wheelInfo_4 = { ...wheelInfo }
+ wheelInfo_4.chassisConnectionPointLocal = [chassisWidth / 2, chassisHeight, chassisBack]
+ wheelInfo_4.isFrontWheel = false
+ wheelInfos.push(wheelInfo_4)
+
+ const [vehicle, api] = useRaycastVehicle(() => ({
+ chassisBody: chassis,
+ wheels: wheels,
+ wheelInfos: wheelInfos,
+ indexForwardAxis: 2,
+ indexRightAxis: 0,
+ indexUpAxis: 1,
+ }))
+
+ const forward = useKeyPress('w')
+ // const forward = useKeyPress('z')
+ const backward = useKeyPress('s')
+ const left = useKeyPress('a')
+ // const left = useKeyPress('q')
+ const right = useKeyPress('d')
+ const brake = useKeyPress(' ') // space bar
+ const reset = useKeyPress('r')
+
+ const [steeringValue, setSteeringValue] = useState(0)
+ const [engineForce, setEngineForce] = useState(0)
+ const [brakeForce, setBrakeForce] = useState(0)
+
+ var maxSteerVal = 0.5
+ var maxForce = 1e3
+ var maxBrakeForce = 1e5
+
+ useFrame(() => {
+ if (left && !right) {
+ setSteeringValue(maxSteerVal)
+ } else if (right && !left) {
+ setSteeringValue(-maxSteerVal)
+ } else {
+ setSteeringValue(0)
+ }
+ if (forward && !backward) {
+ setBrakeForce(0)
+ setEngineForce(-maxForce)
+ } else if (backward && !forward) {
+ setBrakeForce(0)
+ setEngineForce(maxForce)
+ } else if (engineForce !== 0) {
+ setEngineForce(0)
+ }
+ if (brake) {
+ setBrakeForce(maxBrakeForce)
+ }
+ if (reset) {
+ chassis.current.api.position.set(0, 5, 0)
+ chassis.current.api.velocity.set(0, 0, 0)
+ chassis.current.api.angularVelocity.set(0, 0.5, 0)
+ chassis.current.api.rotation.set(0, -Math.PI / 4, 0)
+ }
+ })
+
+ useEffect(() => {
+ api.applyEngineForce(engineForce, 2)
+ api.applyEngineForce(engineForce, 3)
+ }, [engineForce])
+ useEffect(() => {
+ api.setSteeringValue(steeringValue, 0)
+ api.setSteeringValue(steeringValue, 1)
+ }, [steeringValue])
+ useEffect(() => {
+ api.setBrake(brakeForce, 0)
+ api.setBrake(brakeForce, 1)
+ api.setBrake(brakeForce, 2)
+ api.setBrake(brakeForce, 3)
+ }, [brakeForce])
+
+ return (
+
+
+
+
+
+
+
+ )
+}
+
+const defaultContactMaterial = {
+ contactEquationRelaxation: 4,
+ friction: 1e-3,
+}
+
+const VehicleScene = () => {
+ return (
+
+ )
+}
+
+// useKeyPress Hook (credit: https://usehooks.com/useKeyPress/)
+function useKeyPress(targetKey) {
+ // State for keeping track of whether key is pressed
+ const [keyPressed, setKeyPressed] = useState(false)
+
+ // If pressed key is our target key then set to true
+ function downHandler({ key }) {
+ if (key === targetKey) {
+ setKeyPressed(true)
+ }
+ }
+
+ // If released key is our target key then set to false
+ const upHandler = ({ key }) => {
+ if (key === targetKey) {
+ setKeyPressed(false)
+ }
+ }
+
+ // Add event listeners
+ useEffect(() => {
+ window.addEventListener('keydown', downHandler)
+ window.addEventListener('keyup', upHandler)
+ // Remove event listeners on cleanup
+ return () => {
+ window.removeEventListener('keydown', downHandler)
+ window.removeEventListener('keyup', upHandler)
+ }
+ }, []) // Empty array ensures that effect is only run on mount and unmount
+
+ return keyPressed
+}
+
+export default VehicleScene
diff --git a/examples/src/demos/index.js b/examples/src/demos/index.js
index a33be717..fb7df4cb 100644
--- a/examples/src/demos/index.js
+++ b/examples/src/demos/index.js
@@ -14,6 +14,7 @@ const Constraints = { descr: '', tags: [], Component: lazy(() => import('./Const
const Chain = { descr: '', tags: [], Component: lazy(() => import('./Chain')), bright: false }
const CompoundBody = { descr: '', tags: [], Component: lazy(() => import('./CompoundBody')), bright: false }
const Raycast = { descr: '', tags: [], Component: lazy(() => import('./Raycast')), bright: false }
+const Vehicle = { descr: '', tags: [], Component: lazy(() => import('./Vehicle')), bright: false }
export {
MondayMorning,
@@ -25,4 +26,5 @@ export {
Constraints,
CompoundBody,
Raycast,
+ Vehicle,
}
diff --git a/src/Provider.tsx b/src/Provider.tsx
index 48f31e7d..10dc979c 100644
--- a/src/Provider.tsx
+++ b/src/Provider.tsx
@@ -30,7 +30,7 @@ type WorkerFrameMessage = {
data: Buffers & {
op: 'frame'
observations: [string, any]
- active: boolean,
+ active: boolean
bodies?: string[]
}
}
diff --git a/src/hooks.ts b/src/hooks.ts
index 87243aad..749a98e1 100644
--- a/src/hooks.ts
+++ b/src/hooks.ts
@@ -505,3 +505,95 @@ export function useRaycastAny(options: RayOptns, callback: (e: Event) => void, d
export function useRaycastAll(options: RayOptns, callback: (e: Event) => void, deps: any[] = []) {
useRay('All', options, callback, deps)
}
+
+type RaycastVehiclePublicApi = {
+ // addWheel: () => number
+ setSteeringValue: (value: number, wheelIndex: number) => void
+ applyEngineForce: (value: number, wheelIndex: number) => void
+ setBrake: (brake: number, wheelIndex: number) => void
+}
+
+type RaycastVehicleApi = [React.MutableRefObject, RaycastVehiclePublicApi]
+
+type WheelInfoOptions = {
+ radius?: number
+ directionLocal?: number[]
+ suspensionStiffness?: number
+ suspensionRestLength?: number
+ maxSuspensionForce?: number
+ maxSuspensionTravel?: number
+ dampingRelaxation?: number
+ dampingCompression?: number
+ frictionSlip?: number
+ rollInfluence?: number
+ axleLocal?: number[]
+ chassisConnectionPointLocal?: number[]
+ isFrontWheel?: boolean
+ useCustomSlidingRotationalSpeed?: boolean
+ customSlidingRotationalSpeed?: number
+}
+
+type RaycastVehicleProps = {
+ chassisBody: React.MutableRefObject
+ wheels: React.MutableRefObject[]
+ wheelInfos: WheelInfoOptions[]
+ indexForwardAxis?: number
+ indexRightAxis?: number
+ indexUpAxis?: number
+}
+
+type RaycastVehicleFn = () => RaycastVehicleProps
+
+export function useRaycastVehicle(
+ fn: RaycastVehicleFn,
+ fwdRef?: React.MutableRefObject
+): RaycastVehicleApi {
+ const ref = fwdRef ? fwdRef : useRef((null as unknown) as THREE.Object3D)
+ const { worker } = useContext(context)
+
+ useLayoutEffect(() => {
+ if (!ref.current) {
+ // When the reference isn't used we create a stub
+ // The body doesn't have a visual representation but can still be constrained
+ ref.current = new THREE.Object3D()
+ }
+
+ const currentWorker = worker
+ let uuid: string[] = [ref.current.uuid]
+
+ const raycastVehicleProps = fn()
+
+ currentWorker.postMessage({
+ op: 'addRaycastVehicle',
+ uuid,
+ props: [
+ raycastVehicleProps.chassisBody.current?.uuid,
+ raycastVehicleProps.wheels.map((wheel) => wheel.current?.uuid),
+ raycastVehicleProps.wheelInfos,
+ raycastVehicleProps?.indexForwardAxis || 2,
+ raycastVehicleProps?.indexRightAxis || 0,
+ raycastVehicleProps?.indexUpAxis || 1,
+ ],
+ })
+ return () => {
+ currentWorker.postMessage({ op: 'removeRaycastVehicle', uuid })
+ }
+ }, [])
+
+ const api = useMemo(() => {
+ const post = (op: string, props?: any) =>
+ ref.current && worker.postMessage({ op, uuid: ref.current.uuid, props })
+ return {
+ setSteeringValue(value: number, wheelIndex: number) {
+ post('setRaycastVehicleSteeringValue', [value, wheelIndex])
+ },
+ applyEngineForce(value: number, wheelIndex: number) {
+ post('applyRaycastVehicleEngineForce', [value, wheelIndex])
+ },
+ setBrake(brake: number, wheelIndex: number) {
+ post('setRaycastVehicleBrake', [brake, wheelIndex])
+ },
+ }
+ }, [])
+ return [ref, api]
+}
diff --git a/src/worker.js b/src/worker.js
index fe2c5eaa..b28251c2 100644
--- a/src/worker.js
+++ b/src/worker.js
@@ -23,9 +23,11 @@ import {
Quaternion,
Ray,
RaycastResult,
+ RaycastVehicle,
} from 'cannon-es'
let bodies = {}
+const vehicles = {}
const springs = {}
const rays = {}
const world = new World()
@@ -435,5 +437,65 @@ self.onmessage = (e) => {
delete rays[uuid]
break
}
+ case 'addRaycastVehicle': {
+ const [chassisBody, wheels, wheelInfos, indexForwardAxis, indexRightAxis, indexUpAxis] = props
+ const vehicle = new RaycastVehicle({
+ chassisBody: bodies[chassisBody],
+ indexForwardAxis: indexForwardAxis,
+ indexRightAxis: indexRightAxis,
+ indexUpAxis: indexUpAxis,
+ })
+ vehicle.world = world
+ for (let i = 0; i < wheelInfos.length; i++) {
+ const wheelInfo = wheelInfos[i]
+ wheelInfo.directionLocal = new Vec3(...wheelInfo.directionLocal)
+ wheelInfo.chassisConnectionPointLocal = new Vec3(...wheelInfo.chassisConnectionPointLocal)
+ wheelInfo.axleLocal = new Vec3(...wheelInfo.axleLocal)
+ vehicle.addWheel(wheelInfo)
+ const wheelBody = bodies[wheels[i]]
+ }
+ vehicles[uuid] = {
+ vehicle: vehicle,
+ wheels: wheels,
+ preStep: () => {
+ vehicles[uuid].vehicle.updateVehicle(world.dt)
+ },
+ postStep: () => {
+ for (let i = 0; i < vehicles[uuid].vehicle.wheelInfos.length; i++) {
+ vehicles[uuid].vehicle.updateWheelTransform(i)
+ const t = vehicles[uuid].vehicle.wheelInfos[i].worldTransform
+ const wheelBody = bodies[vehicles[uuid].wheels[i]]
+ wheelBody.position.copy(t.position)
+ wheelBody.quaternion.copy(t.quaternion)
+ }
+ },
+ }
+ world.addEventListener('preStep', vehicles[uuid].preStep)
+ world.addEventListener('postStep', vehicles[uuid].postStep)
+ break
+ }
+ case 'removeRaycastVehicle': {
+ world.removeEventListener('preStep', vehicles[uuid].preStep)
+ world.removeEventListener('postStep', vehicles[uuid].postStep)
+ vehicles[uuid].vehicle.world = null
+ vehicles[uuid].vehicle = null
+ delete vehicles[uuid]
+ break
+ }
+ case 'setRaycastVehicleSteeringValue': {
+ const [value, wheelIndex] = props
+ vehicles[uuid].vehicle.setSteeringValue(value, wheelIndex)
+ break
+ }
+ case 'applyRaycastVehicleEngineForce': {
+ const [value, wheelIndex] = props
+ vehicles[uuid].vehicle.applyEngineForce(value, wheelIndex)
+ break
+ }
+ case 'setRaycastVehicleBrake': {
+ const [brake, wheelIndex] = props
+ vehicles[uuid].vehicle.setBrake(brake, wheelIndex)
+ break
+ }
}
}