UNPKG

@threlte/rapier

Version:

Components and hooks to use the Rapier physics engine in Threlte

304 lines (303 loc) 13.9 kB
import { Collider, EventQueue } from '@dimforge/rapier3d-compat'; import { useTask } from '@threlte/core'; import { Object3D, Quaternion, Vector3 } from 'three'; import { simulationKey, synchronizationKey } from './keys'; const tempObject = new Object3D(); const tempVector3 = new Vector3(); const tempQuaternion = new Quaternion(); const getEventDispatchers = (collider1, collider2, colliderEventDispatchers, rigidBodyEventDispatchers) => { const collider1Events = colliderEventDispatchers.get(collider1.handle); const collider2Events = colliderEventDispatchers.get(collider2.handle); const rigidBody1 = collider1.parent(); const rigidBody2 = collider2.parent(); const rigidBody1Events = rigidBody1 ? rigidBodyEventDispatchers.get(rigidBody1.handle) : undefined; const rigidBody2Events = rigidBody2 ? rigidBodyEventDispatchers.get(rigidBody2.handle) : undefined; return { collider1Events, collider2Events, rigidBody1Events, rigidBody2Events }; }; const objectHasPhysicsUserData = (obj) => { return obj.userData.physics !== undefined; }; export const initializeRigidBodyUserData = (obj) => { const userData = { currentPosition: new Vector3(), currentQuaternion: new Quaternion(), lastPosition: new Vector3(), lastQuaternion: new Quaternion(), resetPosition: false, resetRotation: false }; obj.userData.physics = userData; return userData; }; export const setInitialRigidBodyState = (obj, initialPosition, initialQuaternion) => { if (!objectHasPhysicsUserData(obj)) { initializeRigidBodyUserData(obj); } const userData = obj.userData.physics; userData.currentPosition.copy(initialPosition); userData.lastPosition.copy(initialPosition); userData.currentQuaternion.copy(initialQuaternion); userData.lastQuaternion.copy(initialQuaternion); }; export const createPhysicsTasks = (world, framerate, simulationOffset, rigidBodyObjects, updateRigidBodySimulationData, colliderEventDispatchers, rigidBodyEventDispatchers, simulationStage, synchronizationStage) => { const eventQueue = new EventQueue(false); const simulation = useTask(simulationKey, (delta) => { // Set timestep to current delta, to allow for variable frame rates // We cap the delta at 100, so that the physics simulation doesn't get wild if (framerate.current === 'varying') { world.timestep = Math.min(delta, 0.1); } else { world.timestep = delta; } world.step(eventQueue); rigidBodyObjects.forEach((mesh, handle) => { const rigidBody = world.getRigidBody(handle); if (!rigidBody || !rigidBody.isValid()) return; const events = rigidBodyEventDispatchers.get(handle); if (events) { if (rigidBody.isSleeping() && !mesh.userData.isSleeping) { events.onsleep?.(); } if (!rigidBody.isSleeping() && mesh.userData.isSleeping) { events.onwake?.(); } mesh.userData.isSleeping = rigidBody.isSleeping(); } if (rigidBody.isSleeping() || rigidBody.isFixed() || !mesh.parent) { return; } if (updateRigidBodySimulationData.current) { const translation = rigidBody.translation(); const rotation = rigidBody.rotation(); if (objectHasPhysicsUserData(mesh)) { const userData = mesh.userData.physics; if (userData.resetPosition) { userData.resetPosition = false; userData.lastPosition.set(translation.x, translation.y, translation.z); userData.currentPosition.set(translation.x, translation.y, translation.z); } else { userData.lastPosition.copy(userData.currentPosition); userData.currentPosition.set(translation.x, translation.y, translation.z); } if (userData.resetRotation) { userData.resetRotation = false; userData.lastQuaternion.set(rotation.x, rotation.y, rotation.z, rotation.w); userData.currentQuaternion.set(rotation.x, rotation.y, rotation.z, rotation.w); } else { userData.lastQuaternion.copy(userData.currentQuaternion); userData.currentQuaternion.set(rotation.x, rotation.y, rotation.z, rotation.w); } } else { initializeRigidBodyUserData(mesh); setInitialRigidBodyState(mesh, tempVector3.set(translation.x, translation.y, translation.z), tempQuaternion.set(rotation.x, rotation.y, rotation.z, rotation.w)); } } }); eventQueue.drainContactForceEvents((e) => { const collider1 = world.getCollider(e.collider1()); const collider2 = world.getCollider(e.collider2()); // Sanity check if (!collider1 || !collider2) { return; } const { collider1Events, collider2Events, rigidBody1Events, rigidBody2Events } = getEventDispatchers(collider1, collider2, colliderEventDispatchers, rigidBodyEventDispatchers); const rigidBody1 = collider1.parent(); const rigidBody2 = collider2.parent(); // Collider events collider1Events?.oncontact?.({ targetCollider: collider2, targetRigidBody: rigidBody2, maxForceDirection: e.maxForceDirection(), maxForceMagnitude: e.maxForceMagnitude(), totalForce: e.totalForce(), totalForceMagnitude: e.totalForceMagnitude() }); collider2Events?.oncontact?.({ targetCollider: collider1, targetRigidBody: rigidBody1, maxForceDirection: e.maxForceDirection(), maxForceMagnitude: e.maxForceMagnitude(), totalForce: e.totalForce(), totalForceMagnitude: e.totalForceMagnitude() }); // RigidBody Events rigidBody1Events?.oncontact?.({ targetCollider: collider2, targetRigidBody: rigidBody2, maxForceDirection: e.maxForceDirection(), maxForceMagnitude: e.maxForceMagnitude(), totalForce: e.totalForce(), totalForceMagnitude: e.totalForceMagnitude() }); rigidBody2Events?.oncontact?.({ targetCollider: collider1, targetRigidBody: rigidBody1, maxForceDirection: e.maxForceDirection(), maxForceMagnitude: e.maxForceMagnitude(), totalForce: e.totalForce(), totalForceMagnitude: e.totalForceMagnitude() }); }); // Collision events eventQueue.drainCollisionEvents((handle1, handle2, started) => { const collider1 = world.getCollider(handle1); const collider2 = world.getCollider(handle2); // Sanity check if (!collider1 || !collider2) { return; } const { collider1Events, collider2Events, rigidBody1Events, rigidBody2Events } = getEventDispatchers(collider1, collider2, colliderEventDispatchers, rigidBodyEventDispatchers); if (!collider1Events && !collider2Events && !rigidBody1Events && !rigidBody2Events) { return; } const rigidBody1 = collider1.parent(); const rigidBody2 = collider2.parent(); if (started) { // intersections are triggered by sensors const isIntersection = world.intersectionPair(collider1, collider2); if (isIntersection) { // Collider Events collider1Events?.onsensorenter?.({ targetCollider: collider2, targetRigidBody: rigidBody2 }); collider2Events?.onsensorenter?.({ targetCollider: collider1, targetRigidBody: rigidBody1 }); // RigidBody Events rigidBody1Events?.onsensorenter?.({ targetCollider: collider2, targetRigidBody: rigidBody2 }); rigidBody2Events?.onsensorenter?.({ targetCollider: collider1, targetRigidBody: rigidBody1 }); // intersections with sensors don't trigger contact pairs, returning return; } world.contactPair(collider1, collider2, (manifold, flipped) => { // Collider events collider1Events?.oncollisionenter?.({ flipped, manifold, targetCollider: collider2, targetRigidBody: rigidBody2 }); collider2Events?.oncollisionenter?.({ flipped, manifold, targetCollider: collider1, targetRigidBody: rigidBody1 }); // RigidBody Events rigidBody1Events?.oncollisionenter?.({ flipped, manifold, targetCollider: collider2, targetRigidBody: rigidBody2 }); rigidBody2Events?.oncollisionenter?.({ flipped, manifold, targetCollider: collider1, targetRigidBody: rigidBody1 }); }); } else { // COLLISION / INTERSECTION ENDED // intersections are triggered by sensors, but apparently not on collision exit const isIntersection = world.intersectionPair(collider1, collider2) || collider1.isSensor() || collider2.isSensor(); if (isIntersection) { collider1Events?.onsensorexit?.({ targetCollider: collider2, targetRigidBody: rigidBody2 }); collider2Events?.onsensorexit?.({ targetCollider: collider1, targetRigidBody: rigidBody1 }); // RigidBody Events rigidBody1Events?.onsensorexit?.({ targetCollider: collider2, targetRigidBody: rigidBody2 }); rigidBody2Events?.onsensorexit?.({ targetCollider: collider1, targetRigidBody: rigidBody1 }); // intersections with sensors don't trigger contact pairs, returning return; } // Collider events collider1Events?.oncollisionexit?.({ targetCollider: collider2, targetRigidBody: rigidBody2 }); collider2Events?.oncollisionexit?.({ targetCollider: collider1, targetRigidBody: rigidBody1 }); // RigidBody Events rigidBody1Events?.oncollisionexit?.({ targetCollider: collider2, targetRigidBody: rigidBody2 }); rigidBody2Events?.oncollisionexit?.({ targetCollider: collider1, targetRigidBody: rigidBody1 }); } }); }, { stage: simulationStage }); const synchronization = useTask(synchronizationKey, () => { rigidBodyObjects.forEach((mesh) => { if (!objectHasPhysicsUserData(mesh)) return; const userData = mesh.userData.physics; if (framerate.current === 'varying') { tempObject.position.copy(userData.currentPosition); tempObject.quaternion.copy(userData.currentQuaternion); } else { tempObject.position .copy(userData.lastPosition) .lerp(userData.currentPosition, simulationOffset.current); tempObject.quaternion .copy(userData.lastQuaternion) .slerp(userData.currentQuaternion, simulationOffset.current); } // Rapier has no concept of scale, so we use the mesh's scale mesh.getWorldScale(tempVector3); tempObject.scale.copy(tempVector3); tempObject.updateMatrix(); if (mesh.parent) tempObject.applyMatrix4(mesh.parent.matrixWorld.clone().invert()); tempObject.updateMatrix(); mesh.position.setFromMatrixPosition(tempObject.matrix); mesh.rotation.setFromRotationMatrix(tempObject.matrix); }); }, { stage: synchronizationStage }); return { simulationTask: simulation.task, synchronizationTask: synchronization.task }; };