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