diff --git a/apps/examples/package.json b/apps/examples/package.json index ea61078..77ec11c 100644 --- a/apps/examples/package.json +++ b/apps/examples/package.json @@ -15,6 +15,8 @@ "@react-three/jolt-addons": "0.0.1", "@react-three/jolt-controllers": "0.0.1", "leva": "^0.9.35", + "maath": "^0.10.7", + "postprocessing": "^6.35.3", "r3f-perf": "^7.2.1", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/apps/examples/src/App.tsx b/apps/examples/src/App.tsx index 3ac105f..2721bcd 100644 --- a/apps/examples/src/App.tsx +++ b/apps/examples/src/App.tsx @@ -1,277 +1,258 @@ // Base demo copied from r3/rapier //import * as THREE from 'three'; -import { Environment, CameraControls } from '@react-three/drei'; -import { Canvas, useThree } from '@react-three/fiber'; -import { vec3 } from '@react-three/jolt'; -import { Perf } from 'r3f-perf'; +import { Environment, CameraControls } from "@react-three/drei"; +import { Canvas, useThree } from "@react-three/fiber"; +import { vec3 } from "@react-three/jolt"; +import { Perf } from "r3f-perf"; import { - //ReactNode, - //StrictMode, - Suspense, - createContext, - useContext, - useEffect, - //useRef, - //useEffect, - useState, -} from 'react'; -import { - NavLink, - NavLinkProps, - Route, - Routes, - useLocation, -} from 'react-router-dom'; + //ReactNode, + //StrictMode, + Suspense, + createContext, + useContext, + useEffect, + //useRef, + //useEffect, + useState +} from "react"; +import { NavLink, NavLinkProps, Route, Routes, useLocation } from "react-router-dom"; //* All the examples ------------------------------ -import { RaycastManyDemo } from './examples/RaycastManyDemo'; -import { RaycastSimpleDemo } from './examples/RaycastSimpleDemo'; -import { JustBoxes } from './examples/JustBoxes'; -import { HeightfieldDemo } from './examples/Heightfield'; -import { CubeHeap } from './examples/CubeHeap'; -import { FourWheelDemo } from './examples/FourWheelsWithHeightmap'; -import { CharacterVirtualDemo } from './examples/CharacterVirtualDemo'; -//try to import a local module of jolt -// see issue #71 -//import initJolt from './jolt/Distribution/jolt-physics.wasm-compat.js'; +import { RaycastManyDemo } from "./examples/RaycastManyDemo"; +import { RaycastSimpleDemo } from "./examples/RaycastSimpleDemo"; +import { JustBoxes } from "./examples/JustBoxes"; +import { HeightfieldDemo } from "./examples/Heightfield"; +import { CubeHeap } from "./examples/CubeHeap"; +import { FourWheelDemo } from "./examples/FourWheelsWithHeightmap"; +import { CharacterVirtualDemo } from "./examples/CharacterVirtualDemo"; +import { Impulses } from "./examples/Impulses"; const demoContext = createContext<{ - debug: boolean; - paused: boolean; - interpolate: boolean; - physicsKey: number; + debug: boolean; + paused: boolean; + interpolate: boolean; + physicsKey: number; }>({ debug: false, paused: false, interpolate: true, physicsKey: 0 }); export const useDemo = () => useContext(demoContext); const ToggleButton = ({ - label, - value, - onClick, + label, + value, + onClick }: { - label: string; - value: boolean; - onClick(): void; + label: string; + value: boolean; + onClick(): void; }) => ( - + ); //* Controls Wrapper. We have to do this to get root state export function ControlWrapper(props: any) { - const { position = [0, 10, 10], target = [0, 1, 0], ...rest } = props; - const { controls } = useThree(); - useEffect(() => { - const newPosition = vec3.three(position); - const newTarget = vec3.three(target); - if (controls) - //@ts-ignore can't get the types to work here - controls.setLookAt( - newPosition.x, - newPosition.y, - newPosition.z, - newTarget.x, - newTarget.y, - newTarget.z, - true - ); - }, [position]); - return ; + const { position = [0, 10, 10], target = [0, 1, 0], ...rest } = props; + const { controls } = useThree(); + useEffect(() => { + const newPosition = vec3.three(position); + const newTarget = vec3.three(target); + if (controls) + //@ts-ignore can't get the types to work here + controls.setLookAt( + newPosition.x, + newPosition.y, + newPosition.z, + newTarget.x, + newTarget.y, + newTarget.z, + true + ); + }, [position]); + return ; } type Routes = { - [key: string]: { - position?: number[]; - target?: number[]; - background?: string; - element: JSX.Element; - }; + [key: string]: { + position?: number[]; + target?: number[]; + background?: string; + element: JSX.Element; + }; }; const routes: Routes = { - '': { - position: [2, 5, 30], - target: [0, 1, 10], - background: '#f0544f', - element: , - }, + "": { + position: [2, 5, 30], + target: [0, 1, 10], + background: "#f0544f", + element: + }, - RaycastMany: { - position: [0, 0, 5], - target: [0, 0, 0], - background: '#3d405b', - element: , - }, - Heightfield: { - position: [150, 110, 150], - target: [0, 0, 0], - background: '#3d405b', - element: , - }, - CubeHeap: { - position: [2, 25, 51], - target: [0, 1, 10], - background: '#3d405b', - element: , - }, - Vehicle: { - position: [2, 25, 51], - target: [0, 1, 10], - background: '#3d405b', - element: , - }, - Character: { - position: [2, 25, 51], - target: [0, 1, 10], - background: '#3d405b', - element: , - }, - // just for current dev purposes - Boxes: { - position: [-10, 5, 15], - target: [0, 1, 10], - background: '#3d405b', - element: , - }, + RaycastMany: { + position: [0, 0, 5], + target: [0, 0, 0], + background: "#3d405b", + element: + }, + Heightfield: { + position: [150, 110, 150], + target: [0, 0, 0], + background: "#3d405b", + element: + }, + CubeHeap: { + position: [2, 25, 51], + target: [0, 1, 10], + background: "#3d405b", + element: + }, + Vehicle: { + position: [2, 25, 51], + target: [0, 1, 10], + background: "#3d405b", + element: + }, + Character: { + position: [2, 25, 51], + target: [0, 1, 10], + background: "#3d405b", + element: + }, + // just for current dev purposes + Boxes: { + position: [-10, 5, 15], + target: [0, 1, 10], + background: "#3d405b", + element: + }, + Impulses: { + position: [0, 0, 20], + target: [0, 0, 0], + background: "#141622", + element: + } }; export const App = () => { - // state - const [debug, setDebug] = useState(false); - const [perf, setPerf] = useState(false); - const [paused, setPaused] = useState(false); - const [interpolate, setInterpolate] = useState(true); - const [physicsKey, setPhysicsKey] = useState(0); + // state + const [debug, setDebug] = useState(false); + const [perf, setPerf] = useState(false); + const [paused, setPaused] = useState(false); + const [interpolate, setInterpolate] = useState(true); + const [physicsKey, setPhysicsKey] = useState(0); - // visuals - const [background, setBackground] = useState('#3d405b'); - const [cameraProps, setCameraProps] = useState<{ - position: any; - target: any; - } | null>(null); - const location = useLocation(); + // visuals + const [background, setBackground] = useState("#3d405b"); + const [cameraProps, setCameraProps] = useState<{ + position: any; + target: any; + } | null>(null); + const location = useLocation(); - // this triggers a reset of the physics world - const updatePhysicsKey = () => { - setPhysicsKey((current) => current + 1); - }; + // this triggers a reset of the physics world + const updatePhysicsKey = () => { + setPhysicsKey((current) => current + 1); + }; - // when the route changes move the camera - useEffect(() => { - // set the camera position - //@ts-ignore - const route = routes[location.pathname.replace('/', '')]; - setCameraProps({ position: route.position, target: route.target }); - setBackground(route.background || '#3d405b'); - }, [location]); + // when the route changes move the camera + useEffect(() => { + // set the camera position + //@ts-ignore + const route = routes[location.pathname.replace("/", "")]; + setCameraProps({ position: route.position, target: route.target }); + setBackground(route.background || "#3d405b"); + }, [location]); - return ( -
- - - - - + return ( +
+ + + + + - - - - {Object.keys(routes).map((key) => ( - - ))} - - - {perf && } - - + + + + {Object.keys(routes).map((key) => ( + + ))} + + + {perf && } + + -
- {Object.keys(routes).map((key) => ( - - {key.replace(/-/g, ' ') || 'Raycaster'} - - ))} +
+ {Object.keys(routes).map((key) => ( + + {key.replace(/-/g, " ") || "Raycaster"} + + ))} - setDebug((v) => !v)} - /> - setPerf((v) => !v)} - /> - setPaused((v) => !v)} - /> - setInterpolate((v) => !v)} - /> - -
-
- ); + setDebug((v) => !v)} /> + setPerf((v) => !v)} /> + setPaused((v) => !v)} /> + setInterpolate((v) => !v)} + /> + +
+
+ ); }; const Link = (props: NavLinkProps) => { - return ( - ({ - border: '2px solid #311847', - textTransform: 'capitalize', - borderRadius: 4, - padding: 4, - background: isActive ? '#311847' : 'transparent', - textDecoration: 'none', - color: isActive ? 'white' : '#311847', - })} - /> - ); + return ( + ({ + border: "2px solid #311847", + textTransform: "capitalize", + borderRadius: 4, + padding: 4, + background: isActive ? "#311847" : "transparent", + textDecoration: "none", + color: isActive ? "white" : "#311847" + })} + /> + ); }; diff --git a/apps/examples/src/examples/Impulses.tsx b/apps/examples/src/examples/Impulses.tsx new file mode 100644 index 0000000..19e7d0b --- /dev/null +++ b/apps/examples/src/examples/Impulses.tsx @@ -0,0 +1,168 @@ +import { BodyState, Physics, RigidBody } from "@react-three/jolt"; +import { useDemo } from "../App"; +import { useRef, useMemo, useReducer, useEffect } from "react"; +import * as THREE from "three"; +import { Environment, Lightformer } from "@react-three/drei"; +import { useFrame, useThree } from "@react-three/fiber"; +import { easing } from "maath"; + +// for random +const r = THREE.MathUtils.randFloatSpread; +const accents = ["#ff4060", "#ffcc00", "#20ffa0", "#4060ff"]; +const shuffle = (accent = 0) => [ + { color: "#444", roughness: 0.1, metalness: 0.5 }, + { color: "#444", roughness: 0.1, metalness: 0.5 }, + { color: "#444", roughness: 0.1, metalness: 0.5 }, + { color: "white", roughness: 0.1, metalness: 0.1 }, + { color: "white", roughness: 0.1, metalness: 0.1 }, + { color: "white", roughness: 0.1, metalness: 0.1 }, + { color: accents[accent], roughness: 0.1, accent: true }, + { color: accents[accent], roughness: 0.1, accent: true }, + { color: accents[accent], roughness: 0.1, accent: true }, + { color: "#444", roughness: 0.1 }, + { color: "#444", roughness: 0.3 }, + { color: "#444", roughness: 0.3 }, + { color: "white", roughness: 0.1 }, + { color: "white", roughness: 0.2 }, + { color: "white", roughness: 0.1 }, + { color: accents[accent], roughness: 0.1, accent: true, transparent: true, opacity: 0.5 }, + { color: accents[accent], roughness: 0.3, accent: true }, + { color: accents[accent], roughness: 0.1, accent: true } +]; + +export function Impulses() { + const { debug, paused, interpolate, physicsKey } = useDemo(); + const [accent, click] = useReducer((state) => ++state % accents.length, 0); + const connectors = useMemo(() => shuffle(accent), [accent]); + const { gl } = useThree(); + // setup onclick from outside the canvas + useEffect(() => { + gl.domElement.addEventListener("click", click); + return () => gl.domElement.removeEventListener("click", click); + }, [gl]); + const defaultBodySettings = { + mRestitution: 0.1 + }; + + return ( + <> + + + {connectors.map( + ( + props, + i //@ts-ignore biome-ignore Sphere props + ) => ( + + ) + )} + + + + + + + + + self.lookAt(0, 0, 0)} + position={[10, 10, 0]} + scale={10} + /> + + + + ); +} + +function Sphere({ accent = false, color = "white", ...props }) { + const bodyRef = useRef(); + const meshRef = useRef(null); + const pos = useMemo(() => [r(10), r(10), r(10)], []); + useFrame((_state: any, inDelta: number) => { + if (!bodyRef.current || !meshRef.current) return; + const delta = Math.min(0.1, inDelta); + const body = bodyRef.current as BodyState; + body.addImpulse(body.position.clone().negate().multiplyScalar(0.2)); + //@ts-ignore + easing.dampC(meshRef.current.material.color, color, 0.2, delta); + }); + return ( + + + + + + + ); +} + +function Pointer() { + const bodyRef = useRef(null); + const { camera } = useThree(); + useFrame(({ pointer, viewport }, deltaTime) => { + if (!bodyRef.current) return; + const body = bodyRef.current as BodyState; + // with this setup 20 is a good distance to the center + const distance = camera.position.distanceTo(new THREE.Vector3(0, 0, 0)); + const factor = 20 / distance; + const pointerVector = new THREE.Vector3( + (pointer.x * viewport.width) / 2 / factor, + (pointer.y * viewport.height) / 2 / factor, + 1 - distance + ); + //apply the camera space to the vector + pointerVector.applyMatrix4(camera.matrixWorld); + // move the pointer with kinematic force + body.moveKinematic(pointerVector, undefined, deltaTime); + }); + return ( + + + + + + + ); +} diff --git a/packages/react-three-jolt/src/components/RigidBody.tsx b/packages/react-three-jolt/src/components/RigidBody.tsx index 8284de1..24572f8 100644 --- a/packages/react-three-jolt/src/components/RigidBody.tsx +++ b/packages/react-three-jolt/src/components/RigidBody.tsx @@ -35,6 +35,11 @@ interface RigidBodyProps { obstructionTimelimit?: number; isSensor?: boolean; + //physics props + linearDamping?: number; + angularDamping?: number; + friction?: number; + //TODO: do these work yet? scale?: number[]; mass?: number; @@ -66,6 +71,8 @@ export const RigidBody: React.FC = memo( mass, quaternion, isSensor, + angularDamping, + linearDamping, // obstruction allowObstruction, @@ -164,6 +171,8 @@ export const RigidBody: React.FC = memo( const body = rigidBodyRef.current as BodyState; //@ts-ignore if (mass) bodySystem.setMass(body.handle, mass); + if (linearDamping) body.linearDamping = linearDamping; + if (angularDamping) body.angularDamping = angularDamping; // check if the body is allowing obstruction const isAllowing = body.allowObstruction; @@ -177,7 +186,14 @@ export const RigidBody: React.FC = memo( body.obstructionTimelimit = obstructionTimelimit; } } - }, [mass, allowObstruction, obstructionTimelimit]); + }, [ + mass, + allowObstruction, + obstructionTimelimit, + linearDamping, + angularDamping, + rigidBodyRef + ]); // the context should update when a new handle is added //@ts-ignore diff --git a/packages/react-three-jolt/src/systems/body-state.ts b/packages/react-three-jolt/src/systems/body-state.ts index bd52fb1..3434d28 100644 --- a/packages/react-three-jolt/src/systems/body-state.ts +++ b/packages/react-three-jolt/src/systems/body-state.ts @@ -280,10 +280,34 @@ export class BodyState { get restitution() { return this.body.GetRestitution(); } + get angularDamping() { + return this.body.GetMotionProperties().GetAngularDamping(); + } + set angularDamping(damping: number) { + this.body.GetMotionProperties().SetAngularDamping(damping); + } + get linearDamping() { + return this.body.GetMotionProperties().GetLinearDamping(); + } + set linearDamping(damping: number) { + this.body.GetMotionProperties().SetLinearDamping(damping); + } + get gravityFactor() { + return this.body.GetMotionProperties().GetGravityFactor(); + } + set gravityFactor(factor: number) { + this.body.GetMotionProperties().SetGravityFactor(factor); + } + get mass() { + return this.body.GetShape().GetMassProperties().mMass; + } + set mass(mass: number) { + this.bodySystem.setMass(this.handle, mass); + } //* Force Manipulation ---------------------------------- // apply a force to the body - applyForce(force: Vector3) { + addForce(force: Vector3) { const newVec = vec3.jolt(force); this.body.AddForce(newVec); Raw.module.destroy(newVec); @@ -297,10 +321,12 @@ export class BodyState { addImpulse(impulse: Vector3) { const newVec = vec3.jolt(impulse); this.bodyInterface.AddImpulse(this.BodyID, newVec); + //we have to activate the body after applying impulse + //this.bodyInterface.ActivateBody(this.BodyID); Raw.module.destroy(newVec); } //move kinematic - moveKinematic(position: Vector3, rotation: THREE.Quaternion, deltaTime = 0) { + moveKinematic(position: Vector3, rotation = this.rotation, deltaTime = 0.16) { const newVec = vec3.jolt(position); const newQuat = quat.jolt(rotation); diff --git a/packages/react-three-jolt/src/systems/body-system.ts b/packages/react-three-jolt/src/systems/body-system.ts index d28783b..1525c77 100644 --- a/packages/react-three-jolt/src/systems/body-system.ts +++ b/packages/react-three-jolt/src/systems/body-system.ts @@ -1,582 +1,588 @@ // This class holds the bodies and the management of them -import type Jolt from 'jolt-physics'; +import type Jolt from "jolt-physics"; import { - //MathUtils, - // Matrix4, - Object3D, - // Quaternion, - Vector3, - InstancedMesh -} from 'three'; -import * as THREE from 'three'; -import { Raw } from '../raw'; - -import { vec3, quat } from '../utils'; -import { BodyState } from './body-state'; -import { Layer } from '../constants'; + //MathUtils, + // Matrix4, + Object3D, + // Quaternion, + Vector3, + InstancedMesh +} from "three"; +import * as THREE from "three"; +import { Raw } from "../raw"; + +import { vec3, quat } from "../utils"; +import { BodyState } from "./body-state"; +import { Layer } from "../constants"; import { - ShapeSystem, - getShapeSettingsFromObject, - generateHeightfieldShapeFromThree, - createMeshForShape, - AutoShape -} from './shape-system'; + ShapeSystem, + getShapeSettingsFromObject, + generateHeightfieldShapeFromThree, + createMeshForShape, + AutoShape +} from "./shape-system"; // TYPES ======================================== -export type BodyType = 'dynamic' | 'static' | 'kinematic' | 'rig'; +export type BodyType = "dynamic" | "static" | "kinematic" | "rig"; // We call things "bodySettings" to clarify from shapes or other similar labels export interface GenerateBodyOptions { - bodyType?: 'dynamic' | 'static' | 'kinematic' | 'rig'; - bodySettings?: Jolt.BodyCreationSettings; - motionType?: 'static' | 'kinematic' | 'dynamic'; - index?: number; - shapeType?: AutoShape; - activation?: 'activate' | 'deactivate'; - jitter?: THREE.Vector3; - mass?: number; - size?: THREE.Vector3; + bodyType?: "dynamic" | "static" | "kinematic" | "rig"; + bodySettings?: Jolt.BodyCreationSettings; + motionType?: "static" | "kinematic" | "dynamic"; + index?: number; + shapeType?: AutoShape; + activation?: "activate" | "deactivate"; + jitter?: THREE.Vector3; + mass?: number; + size?: THREE.Vector3; } // ================================================ export class BodySystem { - jolt = Raw.module; - dynamicBodies = new Map(); - staticBodies = new Map(); - kinematicBodies = new Map(); - - joltPhysicsSystem: Jolt.PhysicsSystem; - bodyInterface: Jolt.BodyInterface; - - shapeSystem: ShapeSystem; - - // lets defaults be set at the physics system level - defaultBodySettings: any = {}; - - constructor(joltPhysicsSystem: Jolt.PhysicsSystem) { - // set the interfaces - this.joltPhysicsSystem = joltPhysicsSystem; - this.bodyInterface = this.joltPhysicsSystem.GetBodyInterface(); - - this.shapeSystem = new ShapeSystem(this.joltPhysicsSystem); - - // Activate the listeners - // this.initializeActivationListeners(); - // this.initializeContactListeners(); - } - // create a body from an object - createBody(object: Object3D, options: GenerateBodyOptions = {}): Jolt.Body { - let settings = generateBodySettings(object, options); - // if there are properties in the default, merge them with settings - if (Object.keys(this.defaultBodySettings).length > 0) { - settings = mergeBodyCreationSettings(settings, this.defaultBodySettings); - } - const body = this.bodyInterface.CreateBody(settings); - // remove the settings - this.jolt.destroy(settings); - return body; - } - addBody(object: Object3D, options?: GenerateBodyOptions) { - const body = this.createBody(object, options); - //console.log('adding body', object, body); - return this.addExistingBody(object, body, options); - } - // add an EXISTING Jolt body to the system - addExistingBody( - object: Object3D | InstancedMesh, - body: Jolt.Body, - options?: GenerateBodyOptions - ): number { - const state = new BodyState(object, body, this.joltPhysicsSystem, this, options?.index); - // generate the handle - const handle = body.GetID().GetIndexAndSequenceNumber(); - // console.log('adding body', handle, options, state, object, body); - // add to the correct map - if (options?.bodyType === 'static') this.staticBodies.set(handle, state); - else if (options?.bodyType === 'kinematic') this.kinematicBodies.set(handle, state); - else this.dynamicBodies.set(handle, state); - - // allow to add the body but not activate it - let activationState = Raw.module.EActivation_Activate; - if (options?.activation) { - switch (options.activation) { - case 'activate': - activationState = Raw.module.EActivation_Activate; - break; - case 'deactivate': - activationState = Raw.module.EActivation_DontActivate; - break; - } - } - - // VERY IMPORTANT! ADD TO THE ACTUAL SIMULATION - this.bodyInterface.AddBody(body.GetID(), activationState); - - return handle; - } - getBody(handle: number) { - return ( - this.dynamicBodies.get(handle) || - this.staticBodies.get(handle) || - this.kinematicBodies.get(handle) - ); - } - removeBody(bodyHandle: number, ignoreThree = false) { - //console.log('Trying to remove body', bodyHandle); - // get the body so we can process it - const bodyState = this.getBody(bodyHandle); - if (!bodyState) return; - // check if the body exists in the simulation - // first check the simulation is still here (might be removed after physics is removed) - if (!this.joltPhysicsSystem) return; - if (!this.bodyInterface) return; - const bodyID = bodyState.body.GetID(); - const body = this.joltPhysicsSystem.GetBodyLockInterfaceNoLock().TryGetBody(bodyID); - if (!body) { - console.warn('body getter failed during delete', bodyHandle); - return; - } - - if (!this.bodyInterface.IsAdded(bodyID)) { - // console.log('body already removed'); - return; - } - - // remove the body from the simulation - this.bodyInterface.RemoveBody(bodyID); - // destroy it - this.bodyInterface.DestroyBody(bodyID); - // remove it from threeJS by removing it from it's parent - // only if its not an instanced mesh or explicitly told to ignore. - if (!bodyState.isInstance || ignoreThree) bodyState.object.parent?.remove(bodyState.object); - // remove it from the maps - - this.dynamicBodies.delete(bodyHandle); - this.staticBodies.delete(bodyHandle); - this.kinematicBodies.delete(bodyHandle); - // console.log('Removed body', bodyHandle); - } - - // There's probably a better pattern, but im making my own function for this - public addHeightfield(planeMesh: THREE.Mesh): number { - //const position = vec3.threeToJolt(planeMesh.position); - // const quaternion = quat.threeToJolt(planeMesh.quaternion); - const shapeSettings = generateHeightfieldShapeFromThree(planeMesh); - //const position = new Raw.module.Vec3(0, -20, 0); // The image tends towards 'white', so offset it down closer to zero - const quaternion = new Raw.module.Quat(0, 0, 0, 1); - //@ts-ignore - const shape: Jolt.HeightFieldShape = shapeSettings.Create().Get(); - const size = shapeSettings.mSampleCount; - //@ts-ignore yes it does exist - const planeWidth = planeMesh.geometry.parameters.width; - const scale = planeWidth / size; - const offset = -size * scale * 0.5; - const position = new Raw.module.Vec3( - offset + planeMesh.position.x, - planeMesh.position.y, - planeMesh.position.z + offset - ); - - const creationSettings = new Raw.module.BodyCreationSettings( - shape, - position, - quaternion, - Raw.module.EMotionType_Static, - Layer.NON_MOVING - ); - const body = this.bodyInterface.CreateBody(creationSettings); - // cleanup before returning - this.jolt.destroy(shapeSettings); - this.jolt.destroy(creationSettings); - //TODO: One of these causes a crash. - // this.jolt.destroy(position); - //this.jolt.destroy(quaternion); - //this.jolt.destroy(shape); - return this.addExistingBody(planeMesh, body, { bodyType: 'static' }); - } - // Body Modification =================================== - // change the mass of a body - setMass(bodyHandle: number, mass: number) { - const body = this.getBody(bodyHandle); - if (!body) return; - changeMassInertia(body.body, mass); - } - - // Activation Listeners ================================ - //@ts-ignore - private initializeActivationListeners() { - const activationListener = new Raw.module.BodyActivationListenerJS(); - //@ts-ignore - activationListener.OnBodyActivated = (bodyId: Jolt.BodyID) => { - //@ts-ignore wrapPointer bug - bodyId = Raw.module.wrapPointer(bodyId, Raw.module.BodyID); - this.triggerActivationListeners(bodyId.GetIndexAndSequenceNumber()); - }; - //@ts-ignore - activationListener.OnBodyDeactivated = (bodyId: Jolt.BodyID) => { - //@ts-ignore wrapPointer bug - bodyId = Raw.module.wrapPointer(bodyId, Raw.module.BodyID); - this.triggerActivationListeners(bodyId.GetIndexAndSequenceNumber()); - }; - - this.joltPhysicsSystem.SetBodyActivationListener(activationListener); - } - private triggerActivationListeners(handle: number) { - // go through the body system and trigger the activation listeners{ - const body = this.getBody(handle); - if (!body) return; - - body.activationListeners.forEach((listener) => listener(body)); - } - - // Contact Listeners =================================== - //@ts-ignore - private initializeContactListeners() { - const contactListener = new Raw.module.ContactListenerJS(); - //@ts-ignore - contactListener.OnContactAdded = ( - body1: Jolt.Body, - body2: Jolt.Body, - manifold: Jolt.ContactManifold, - settings: Jolt.ContactSettings - ) => { - //@ts-ignore - body1 = Raw.module.wrapPointer(body1, Raw.module.Body); - //@ts-ignore - body2 = Raw.module.wrapPointer(body2, Raw.module.Body); - const body1Handle = body1.GetID().GetIndexAndSequenceNumber(); - const body2Handle = body2.GetID().GetIndexAndSequenceNumber(); - //@ts-ignore - manifold = Raw.module.wrapPointer(manifold, Raw.module.ContactManifold); - //@ts-ignore - settings = Raw.module.wrapPointer(settings, Raw.module.ContactSettings); - // get the contact count, if it doesn't exist we are creating it - const body1State = this.getBody(body1Handle); - //@ts-ignore - let isContacting = body1State.isContacting(body2Handle); - - // check the body contact threshold as we may have JUST been in contact - // even if isCOntacting is 0 - if (isContacting === 0) { - const timestamp = Date.now(); - const lastFinalRemoval = - //@ts-ignore it'll come back - body1State!.contactTimestamps.get(body2Handle); - if ( - lastFinalRemoval && - //@ts-ignore - timestamp - lastFinalRemoval < body1State.contactThreshold - ) { - isContacting = 1; - } - } - - // add the contact pair - const numContacts = this.addContactPair( - body1.GetID().GetIndexAndSequenceNumber(), - body2.GetID().GetIndexAndSequenceNumber() - ); - /* This doesn't seem to work + jolt = Raw.module; + dynamicBodies = new Map(); + staticBodies = new Map(); + kinematicBodies = new Map(); + + joltPhysicsSystem: Jolt.PhysicsSystem; + bodyInterface: Jolt.BodyInterface; + + shapeSystem: ShapeSystem; + + // lets defaults be set at the physics system level + defaultBodySettings: any = {}; + + constructor(joltPhysicsSystem: Jolt.PhysicsSystem) { + // set the interfaces + this.joltPhysicsSystem = joltPhysicsSystem; + this.bodyInterface = this.joltPhysicsSystem.GetBodyInterface(); + + this.shapeSystem = new ShapeSystem(this.joltPhysicsSystem); + + // Activate the listeners + // this.initializeActivationListeners(); + // this.initializeContactListeners(); + } + // create a body from an object + createBody(object: Object3D, options: GenerateBodyOptions = {}): Jolt.Body { + let settings = generateBodySettings(object, options); + // if there are properties in the default, merge them with settings + if (Object.keys(this.defaultBodySettings).length > 0) { + settings = mergeBodyCreationSettings(settings, this.defaultBodySettings); + } + const body = this.bodyInterface.CreateBody(settings); + // remove the settings + this.jolt.destroy(settings); + return body; + } + addBody(object: Object3D, options?: GenerateBodyOptions) { + const body = this.createBody(object, options); + //console.log('adding body', object, body); + return this.addExistingBody(object, body, options); + } + // add an EXISTING Jolt body to the system + addExistingBody( + object: Object3D | InstancedMesh, + body: Jolt.Body, + options?: GenerateBodyOptions + ): number { + const state = new BodyState(object, body, this.joltPhysicsSystem, this, options?.index); + // generate the handle + const handle = body.GetID().GetIndexAndSequenceNumber(); + // console.log('adding body', handle, options, state, object, body); + // add to the correct map + if (options?.bodyType === "static") this.staticBodies.set(handle, state); + else if (options?.bodyType === "kinematic") this.kinematicBodies.set(handle, state); + else this.dynamicBodies.set(handle, state); + + // allow to add the body but not activate it + let activationState = Raw.module.EActivation_Activate; + if (options?.activation) { + switch (options.activation) { + case "activate": + activationState = Raw.module.EActivation_Activate; + break; + case "deactivate": + activationState = Raw.module.EActivation_DontActivate; + break; + } + } + + // VERY IMPORTANT! ADD TO THE ACTUAL SIMULATION + this.bodyInterface.AddBody(body.GetID(), activationState); + return handle; + } + getBody(handle: number) { + return ( + this.dynamicBodies.get(handle) || + this.staticBodies.get(handle) || + this.kinematicBodies.get(handle) + ); + } + removeBody(bodyHandle: number, ignoreThree = false) { + //console.log('Trying to remove body', bodyHandle); + // get the body so we can process it + const bodyState = this.getBody(bodyHandle); + if (!bodyState) return; + // check if the body exists in the simulation + // first check the simulation is still here (might be removed after physics is removed) + if (!this.joltPhysicsSystem) return; + if (!this.bodyInterface) return; + const bodyID = bodyState.body.GetID(); + const body = this.joltPhysicsSystem.GetBodyLockInterfaceNoLock().TryGetBody(bodyID); + if (!body) { + console.warn("body getter failed during delete", bodyHandle); + return; + } + + if (!this.bodyInterface.IsAdded(bodyID)) { + // console.log('body already removed'); + return; + } + + // remove the body from the simulation + this.bodyInterface.RemoveBody(bodyID); + // destroy it + this.bodyInterface.DestroyBody(bodyID); + // remove it from threeJS by removing it from it's parent + // only if its not an instanced mesh or explicitly told to ignore. + if (!bodyState.isInstance || ignoreThree) bodyState.object.parent?.remove(bodyState.object); + // remove it from the maps + + this.dynamicBodies.delete(bodyHandle); + this.staticBodies.delete(bodyHandle); + this.kinematicBodies.delete(bodyHandle); + // console.log('Removed body', bodyHandle); + } + + // There's probably a better pattern, but im making my own function for this + public addHeightfield(planeMesh: THREE.Mesh): number { + //const position = vec3.threeToJolt(planeMesh.position); + // const quaternion = quat.threeToJolt(planeMesh.quaternion); + const shapeSettings = generateHeightfieldShapeFromThree(planeMesh); + //const position = new Raw.module.Vec3(0, -20, 0); // The image tends towards 'white', so offset it down closer to zero + const quaternion = new Raw.module.Quat(0, 0, 0, 1); + //@ts-ignore + const shape: Jolt.HeightFieldShape = shapeSettings.Create().Get(); + const size = shapeSettings.mSampleCount; + //@ts-ignore yes it does exist + const planeWidth = planeMesh.geometry.parameters.width; + const scale = planeWidth / size; + const offset = -size * scale * 0.5; + const position = new Raw.module.Vec3( + offset + planeMesh.position.x, + planeMesh.position.y, + planeMesh.position.z + offset + ); + + const creationSettings = new Raw.module.BodyCreationSettings( + shape, + position, + quaternion, + Raw.module.EMotionType_Static, + Layer.NON_MOVING + ); + const body = this.bodyInterface.CreateBody(creationSettings); + // cleanup before returning + this.jolt.destroy(shapeSettings); + this.jolt.destroy(creationSettings); + //TODO: One of these causes a crash. + // this.jolt.destroy(position); + //this.jolt.destroy(quaternion); + //this.jolt.destroy(shape); + return this.addExistingBody(planeMesh, body, { bodyType: "static" }); + } + // Body Modification =================================== + // change the mass of a body + setMass(bodyHandle: number, mass: number) { + const body = this.getBody(bodyHandle); + if (!body) return; + changeMassInertia(body.body, mass); + } + + // Activation Listeners ================================ + //@ts-ignore + private initializeActivationListeners() { + const activationListener = new Raw.module.BodyActivationListenerJS(); + //@ts-ignore + activationListener.OnBodyActivated = (bodyId: Jolt.BodyID) => { + //@ts-ignore wrapPointer bug + bodyId = Raw.module.wrapPointer(bodyId, Raw.module.BodyID); + this.triggerActivationListeners(bodyId.GetIndexAndSequenceNumber()); + }; + //@ts-ignore + activationListener.OnBodyDeactivated = (bodyId: Jolt.BodyID) => { + //@ts-ignore wrapPointer bug + bodyId = Raw.module.wrapPointer(bodyId, Raw.module.BodyID); + this.triggerActivationListeners(bodyId.GetIndexAndSequenceNumber()); + }; + + this.joltPhysicsSystem.SetBodyActivationListener(activationListener); + } + private triggerActivationListeners(handle: number) { + // go through the body system and trigger the activation listeners{ + const body = this.getBody(handle); + if (!body) return; + + body.activationListeners.forEach((listener) => listener(body)); + } + + // Contact Listeners =================================== + //@ts-ignore + private initializeContactListeners() { + const contactListener = new Raw.module.ContactListenerJS(); + //@ts-ignore + contactListener.OnContactAdded = ( + body1: Jolt.Body, + body2: Jolt.Body, + manifold: Jolt.ContactManifold, + settings: Jolt.ContactSettings + ) => { + //@ts-ignore + body1 = Raw.module.wrapPointer(body1, Raw.module.Body); + //@ts-ignore + body2 = Raw.module.wrapPointer(body2, Raw.module.Body); + const body1Handle = body1.GetID().GetIndexAndSequenceNumber(); + const body2Handle = body2.GetID().GetIndexAndSequenceNumber(); + //@ts-ignore + manifold = Raw.module.wrapPointer(manifold, Raw.module.ContactManifold); + //@ts-ignore + settings = Raw.module.wrapPointer(settings, Raw.module.ContactSettings); + // get the contact count, if it doesn't exist we are creating it + const body1State = this.getBody(body1Handle); + //@ts-ignore + let isContacting = body1State.isContacting(body2Handle); + + // check the body contact threshold as we may have JUST been in contact + // even if isCOntacting is 0 + if (isContacting === 0) { + const timestamp = Date.now(); + const lastFinalRemoval = + //@ts-ignore it'll come back + body1State!.contactTimestamps.get(body2Handle); + if ( + lastFinalRemoval && + //@ts-ignore + timestamp - lastFinalRemoval < body1State.contactThreshold + ) { + isContacting = 1; + } + } + + // add the contact pair + const numContacts = this.addContactPair( + body1.GetID().GetIndexAndSequenceNumber(), + body2.GetID().GetIndexAndSequenceNumber() + ); + /* This doesn't seem to work const subshape1 = manifold.mSubShapeID1; const subshape2 = manifold.mSubShapeID2; console.log('subshape1', subshape1.GetValue()); */ - this.triggerContactListeners( - body1.GetID().GetIndexAndSequenceNumber(), - body2.GetID().GetIndexAndSequenceNumber(), - 'added', - isContacting ? 'additional' : 'new', // 'new' or 'additional' - numContacts, - manifold, - settings - ); - }; - //@ts-ignore - contactListener.OnContactPersisted = ( - body1: Jolt.Body, - body2: Jolt.Body, - manifold: Jolt.ContactManifold, - settings: Jolt.ContactSettings - ) => { - //@ts-ignore - body1 = Raw.module.wrapPointer(body1, Raw.module.Body); - //@ts-ignore - body2 = Raw.module.wrapPointer(body2, Raw.module.Body); - //@ts-ignore - manifold = Raw.module.wrapPointer(manifold, Raw.module.ContactManifold); - //@ts-ignore - settings = Raw.module.wrapPointer(settings, Raw.module.ContactSettings); - //@ts-ignore - const numContacts = this.getBody( - body1.GetID().GetIndexAndSequenceNumber() - ).isContacting(body2.GetID().GetIndexAndSequenceNumber()); - this.triggerContactListeners( - body1.GetID().GetIndexAndSequenceNumber(), - body2.GetID().GetIndexAndSequenceNumber(), - 'persisted', - 'persisted', - numContacts, - manifold, - settings - ); - }; - // removed uses a weird subshapepair argument - //@ts-ignore - contactListener.OnContactRemoved = (subShapePair: Jolt.SubShapeIDPair) => { - //@ts-ignore - subShapePair = Raw.module.wrapPointer(subShapePair, Raw.module.SubShapeIDPair); - const numContacts = this.removeContactPair( - subShapePair.GetBody1ID().GetIndexAndSequenceNumber(), - subShapePair.GetBody2ID().GetIndexAndSequenceNumber() - ); - let isFinalRemoval = false; - if (!numContacts) { - const bodyState1 = this.getBody( - subShapePair.GetBody1ID().GetIndexAndSequenceNumber() - ); - if (bodyState1) - //@ts-ignore - bodyState1.contactTimestamps.set( - subShapePair.GetBody2ID().GetIndexAndSequenceNumber(), - Date.now() - ); - isFinalRemoval = true; - } - this.triggerContactListeners( - subShapePair.GetBody1ID().GetIndexAndSequenceNumber(), - subShapePair.GetBody2ID().GetIndexAndSequenceNumber(), - 'removed', - isFinalRemoval ? 'final' : 'removed', - numContacts - ); - }; - - // for now we aren't messing with validated but its required - contactListener.OnContactValidate = () => { - return Raw.module.ValidateResult_AcceptAllContactsForThisBodyPair; - }; - - this.joltPhysicsSystem.SetContactListener(contactListener); - } - private triggerContactListeners( - body1: number, - body2: number, - type: string, - context: string, - numContacts?: number, - manifold?: Jolt.ContactManifold, - settings?: Jolt.ContactSettings - ) { - // go through the body system and trigger the contact listeners - const body = this.getBody(body1); - if (!body) return; - const target = - type === 'added' - ? body.contactAddedListeners - : type === 'persisted' - ? body.contactPersistedListeners - : body.contactRemovedListeners; - if (target.length) - target.forEach((listener) => - listener(body1, body2, numContacts, context, manifold, settings) - ); - } - - // Contact Pairing =================================== - - // add contact to body - private addContactToBody(body: BodyState, contact: number) { - // see if the contact exists, if so increment it - //@ts-ignore - const current = body.contacts.get(contact); - if (current) { - //@ts-ignore - body.contacts.set(contact, current + 1); - return current + 1; - } - // otherwise set it to 1 - //@ts-ignore - body.contacts.set(contact, 1); - return 1; - } - // remove contact from body - private removeContactFromBody(body: BodyState, contact: number) { - //@ts-ignore - const current = body.contacts.get(contact); - if (current) { - if (current === 1) { - //@ts-ignore - body.contacts.delete(contact); - return 0; - } //@ts-ignore - body.contacts.set(contact, current - 1); - return current - 1; - } - return 0; - } - - private addContactPair(body1Handle: number, body2Handle: number) { - const body1 = this.getBody(body1Handle); - const body2 = this.getBody(body2Handle); - // early bail; - if (!body1 || !body2) return 0; - // add the contact to both bodies - this.addContactToBody(body1, body2Handle); - this.addContactToBody(body2, body1Handle); - // return the new count - return body1.isContacting(body2Handle); - } - private removeContactPair(body1Handle: number, body2Handle: number) { - const body1 = this.getBody(body1Handle); - const body2 = this.getBody(body2Handle); - // early bail; - if (!body1 || !body2) return 0; - // remove the contact from both bodies - this.removeContactFromBody(body1, body2Handle); - this.removeContactFromBody(body2, body1Handle); - // return the new count - return body1.isContacting(body2Handle); - } + this.triggerContactListeners( + body1.GetID().GetIndexAndSequenceNumber(), + body2.GetID().GetIndexAndSequenceNumber(), + "added", + isContacting ? "additional" : "new", // 'new' or 'additional' + numContacts, + manifold, + settings + ); + }; + //@ts-ignore + contactListener.OnContactPersisted = ( + body1: Jolt.Body, + body2: Jolt.Body, + manifold: Jolt.ContactManifold, + settings: Jolt.ContactSettings + ) => { + //@ts-ignore + body1 = Raw.module.wrapPointer(body1, Raw.module.Body); + //@ts-ignore + body2 = Raw.module.wrapPointer(body2, Raw.module.Body); + //@ts-ignore + manifold = Raw.module.wrapPointer(manifold, Raw.module.ContactManifold); + //@ts-ignore + settings = Raw.module.wrapPointer(settings, Raw.module.ContactSettings); + //@ts-ignore + const numContacts = this.getBody( + body1.GetID().GetIndexAndSequenceNumber() + ).isContacting(body2.GetID().GetIndexAndSequenceNumber()); + this.triggerContactListeners( + body1.GetID().GetIndexAndSequenceNumber(), + body2.GetID().GetIndexAndSequenceNumber(), + "persisted", + "persisted", + numContacts, + manifold, + settings + ); + }; + // removed uses a weird subshapepair argument + //@ts-ignore + contactListener.OnContactRemoved = (subShapePair: Jolt.SubShapeIDPair) => { + //@ts-ignore + subShapePair = Raw.module.wrapPointer(subShapePair, Raw.module.SubShapeIDPair); + const numContacts = this.removeContactPair( + subShapePair.GetBody1ID().GetIndexAndSequenceNumber(), + subShapePair.GetBody2ID().GetIndexAndSequenceNumber() + ); + let isFinalRemoval = false; + if (!numContacts) { + const bodyState1 = this.getBody( + subShapePair.GetBody1ID().GetIndexAndSequenceNumber() + ); + if (bodyState1) + //@ts-ignore + bodyState1.contactTimestamps.set( + subShapePair.GetBody2ID().GetIndexAndSequenceNumber(), + Date.now() + ); + isFinalRemoval = true; + } + this.triggerContactListeners( + subShapePair.GetBody1ID().GetIndexAndSequenceNumber(), + subShapePair.GetBody2ID().GetIndexAndSequenceNumber(), + "removed", + isFinalRemoval ? "final" : "removed", + numContacts + ); + }; + + // for now we aren't messing with validated but its required + contactListener.OnContactValidate = () => { + return Raw.module.ValidateResult_AcceptAllContactsForThisBodyPair; + }; + + this.joltPhysicsSystem.SetContactListener(contactListener); + } + private triggerContactListeners( + body1: number, + body2: number, + type: string, + context: string, + numContacts?: number, + manifold?: Jolt.ContactManifold, + settings?: Jolt.ContactSettings + ) { + // go through the body system and trigger the contact listeners + const body = this.getBody(body1); + if (!body) return; + const target = + type === "added" + ? body.contactAddedListeners + : type === "persisted" + ? body.contactPersistedListeners + : body.contactRemovedListeners; + if (target.length) + target.forEach((listener) => + listener(body1, body2, numContacts, context, manifold, settings) + ); + } + + // Contact Pairing =================================== + + // add contact to body + private addContactToBody(body: BodyState, contact: number) { + // see if the contact exists, if so increment it + //@ts-ignore + const current = body.contacts.get(contact); + if (current) { + //@ts-ignore + body.contacts.set(contact, current + 1); + return current + 1; + } + // otherwise set it to 1 + //@ts-ignore + body.contacts.set(contact, 1); + return 1; + } + // remove contact from body + private removeContactFromBody(body: BodyState, contact: number) { + //@ts-ignore + const current = body.contacts.get(contact); + if (current) { + if (current === 1) { + //@ts-ignore + body.contacts.delete(contact); + return 0; + } //@ts-ignore + body.contacts.set(contact, current - 1); + return current - 1; + } + return 0; + } + + private addContactPair(body1Handle: number, body2Handle: number) { + const body1 = this.getBody(body1Handle); + const body2 = this.getBody(body2Handle); + // early bail; + if (!body1 || !body2) return 0; + // add the contact to both bodies + this.addContactToBody(body1, body2Handle); + this.addContactToBody(body2, body1Handle); + // return the new count + return body1.isContacting(body2Handle); + } + private removeContactPair(body1Handle: number, body2Handle: number) { + const body1 = this.getBody(body1Handle); + const body2 = this.getBody(body2Handle); + // early bail; + if (!body1 || !body2) return 0; + // remove the contact from both bodies + this.removeContactFromBody(body1, body2Handle); + this.removeContactFromBody(body2, body1Handle); + // return the new count + return body1.isContacting(body2Handle); + } } // Jolt Utilities ================================= // merge jolt Settings with optional object export function mergeBodyCreationSettings( - settings: Jolt.BodyCreationSettings, - options?: Jolt.BodyCreationSettings + settings: Jolt.BodyCreationSettings, + options?: Jolt.BodyCreationSettings ) { - if (!options) return settings; - // loop over the object keys and set the settings - for (const key in options) { - // @ts-ignore - settings[key] = options[key]; - } - return settings; + if (!options) return settings; + // loop over the object keys and set the settings + for (const key in options) { + // @ts-ignore + settings[key] = options[key]; + } + return settings; } export function generateBodySettings( - object: Object3D | Jolt.ShapeSettings, - options: GenerateBodyOptions = {} + object: Object3D | Jolt.ShapeSettings, + options: GenerateBodyOptions = {} ): Jolt.BodyCreationSettings { - const jolt = Raw.module; - const isObject = object instanceof Object3D; - // can I move this into the args? - const { bodyType = 'dynamic', shapeType } = options; - // Generate or pass along the shape settings - const shapeSettings = isObject ? getShapeSettingsFromObject(object, shapeType) : object; - if (!shapeSettings) throw new Error('No shape settings found'); - // Due to a Jolt limitation we cant just pass the settings and have to generate the shape here - const shape = shapeSettings.Create().Get(); - // TODO: do we need to destroy the shapeSettings? - // jolt.destroy(shapeSettings); - - // create position and quaternion from three to jolt - let position: any = new THREE.Vector3(); - let quaternion: any = new THREE.Quaternion(); - if (isObject) { - position.copy(object.position); - quaternion.copy(object.quaternion); - } - // Jitter fixes a problem where rapidly created bodies jam each other - // also allows nice effects like fountains when creating bodies - if (options.jitter) { - // jitter is a vector3 with a max distance - // generate a new vector3 with a random value between 0 and the jitter value for each axis - const jitter = new THREE.Vector3( - Math.random() * options.jitter.x, - Math.random() * options.jitter.y, - Math.random() * options.jitter.z - ); - position.add(jitter); - // jitter the rotation too - quaternion.setFromEuler( - new THREE.Euler( - Math.random() * options.jitter.x, - Math.random() * options.jitter.y, - Math.random() * options.jitter.z - ) - ); - } - // reset the items to jolt types - position = vec3.threeToJolt(position); - quaternion = quat.threeToJolt(quaternion); - - // type bases on bodyType (Dynamic by default) - let layer, motionType; - switch (bodyType) { - case 'static': - motionType = jolt.EMotionType_Static; - layer = Layer.NON_MOVING; - break; - case 'kinematic': - motionType = jolt.EMotionType_Kinematic; - layer = Layer.MOVING; - break; - case 'rig': - motionType = jolt.EMotionType_Dynamic; - layer = Layer.RIG; - //rigs need to have no gravity - // TODO fix these type warnings - /* + const jolt = Raw.module; + const isObject = object instanceof Object3D; + // can I move this into the args? + const { bodyType = "dynamic", shapeType } = options; + // Generate or pass along the shape settings + const shapeSettings = isObject ? getShapeSettingsFromObject(object, shapeType) : object; + if (!shapeSettings) throw new Error("No shape settings found"); + // Due to a Jolt limitation we cant just pass the settings and have to generate the shape here + const shape = shapeSettings.Create().Get(); + // TODO: do we need to destroy the shapeSettings? + // jolt.destroy(shapeSettings); + + // create position and quaternion from three to jolt + let position: any = new THREE.Vector3(); + let quaternion: any = new THREE.Quaternion(); + if (isObject) { + position.copy(object.position); + quaternion.copy(object.quaternion); + } + // Jitter fixes a problem where rapidly created bodies jam each other + // also allows nice effects like fountains when creating bodies + if (options.jitter) { + // jitter is a vector3 with a max distance + // generate a new vector3 with a random value between 0 and the jitter value for each axis + const jitter = new THREE.Vector3( + Math.random() * options.jitter.x, + Math.random() * options.jitter.y, + Math.random() * options.jitter.z + ); + position.add(jitter); + // jitter the rotation too + quaternion.setFromEuler( + new THREE.Euler( + Math.random() * options.jitter.x, + Math.random() * options.jitter.y, + Math.random() * options.jitter.z + ) + ); + } + // reset the items to jolt types + position = vec3.threeToJolt(position); + quaternion = quat.threeToJolt(quaternion); + + // type bases on bodyType (Dynamic by default) + let layer, motionType; + switch (bodyType) { + case "static": + motionType = jolt.EMotionType_Static; + layer = Layer.NON_MOVING; + break; + case "kinematic": + motionType = jolt.EMotionType_Kinematic; + layer = Layer.MOVING; + break; + case "rig": + motionType = jolt.EMotionType_Dynamic; + layer = Layer.RIG; + //rigs need to have no gravity + // TODO fix these type warnings + /* if (!options?.mGravityFactor) { if (!options?.bodySettings) options.bodySettings = {}; options!.bodySettings.mGravityFactor = 0; }*/ - break; - - default: - motionType = jolt.EMotionType_Dynamic; - layer = Layer.MOVING; - } - // if the user specefied a motionType we need to override the last switch - if (options?.motionType) { - switch (options.motionType) { - case 'static': - motionType = jolt.EMotionType_Static; - break; - case 'kinematic': - motionType = jolt.EMotionType_Kinematic; - break; - default: - motionType = jolt.EMotionType_Dynamic; - layer = Layer.MOVING; - } - } - // create the settings - const settings = mergeBodyCreationSettings( - new jolt.BodyCreationSettings(shape, position, quaternion, motionType, layer), - options.bodySettings - ); - // Trimesh override to add mass and inertia - // see: https://jrouwe.github.io/JoltPhysics/#dynamic-mesh-shapes - if (shapeType === 'trimesh' && motionType === jolt.EMotionType_Dynamic) { - settings.mOverrideMassProperties = - Raw.module.EOverrideMassProperties_MassAndInertiaProvided; - // if the object is an object we need to get the size from it - let size; - let mass = options?.mass || 200; - if (isObject) { - const box = new THREE.Box3().setFromObject(object); - size = box.getSize(new Vector3()); - } else { - size = options?.size || new THREE.Vector3(1, 1, 1); - } - //size = new jolt.Vec3(1, 1, 1); - //console.log('trimesh size', size, mass); - settings.mMassPropertiesOverride.SetMassAndInertiaOfSolidBox(vec3.jolt(size), mass); - } - // destroy the position and quaternion - jolt.destroy(position); - jolt.destroy(quaternion); - // return the settings - return settings; + break; + + default: + motionType = jolt.EMotionType_Dynamic; + layer = Layer.MOVING; + } + // if the user specefied a motionType we need to override the last switch + if (options?.motionType) { + switch (options.motionType) { + case "static": + motionType = jolt.EMotionType_Static; + break; + case "kinematic": + motionType = jolt.EMotionType_Kinematic; + break; + default: + motionType = jolt.EMotionType_Dynamic; + layer = Layer.MOVING; + } + } + // create the settings + const settings = mergeBodyCreationSettings( + new jolt.BodyCreationSettings(shape, position, quaternion, motionType, layer), + options.bodySettings + ); + // Trimesh override to add mass and inertia + // see: https://jrouwe.github.io/JoltPhysics/#dynamic-mesh-shapes + if (shapeType === "trimesh" && motionType === jolt.EMotionType_Dynamic) { + settings.mOverrideMassProperties = + Raw.module.EOverrideMassProperties_MassAndInertiaProvided; + // if the object is an object we need to get the size from it + let size; + let mass = options?.mass || 200; + if (isObject) { + const box = new THREE.Box3().setFromObject(object); + size = box.getSize(new Vector3()); + } else { + size = options?.size || new THREE.Vector3(1, 1, 1); + } + //size = new jolt.Vec3(1, 1, 1); + //console.log('trimesh size', size, mass); + settings.mMassPropertiesOverride.SetMassAndInertiaOfSolidBox(vec3.jolt(size), mass); + } + // destroy the position and quaternion + jolt.destroy(position); + jolt.destroy(quaternion); + // return the settings + return settings; } // TODO: my base generators require three objects. perhaps abastract out or make better names // Change a bodies mass settings after already being created // src:PhoenixIllusion @ https://github.com/jrouwe/JoltPhysics.js/discussions/112 +function changeMassInertia(body: Jolt.Body, mass: number) { + const motionProps = body.GetMotionProperties(); + const massProps = body.GetShape().GetMassProperties(); + massProps.ScaleToMass(mass); //<--- newly exposed function + motionProps.SetMassProperties(Raw.module.EAllowedDOFs_All, massProps); +} +/* og export function changeMassInertia(body: Jolt.Body, mass: number) { const motionProps = body.GetMotionProperties(); const massProps = body.GetShape().GetMassProperties(); @@ -594,79 +600,80 @@ export function changeMassInertia(body: Jolt.Body, mass: number) { massProps.mInertia = inertia; motionProps.SetMassProperties(Raw.module.EAllowedDOFs_All, massProps); } +*/ // When debugging we need to create a three debug object // initially pulled from Jolt Demo, -export function getThreeObjectForBody(body: Jolt.Body, color = '#E07A5F') { - let shape = body.GetShape(); - // lets see if we can get the material color by the shape - // TODO this isn't in Jolt.js yet. - - //const physicsMaterial: Jolt.PhysicsMaterial = shape.GetMaterial(); - //const pmColor = physicsMaterial.GetDebugColor(); - const material = new THREE.MeshPhongMaterial({ - color: color, - wireframe: true - }); - - let threeObject; - - let extent; - switch (shape.GetSubType()) { - case Raw.module.EShapeSubType_Box: - shape = Raw.module.castObject(shape, Raw.module.BoxShape); - //@ts-ignore - extent = vec3.three(shape.GetHalfExtent()).multiplyScalar(2); - threeObject = new THREE.Mesh( - new THREE.BoxGeometry(extent.x, extent.y, extent.z, 1, 1, 1), - material - ); - break; - case Raw.module.EShapeSubType_Sphere: - shape = Raw.module.castObject(shape, Raw.module.SphereShape); - threeObject = new THREE.Mesh( - //@ts-ignore - new THREE.SphereGeometry(shape.GetRadius(), 32, 32), - material - ); - break; - case Raw.module.EShapeSubType_Capsule: - shape = Raw.module.castObject(shape, Raw.module.CapsuleShape); - threeObject = new THREE.Mesh( - new THREE.CapsuleGeometry( - //@ts-ignore - shape.GetRadius(), - //@ts-ignore - 2 * shape.GetHalfHeightOfCylinder(), - 20, - 10 - ), - material - ); - break; - case Raw.module.EShapeSubType_Cylinder: - shape = Raw.module.castObject(shape, Raw.module.CylinderShape); - threeObject = new THREE.Mesh( - new THREE.CylinderGeometry( - //@ts-ignore - shape.GetRadius(), - //@ts-ignore - shape.GetRadius(), - //@ts-ignore - 2 * shape.GetHalfHeight(), - 20, - 1 - ), - material - ); - break; - default: - threeObject = new THREE.Mesh(createMeshForShape(shape), material); - break; - } - - threeObject.position.copy(vec3.three(body.GetPosition())); - threeObject.quaternion.copy(quat.joltToThree(body.GetRotation())); - - return threeObject; +export function getThreeObjectForBody(body: Jolt.Body, color = "#E07A5F") { + let shape = body.GetShape(); + // lets see if we can get the material color by the shape + // TODO this isn't in Jolt.js yet. + + //const physicsMaterial: Jolt.PhysicsMaterial = shape.GetMaterial(); + //const pmColor = physicsMaterial.GetDebugColor(); + const material = new THREE.MeshPhongMaterial({ + color: color, + wireframe: true + }); + + let threeObject; + + let extent; + switch (shape.GetSubType()) { + case Raw.module.EShapeSubType_Box: + shape = Raw.module.castObject(shape, Raw.module.BoxShape); + //@ts-ignore + extent = vec3.three(shape.GetHalfExtent()).multiplyScalar(2); + threeObject = new THREE.Mesh( + new THREE.BoxGeometry(extent.x, extent.y, extent.z, 1, 1, 1), + material + ); + break; + case Raw.module.EShapeSubType_Sphere: + shape = Raw.module.castObject(shape, Raw.module.SphereShape); + threeObject = new THREE.Mesh( + //@ts-ignore + new THREE.SphereGeometry(shape.GetRadius(), 32, 32), + material + ); + break; + case Raw.module.EShapeSubType_Capsule: + shape = Raw.module.castObject(shape, Raw.module.CapsuleShape); + threeObject = new THREE.Mesh( + new THREE.CapsuleGeometry( + //@ts-ignore + shape.GetRadius(), + //@ts-ignore + 2 * shape.GetHalfHeightOfCylinder(), + 20, + 10 + ), + material + ); + break; + case Raw.module.EShapeSubType_Cylinder: + shape = Raw.module.castObject(shape, Raw.module.CylinderShape); + threeObject = new THREE.Mesh( + new THREE.CylinderGeometry( + //@ts-ignore + shape.GetRadius(), + //@ts-ignore + shape.GetRadius(), + //@ts-ignore + 2 * shape.GetHalfHeight(), + 20, + 1 + ), + material + ); + break; + default: + threeObject = new THREE.Mesh(createMeshForShape(shape), material); + break; + } + + threeObject.position.copy(vec3.three(body.GetPosition())); + threeObject.quaternion.copy(quat.joltToThree(body.GetRotation())); + + return threeObject; } diff --git a/packages/react-three-jolt/src/systems/physics-system.ts b/packages/react-three-jolt/src/systems/physics-system.ts index fc03ade..8843397 100644 --- a/packages/react-three-jolt/src/systems/physics-system.ts +++ b/packages/react-three-jolt/src/systems/physics-system.ts @@ -151,7 +151,11 @@ export class PhysicsSystem { : this.steppingState.accumulator / this.timeStep; // Loop over all dynamic bodies // NOTE: using "state" to match rapier logic - this.bodySystem.dynamicBodies.forEach((state: BodyState) => { + const mergedBodies: BodyState[] = [ + ...this.bodySystem.dynamicBodies.values(), + ...this.bodySystem.kinematicBodies.values() + ]; + mergedBodies.forEach((state: BodyState) => { const body = state.body; if (state.isSleeping) return; diff --git a/yarn.lock b/yarn.lock index a9e5e89..305ce68 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4073,6 +4073,8 @@ __metadata: eslint-plugin-react-hooks: "npm:^4.6.0" eslint-plugin-react-refresh: "npm:^0.4.6" leva: "npm:^0.9.35" + maath: "npm:^0.10.7" + postprocessing: "npm:^6.35.3" prettier: "npm:^3.2.5" r3f-perf: "npm:^7.2.1" react: "npm:^18.2.0" @@ -6579,6 +6581,15 @@ __metadata: languageName: node linkType: hard +"postprocessing@npm:^6.35.3": + version: 6.35.3 + resolution: "postprocessing@npm:6.35.3" + peerDependencies: + three: ">= 0.152.0 < 0.164.0" + checksum: 10c0/9882c0899fd22e080398c8d2ecec297959971698d5e4e43ded0b706fafac8037140a19da65d4cd3662501ef770b67b3d2d950a4f62d8c4e71c97e57de228336e + languageName: node + linkType: hard + "potpack@npm:^1.0.1": version: 1.0.2 resolution: "potpack@npm:1.0.2"