UNPKG

@needle-tools/engine

Version:

Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development with great integrations into editors like Unity or Blender - and can be deployed onto any device! It is flexible, extensible and networking and XR are built-in.

1,093 lines • 66 kB
import { BufferAttribute, BufferGeometry, InterleavedBufferAttribute, LineBasicMaterial, LineSegments, Matrix4, Quaternion, Vector3 } from 'three'; import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js'; import { CollisionDetectionMode, PhysicsMaterialCombine } from '../engine/engine_physics.types.js'; import { MeshCollider } from '../engine-components/Collider.js'; import { isDevEnvironment } from './debug/debug.js'; import { ContextEvent, ContextRegistry } from './engine_context_registry.js'; import { foreachComponent } from './engine_gameobject.js'; import { Gizmos } from './engine_gizmos.js'; import { Mathf } from './engine_math.js'; import { MODULES } from './engine_modules.js'; import { getWorldPosition, getWorldQuaternion, getWorldScale, setWorldPositionXYZ, setWorldQuaternionXYZW } from "./engine_three_utils.js"; import { Collision, ContactPoint } from './engine_types.js'; import { SphereOverlapResult } from './engine_types.js'; import { CircularBuffer, getParam } from "./engine_utils.js"; const debugPhysics = getParam("debugphysics"); const debugColliderPlacement = getParam("debugcolliderplacement"); const debugCollisions = getParam("debugcollisions"); const showColliders = getParam("showcolliders"); const showPhysicsRaycasts = getParam("debugraycasts"); /** on physics body and references the needle component */ const $componentKey = Symbol("needle component"); /** on needle component and references physics body */ const $bodyKey = Symbol("physics body"); const $colliderRigidbody = Symbol("rigidbody"); globalThis["NEEDLE_USE_RAPIER"] = globalThis["NEEDLE_USE_RAPIER"] !== undefined ? globalThis["NEEDLE_USE_RAPIER"] : true; if (debugPhysics) console.log("Use Rapier", NEEDLE_USE_RAPIER, globalThis["NEEDLE_USE_RAPIER"]); if (NEEDLE_USE_RAPIER) { ContextRegistry.registerCallback(ContextEvent.ContextCreationStart, evt => { if (debugPhysics) console.log("Register rapier physics backend"); evt.context.physics.engine = new RapierPhysics(evt.context); // We do not initialize physics immediately to avoid loading the physics engine if it is not needed }); } export class RapierPhysics { debugRenderColliders = false; debugRenderRaycasts = false; removeBody(obj) { if (!obj) return; this.validate(); const body = obj[$bodyKey]; obj[$bodyKey] = null; if (body && this.world) { const index = this.objects.findIndex(o => o === obj); if (index >= 0) { const rapierBody = this.bodies[index]; // Remove references this.bodies.splice(index, 1); this.objects.splice(index, 1); // Remove the collider from the physics world if (rapierBody instanceof MODULES.RAPIER_PHYSICS.MODULE.Collider) { const rapierCollider = rapierBody; this.world?.removeCollider(rapierCollider, true); // also remove the rigidbody if it doesnt have colliders anymore const rapierRigidbody = rapierCollider.parent(); if (rapierRigidbody && rapierRigidbody.numColliders() <= 0) { const rigidbody = rapierRigidbody[$componentKey]; if (rigidbody) { // If the collider was attached to a rigidbody and this rigidbody now has no colliders anymore we should ignore it - because the Rigidbody component will delete itself } else { // But if there is no explicit rigidbody needle component then the colliders did create it implictly and thus we need to remove it here: this.world?.removeRigidBody(rapierRigidbody); } } } // Remove the rigidbody from the physics world else if (rapierBody instanceof MODULES.RAPIER_PHYSICS.MODULE.RigidBody) { if (rapierBody.numColliders() <= 0) { this.world?.removeRigidBody(rapierBody); } else { if (isDevEnvironment()) { if (!rapierBody["did_log_removing"]) { setTimeout(() => { if (rapierBody.numColliders() > 0) { rapierBody["did_log_removing"] = true; console.warn("RapierPhysics: removing rigidbody with colliders from the physics world is not possible right now, please remove the colliders first"); } }, 1); } } } } } } } updateBody(comp, translation, rotation) { this.validate(); if (!this.enabled) return; if (comp.destroyed || !comp.gameObject) return; if (!translation && !rotation) return; if (comp.isCollider === true) { // const collider = comp as ICollider; console.warn("TODO: implement updating collider position"); } else { const rigidbody = comp; const body = rigidbody[$bodyKey]; if (body) { this.syncPhysicsBody(rigidbody.gameObject, body, translation, rotation); } } } updateProperties(obj) { this.validate(); if (obj.isCollider) { const col = obj; const body = col[$bodyKey]; if (body) { this.internalUpdateColliderProperties(col, body); if (col.sharedMaterial) this.updatePhysicsMaterial(col); } } else { const rb = obj; const physicsBody = this.internal_getRigidbody(rb); if (physicsBody) { this.internalUpdateRigidbodyProperties(rb, physicsBody); } } } addForce(rigidbody, force, wakeup) { this.validate(); const body = this.internal_getRigidbody(rigidbody); if (body) body.addForce(force, wakeup); else console.warn("Rigidbody doesn't exist: can not apply force (does your object with the Rigidbody have a collider?)"); } addImpulse(rigidbody, force, wakeup) { this.validate(); const body = this.internal_getRigidbody(rigidbody); if (body) body.applyImpulse(force, wakeup); else console.warn("Rigidbody doesn't exist: can not apply impulse (does your object with the Rigidbody have a collider?)"); } getLinearVelocity(comp) { this.validate(); const body = this.internal_getRigidbody(comp); if (body) { const vel = body.linvel(); return vel; } // else console.warn("Rigidbody doesn't exist: can not get linear velocity (does your object with the Rigidbody have a collider?)"); return null; } getAngularVelocity(rb) { this.validate(); const body = this.internal_getRigidbody(rb); if (body) { const vel = body.angvel(); return vel; } // else console.warn("Rigidbody doesn't exist: can not get angular velocity (does your object with the Rigidbody have a collider?)"); return null; } resetForces(rb, wakeup) { this.validate(); const body = this.internal_getRigidbody(rb); body?.resetForces(wakeup); } resetTorques(rb, wakeup) { this.validate(); const body = this.internal_getRigidbody(rb); body?.resetTorques(wakeup); } applyImpulse(rb, vec, wakeup) { this.validate(); const body = this.internal_getRigidbody(rb); if (body) body.applyImpulse(vec, wakeup); else console.warn("Rigidbody doesn't exist: can not apply impulse (does your object with the Rigidbody have a collider?)"); } wakeup(rb) { this.validate(); const body = this.internal_getRigidbody(rb); if (body) body.wakeUp(); else console.warn("Rigidbody doesn't exist: can not wake up (does your object with the Rigidbody have a collider?)"); } isSleeping(rb) { this.validate(); const body = this.internal_getRigidbody(rb); return body?.isSleeping(); } setAngularVelocity(rb, vec, wakeup) { this.validate(); const body = this.internal_getRigidbody(rb); if (body) body.setAngvel(vec, wakeup); else console.warn("Rigidbody doesn't exist: can not set angular velocity (does your object with the Rigidbody have a collider?)"); } setLinearVelocity(rb, vec, wakeup) { this.validate(); const body = this.internal_getRigidbody(rb); if (body) body.setLinvel(vec, wakeup); else console.warn("Rigidbody doesn't exist: can not set linear velocity (does your object with the Rigidbody have a collider?)"); } context; _initializePromise; _isInitialized = false; constructor(ctx) { this.context = ctx; } get isInitialized() { return this._isInitialized; } async initialize() { if (!this._initializePromise) this._initializePromise = this.internalInitialization(); return this._initializePromise; } async internalInitialization() { if (getParam("__nophysics")) { console.warn("Physics are disabled"); return false; } if (debugPhysics) console.log("Initialize rapier physics engine"); // NEEDLE_PHYSICS_INIT_START // use .env file with VITE_NEEDLE_USE_RAPIER=false to treeshake rapier // @ts-ignore if ("env" in import.meta && import.meta.env.VITE_NEEDLE_USE_RAPIER === "false") { if (debugPhysics) console.log("Rapier disabled"); return false; } // Can be transformed during build time to disable rapier if (!NEEDLE_USE_RAPIER) return false; if (this._hasCreatedWorld) { console.error("Invalid call to create physics world: world is already created"); return true; } this._hasCreatedWorld = true; if (MODULES.RAPIER_PHYSICS.MAYBEMODULE == undefined) { if (debugPhysics) console.trace("Loading rapier physics engine"); const module = await MODULES.RAPIER_PHYSICS.load(); await module.init(); } if (debugPhysics) console.log("Physics engine initialized, creating world..."); this._world = new MODULES.RAPIER_PHYSICS.MODULE.World(this._gravity); this.rapierRay = new MODULES.RAPIER_PHYSICS.MODULE.Ray({ x: 0, y: 0, z: 0 }, { x: 0, y: 0, z: 1 }); this.enabled = true; this._isInitialized = true; if (debugPhysics) console.log("Physics world created"); return true; // NEEDLE_PHYSICS_INIT_END } /** Check is the physics engine has been initialized and the call can be made */ validate() { if (!this._isInitialized) { if (debugPhysics) { this["_lastWarnTime"] = this["_lastWarnTime"] ?? 0; if (Date.now() - this["_lastWarnTime"] > 1000) { this["_lastWarnTime"] = Date.now(); console.warn("Physics engine is not initialized"); } } } } rapierRay; raycastVectorsBuffer = new CircularBuffer(() => new Vector3(), 10); raycast(origin, direction, options) { if (!this._isInitialized) { console.log("Physics engine is not initialized"); return null; } let maxDistance = options?.maxDistance; let solid = options?.solid; if (maxDistance === undefined) maxDistance = Infinity; if (solid === undefined) solid = true; const ray = this.getPhysicsRay(this.rapierRay, origin, direction); if (!ray) return null; if (this.debugRenderRaycasts || showPhysicsRaycasts) Gizmos.DrawRay(ray.origin, ray.dir, 0x0000ff, 1); const hit = this.world?.castRay(ray, maxDistance, solid, options?.queryFilterFlags, options?.filterGroups, undefined, undefined, (c) => { const component = c[$componentKey]; if (options?.filterPredicate) return options.filterPredicate(component); if (options?.useIgnoreRaycastLayer !== false) { // ignore objects in the IgnoreRaycast=2 layer return !component?.gameObject.layers.isEnabled(2); } return true; }); if (hit) { const point = ray.pointAt(hit.timeOfImpact); const vec = this.raycastVectorsBuffer.get(); vec.set(point.x, point.y, point.z); return { point: vec, collider: hit.collider[$componentKey] }; } return null; } raycastAndGetNormal(origin, direction, options) { if (!this._isInitialized) { return null; } let maxDistance = options?.maxDistance; let solid = options?.solid; if (maxDistance === undefined) maxDistance = Infinity; if (solid === undefined) solid = true; const ray = this.getPhysicsRay(this.rapierRay, origin, direction); if (!ray) return null; if (this.debugRenderRaycasts || showPhysicsRaycasts) Gizmos.DrawRay(ray.origin, ray.dir, 0x0000ff, 1); const hit = this.world?.castRayAndGetNormal(ray, maxDistance, solid, options?.queryFilterFlags, options?.filterGroups, undefined, undefined, (c) => { const component = c[$componentKey]; if (options?.filterPredicate) return options.filterPredicate(component); if (options?.useIgnoreRaycastLayer !== false) { // ignore objects in the IgnoreRaycast=2 layer return !component?.gameObject.layers.isEnabled(2); } return true; }); if (hit) { const point = ray.pointAt(hit.timeOfImpact); const normal = hit.normal; const vec = this.raycastVectorsBuffer.get(); const nor = this.raycastVectorsBuffer.get(); vec.set(point.x, point.y, point.z); nor.set(normal.x, normal.y, normal.z); return { point: vec, normal: nor, collider: hit.collider[$componentKey] }; } return null; } getPhysicsRay(ray, origin, direction) { const cam = this.context?.mainCamera; if (origin === undefined) { const pos = this.context?.input.getPointerPosition(0); if (pos) origin = pos; else return null; } // if we get origin in 2d space we need to project it to 3d space if (origin["z"] === undefined) { if (!cam) { console.error("Can not perform raycast from 2d point - no main camera found"); return null; } const vec3 = this.raycastVectorsBuffer.get(); vec3.x = origin.x; vec3.y = origin.y; vec3.z = 0; // if the origin is in screen space we need to convert it to raycaster space if (vec3.x > 1 || vec3.y > 1 || vec3.y < -1 || vec3.x < -1) { if (debugPhysics) console.warn("Converting screenspace to raycast space", vec3); this.context?.input.convertScreenspaceToRaycastSpace(vec3); } vec3.unproject(cam); origin = vec3; } const o = origin; ray.origin.x = o.x; ray.origin.y = o.y; ray.origin.z = o.z; const vec = this.raycastVectorsBuffer.get(); if (direction) vec.set(direction.x, direction.y, direction.z); else { if (!cam) { console.error("Can not perform raycast - no camera found"); return null; } vec.set(ray.origin.x, ray.origin.y, ray.origin.z); const camPosition = getWorldPosition(cam); vec.sub(camPosition); } // we need to normalize the ray because our input is a max travel length and the direction may be not normalized vec.normalize(); ray.dir.x = vec.x; ray.dir.y = vec.y; ray.dir.z = vec.z; // Gizmos.DrawRay(ray.origin, ray.dir, 0xff0000, Infinity); return ray; } rapierSphere = null; rapierColliderArray = []; rapierIdentityRotation = { x: 0, y: 0, z: 0, w: 1 }; rapierForwardVector = { x: 0, y: 0, z: 1 }; /** Precice sphere overlap detection using rapier against colliders * @param point center of the sphere in worldspace * @param radius radius of the sphere * @returns array of colliders that overlap with the sphere. Note: they currently only contain the collider and the gameobject */ sphereOverlap(point, radius) { this.rapierColliderArray.length = 0; if (!this._isInitialized) { return this.rapierColliderArray; } if (!this.world) return this.rapierColliderArray; this.rapierSphere ??= new MODULES.RAPIER_PHYSICS.MODULE.Ball(radius); this.rapierSphere.radius = radius; if (this.debugRenderRaycasts || showPhysicsRaycasts) Gizmos.DrawWireSphere(point, radius, 0x3344ff, 1); this.world.intersectionsWithShape(point, this.rapierIdentityRotation, this.rapierSphere, col => { const collider = col[$componentKey]; // if (collider.gameObject.layers.isEnabled(2)) return true; const intersection = new SphereOverlapResult(collider.gameObject, collider); this.rapierColliderArray.push(intersection); return true; // Return `false` instead if we want to stop searching for other colliders that contain this point. }, // TODO: it seems as QueryFilterFlags.EXCLUDE_SENSORS also excludes DYNAMIC Rigidbodies (only if they're set to kinematic) undefined, // QueryFilterFlags.EXCLUDE_SENSORS, undefined, undefined, undefined, col => { // we don't want to raycast against triggers (see comment about Exclude Sensors above) if (col.isSensor()) return false; const collider = col[$componentKey]; return collider.gameObject.layers.isEnabled(2) == false; }); return this.rapierColliderArray; // TODO: this only returns one hit // let filterGroups = 0xffffffff; // filterGroups &= ~(1 << 2); // const hit: ShapeColliderTOI | null = this.world.castShape(point, // this.rapierIdentityRotation, // this.rapierForwardVector, // this.rapierSphere, // 0, // QueryFilterFlags.EXCLUDE_SENSORS, // // filterGroups, // ); // // console.log(hit); // if (hit) { // const collider = hit.collider[$componentKey] as ICollider // const intersection = new SphereOverlapResult(collider.gameObject); // this.rapierColliderArray.push(intersection); // // const localpt = hit.witness2; // // // const normal = hit.normal2; // // const hitPoint = new Vector3(localpt.x, localpt.y, localpt.z); // // // collider.gameObject.localToWorld(hitPoint); // // // const normalPt = new Vector3(normal.x, normal.y, normal.z); // // // const mat = new Matrix4().setPosition(point).scale(new Vector3(radius, radius, radius)); // // // hitPoint.applyMatrix4(mat); // // console.log(hit.witness2) // // // hitPoint.add(point); // // const dist = hitPoint.distanceTo(point); // } // return this.rapierColliderArray; } // physics simulation enabled = false; /** Get access to the rapier world */ get world() { return this._world; } ; _tempPosition = new Vector3(); _tempQuaternion = new Quaternion(); _tempScale = new Vector3(); _tempMatrix = new Matrix4(); static _didLoadPhysicsEngine = false; _isUpdatingPhysicsWorld = false; get isUpdating() { return this._isUpdatingPhysicsWorld; } _world; _hasCreatedWorld = false; eventQueue; collisionHandler; objects = []; bodies = []; _meshCache = new Map(); _gravity = { x: 0.0, y: -9.81, z: 0.0 }; get gravity() { return this.world?.gravity ?? this._gravity; } set gravity(value) { if (this.world) { this.world.gravity = value; } else { this._gravity = value; } } clearCaches() { this._meshCache.clear(); if (this.eventQueue?.raw) this.eventQueue?.free(); if (this.world?.bodies) this.world?.free(); } async addBoxCollider(collider, size) { if (!this._isInitialized) await this.initialize(); if (!collider.activeAndEnabled) return; if (!this.enabled) { if (debugPhysics) console.warn("Physics are disabled"); return; } const obj = collider.gameObject; const scale = getWorldScale(obj, this._tempPosition).multiply(size); scale.multiplyScalar(0.5); // prevent negative scale if (scale.x < 0) scale.x = Math.abs(scale.x); if (scale.y < 0) scale.y = Math.abs(scale.y); if (scale.z < 0) scale.z = Math.abs(scale.z); // prevent zero scale - seems normals are flipped otherwise const minSize = 0.0000001; if (scale.x < minSize) scale.x = minSize; if (scale.y < minSize) scale.y = minSize; if (scale.z < minSize) scale.z = minSize; const desc = MODULES.RAPIER_PHYSICS.MODULE.ColliderDesc.cuboid(scale.x, scale.y, scale.z); // const objectLayerMask = collider.gameObject.layers.mask; // const mask = objectLayerMask & ~2; // TODO: https://rapier.rs/docs/user_guides/javascript/colliders/#collision-groups-and-solver-groups // desc.setCollisionGroups(objectLayerMask); this.createCollider(collider, desc); } async addSphereCollider(collider) { if (!this._isInitialized) await this.initialize(); if (!collider.activeAndEnabled) return; if (!this.enabled) { if (debugPhysics) console.warn("Physics are disabled"); return; } const desc = MODULES.RAPIER_PHYSICS.MODULE.ColliderDesc.ball(.5); this.createCollider(collider, desc); this.updateProperties(collider); } async addCapsuleCollider(collider, height, radius) { if (!this._isInitialized) await this.initialize(); if (!collider.activeAndEnabled) return; if (!this.enabled) { if (debugPhysics) console.warn("Physics are disabled"); return; } const obj = collider.gameObject; const scale = getWorldScale(obj, this._tempPosition); // Prevent negative scales scale.x = Math.abs(scale.x); scale.y = Math.abs(scale.y); const finalRadius = radius * scale.x; // half height = distance between capsule origin and top sphere origin (not the top end of the capsule) height = Math.max(height, finalRadius * 2); const hh = Mathf.clamp((height * .5 * scale.y) - (radius * scale.x), 0, Number.MAX_SAFE_INTEGER); const desc = MODULES.RAPIER_PHYSICS.MODULE.ColliderDesc.capsule(hh, finalRadius); this.createCollider(collider, desc); } async addMeshCollider(collider, mesh, convex, extraScale) { // capture the geometry before waiting for phyiscs engine let geo = mesh.geometry; if (!geo) { if (debugPhysics) console.warn("Missing mesh geometry", mesh.name); return; } // check if mesh is indexed, if not generate indices if (!geo.index?.array?.length) { console.warn(`Your MeshCollider is missing vertices or indices in the assined mesh \"${mesh.name}\". Consider providing an indexed geometry.`); geo = BufferGeometryUtils.mergeVertices(geo); } let positions = null; const positionAttribute = geo.getAttribute("position"); if (positionAttribute instanceof InterleavedBufferAttribute) { const count = positionAttribute.count; positions = new Float32Array(count * 3); for (let i = 0; i < count; i++) { const x = positionAttribute.getX(i); const y = positionAttribute.getY(i); const z = positionAttribute.getZ(i); positions[i * 3] = x; positions[i * 3 + 1] = y; positions[i * 3 + 2] = z; } } else { positions = positionAttribute.array; } await this.initialize(); if (!this.enabled) { if (debugPhysics) console.warn("Physics are disabled"); return; } if (!collider.activeAndEnabled) return; // let positions = geo.getAttribute("position").array as Float32Array; const indices = geo.index?.array; const scale = collider.gameObject.worldScale.clone(); if (extraScale) scale.multiply(extraScale); // scaling seems not supported yet https://github.com/dimforge/rapier/issues/243 if (Math.abs(scale.x - 1) > 0.0001 || Math.abs(scale.y - 1) > 0.0001 || Math.abs(scale.z - 1) > 0.0001) { const key = `${geo.uuid}_${scale.x}_${scale.y}_${scale.z}_${convex}`; if (this._meshCache.has(key)) { if (debugPhysics) console.warn("Use cached mesh collider"); positions = this._meshCache.get(key); } else { if (debugPhysics || isDevEnvironment()) console.debug(`[Performance] Your MeshCollider \"${collider.name}\" is scaled: consider applying the scale to the collider mesh instead (${scale.x}, ${scale.y}, ${scale.z})`); const scaledPositions = new Float32Array(positions.length); for (let i = 0; i < positions.length; i += 3) { scaledPositions[i] = positions[i] * scale.x; scaledPositions[i + 1] = positions[i + 1] * scale.y; scaledPositions[i + 2] = positions[i + 2] * scale.z; } positions = scaledPositions; this._meshCache.set(key, scaledPositions); } } const desc = convex ? MODULES.RAPIER_PHYSICS.MODULE.ColliderDesc.convexHull(positions) : MODULES.RAPIER_PHYSICS.MODULE.ColliderDesc.trimesh(positions, indices); if (desc) { this.createCollider(collider, desc); // col.setMassProperties(1, { x: 0, y: 0, z: 0 }, { x: 0, y: 0, z: 0 }, { x: 0, y: 0, z: 0, w: 1 }); // rb?.setTranslation({ x: 0, y: 2, z: 0 }); // col.setTranslationWrtParent(new Vector3(0,2,0)); } } updatePhysicsMaterial(col) { if (!col) return; const physicsMaterial = col.sharedMaterial; const rapier_collider = col[$bodyKey]; if (!rapier_collider) return; if (physicsMaterial) { if (physicsMaterial.bounciness !== undefined) rapier_collider.setRestitution(physicsMaterial.bounciness); if (physicsMaterial.bounceCombine !== undefined) { switch (physicsMaterial.bounceCombine) { case PhysicsMaterialCombine.Average: rapier_collider.setRestitutionCombineRule(MODULES.RAPIER_PHYSICS.MODULE.CoefficientCombineRule.Average); break; case PhysicsMaterialCombine.Maximum: rapier_collider.setRestitutionCombineRule(MODULES.RAPIER_PHYSICS.MODULE.CoefficientCombineRule.Max); break; case PhysicsMaterialCombine.Minimum: rapier_collider.setRestitutionCombineRule(MODULES.RAPIER_PHYSICS.MODULE.CoefficientCombineRule.Min); break; case PhysicsMaterialCombine.Multiply: rapier_collider.setRestitutionCombineRule(MODULES.RAPIER_PHYSICS.MODULE.CoefficientCombineRule.Multiply); break; } } if (physicsMaterial.dynamicFriction !== undefined) rapier_collider.setFriction(physicsMaterial.dynamicFriction); if (physicsMaterial.frictionCombine !== undefined) { switch (physicsMaterial.frictionCombine) { case PhysicsMaterialCombine.Average: rapier_collider.setFrictionCombineRule(MODULES.RAPIER_PHYSICS.MODULE.CoefficientCombineRule.Average); break; case PhysicsMaterialCombine.Maximum: rapier_collider.setFrictionCombineRule(MODULES.RAPIER_PHYSICS.MODULE.CoefficientCombineRule.Max); break; case PhysicsMaterialCombine.Minimum: rapier_collider.setFrictionCombineRule(MODULES.RAPIER_PHYSICS.MODULE.CoefficientCombineRule.Min); break; case PhysicsMaterialCombine.Multiply: rapier_collider.setFrictionCombineRule(MODULES.RAPIER_PHYSICS.MODULE.CoefficientCombineRule.Multiply); break; } } } } /** Get the rapier body for a Needle component */ getBody(obj) { if (!obj) return null; const body = obj[$bodyKey]; return body; } /** Get the Needle Engine component for a rapier object */ getComponent(rapierObject) { if (!rapierObject) return null; const component = rapierObject[$componentKey]; return component; } createCollider(collider, desc) { if (!this.world) throw new Error("Physics world not initialized"); const matrix = this._tempMatrix; let rigidBody = undefined; if (!collider.attachedRigidbody) { if (debugPhysics) console.log("Create collider without rigidbody", collider.name); matrix.makeRotationFromQuaternion(getWorldQuaternion(collider.gameObject)); matrix.setPosition(getWorldPosition(collider.gameObject)); } else { rigidBody = this.getRigidbody(collider, this._tempMatrix); } matrix.decompose(this._tempPosition, this._tempQuaternion, this._tempScale); this.tryApplyCenter(collider, this._tempPosition); desc.setTranslation(this._tempPosition.x, this._tempPosition.y, this._tempPosition.z); desc.setRotation(this._tempQuaternion); desc.setSensor(collider.isTrigger); // TODO: we might want to update this if the material changes const physicsMaterial = collider.sharedMaterial; if (physicsMaterial) { if (physicsMaterial.bounciness !== undefined) desc.setRestitution(physicsMaterial.bounciness); if (physicsMaterial.bounceCombine !== undefined) { switch (physicsMaterial.bounceCombine) { case PhysicsMaterialCombine.Average: desc.setRestitutionCombineRule(MODULES.RAPIER_PHYSICS.MODULE.CoefficientCombineRule.Average); break; case PhysicsMaterialCombine.Maximum: desc.setRestitutionCombineRule(MODULES.RAPIER_PHYSICS.MODULE.CoefficientCombineRule.Max); break; case PhysicsMaterialCombine.Minimum: desc.setRestitutionCombineRule(MODULES.RAPIER_PHYSICS.MODULE.CoefficientCombineRule.Min); break; case PhysicsMaterialCombine.Multiply: desc.setRestitutionCombineRule(MODULES.RAPIER_PHYSICS.MODULE.CoefficientCombineRule.Multiply); break; } } if (physicsMaterial.dynamicFriction !== undefined) desc.setFriction(physicsMaterial.dynamicFriction); if (physicsMaterial.frictionCombine !== undefined) { switch (physicsMaterial.frictionCombine) { case PhysicsMaterialCombine.Average: desc.setFrictionCombineRule(MODULES.RAPIER_PHYSICS.MODULE.CoefficientCombineRule.Average); break; case PhysicsMaterialCombine.Maximum: desc.setFrictionCombineRule(MODULES.RAPIER_PHYSICS.MODULE.CoefficientCombineRule.Max); break; case PhysicsMaterialCombine.Minimum: desc.setFrictionCombineRule(MODULES.RAPIER_PHYSICS.MODULE.CoefficientCombineRule.Min); break; case PhysicsMaterialCombine.Multiply: desc.setFrictionCombineRule(MODULES.RAPIER_PHYSICS.MODULE.CoefficientCombineRule.Multiply); break; } } } // if we want to use explicit mass properties, we need to set the collider density to 0 // otherwise rapier will compute the mass properties based on the collider shape and density // https://rapier.rs/docs/user_guides/javascript/rigid_bodies#mass-properties if (collider.attachedRigidbody?.autoMass === false) { desc.setDensity(.000001); desc.setMass(.000001); } try { const col = this.world.createCollider(desc, rigidBody); col[$componentKey] = collider; collider[$bodyKey] = col; col.setActiveEvents(MODULES.RAPIER_PHYSICS.MODULE.ActiveEvents.COLLISION_EVENTS); // We want to receive collisitons between two triggers too col.setActiveCollisionTypes(MODULES.RAPIER_PHYSICS.MODULE.ActiveCollisionTypes.ALL); this.objects.push(collider); this.bodies.push(col); // set the collider layers this.updateColliderCollisionGroups(collider); return col; } catch (e) { console.error("Error creating collider \"" + collider.name + "\"\nError:", e); return null; } } /** * Updates the collision groups of a collider. * * @param collider - The collider to update. */ updateColliderCollisionGroups(collider) { const body = collider[$bodyKey]; const members = collider.membership; let memberMask = 0; if (members == undefined) { memberMask = 0xffff; } else { for (let i = 0; i < members.length; i++) { const member = members[i]; if (member > 31) console.error(`Rapier only supports 32 layers, layer ${member} is not supported`); else memberMask |= 1 << Math.floor(member); } } const mask = collider.filter; let filterMask = 0; if (mask == undefined) { filterMask = 0xffff; } else { for (let i = 0; i < mask.length; i++) { const member = mask[i]; if (member > 31) console.error(`Rapier only supports 32 layers, layer ${member} is not supported`); else filterMask |= 1 << Math.floor(member); } } body.setCollisionGroups((memberMask << 16) | filterMask); } getRigidbody(collider, _matrix) { if (!this.world) throw new Error("Physics world not initialized"); let rigidBody = null; if (collider.attachedRigidbody) { const rb = collider.attachedRigidbody; rigidBody = rb[$bodyKey]; if (!rigidBody) { const kinematic = rb.isKinematic && !debugColliderPlacement; if (debugPhysics) console.log("Create rigidbody", kinematic); const rigidBodyDesc = (kinematic ? MODULES.RAPIER_PHYSICS.MODULE.RigidBodyDesc.kinematicPositionBased() : MODULES.RAPIER_PHYSICS.MODULE.RigidBodyDesc.dynamic()); const pos = getWorldPosition(collider.attachedRigidbody.gameObject); rigidBodyDesc.setTranslation(pos.x, pos.y, pos.z); rigidBodyDesc.setRotation(getWorldQuaternion(collider.attachedRigidbody.gameObject)); rigidBodyDesc.centerOfMass = new MODULES.RAPIER_PHYSICS.MODULE.Vector3(rb.centerOfMass.x, rb.centerOfMass.y, rb.centerOfMass.z); rigidBody = this.world.createRigidBody(rigidBodyDesc); this.bodies.push(rigidBody); this.objects.push(rb); } rigidBody[$componentKey] = rb; rb[$bodyKey] = rigidBody; this.internalUpdateRigidbodyProperties(rb, rigidBody); this.getRigidbodyRelativeMatrix(collider.gameObject, rb.gameObject, _matrix); collider[$colliderRigidbody] = rigidBody; } else { const rigidBodyDesc = MODULES.RAPIER_PHYSICS.MODULE.RigidBodyDesc.kinematicPositionBased(); const pos = getWorldPosition(collider.gameObject); rigidBodyDesc.setTranslation(pos.x, pos.y, pos.z); rigidBodyDesc.setRotation(getWorldQuaternion(collider.gameObject)); rigidBody = this.world.createRigidBody(rigidBodyDesc); _matrix.identity(); rigidBody[$componentKey] = null; } return rigidBody; } internal_getRigidbody(rb) { if (rb.isCollider === true) return rb[$colliderRigidbody]; return rb[$bodyKey]; } internalUpdateColliderProperties(col, collider) { const shape = collider.shape; let sizeHasChanged = false; switch (shape.type) { // Sphere Collider case MODULES.RAPIER_PHYSICS.MODULE.ShapeType.Ball: { const ball = shape; const sc = col; const obj = col.gameObject; const scale = getWorldScale(obj, this._tempPosition); const radius = Math.abs(sc.radius * scale.x); sizeHasChanged = ball.radius !== radius; ball.radius = radius; if (sizeHasChanged) { collider.setShape(ball); } break; } case MODULES.RAPIER_PHYSICS.MODULE.ShapeType.Cuboid: const cuboid = shape; const sc = col; const obj = col.gameObject; const scale = getWorldScale(obj, this._tempPosition); const newX = Math.abs(sc.size.x * 0.5 * scale.x); const newY = Math.abs(sc.size.y * 0.5 * scale.y); const newZ = Math.abs(sc.size.z * 0.5 * scale.z); sizeHasChanged = cuboid.halfExtents.x !== newX || cuboid.halfExtents.y !== newY || cuboid.halfExtents.z !== newZ; cuboid.halfExtents.x = newX; cuboid.halfExtents.y = newY; cuboid.halfExtents.z = newZ; if (sizeHasChanged) { collider.setShape(cuboid); } break; } if (sizeHasChanged) { const rb = col.attachedRigidbody; if (rb?.autoMass) { const ph = this.getBody(rb); ph?.recomputeMassPropertiesFromColliders(); } } this.updateColliderCollisionGroups(col); if (col.isTrigger !== collider.isSensor()) collider.setSensor(col.isTrigger); } internalUpdateRigidbodyProperties(rb, rigidbody) { // continuous collision detection // https://rapier.rs/docs/user_guides/javascript/rigid_bodies#continuous-collision-detection rigidbody.enableCcd(rb.collisionDetectionMode !== CollisionDetectionMode.Discrete); rigidbody.setLinearDamping(rb.drag); rigidbody.setAngularDamping(rb.angularDrag); rigidbody.setGravityScale(rb.useGravity ? rb.gravityScale : 0, true); // https://rapier.rs/docs/user_guides/javascript/rigid_bodies#dominance if (rb.dominanceGroup <= 127 && rb.dominanceGroup >= -127) rigidbody.setDominanceGroup(Math.floor(rb.dominanceGroup)); else rigidbody.setDominanceGroup(0); if (rb.autoMass) { rigidbody.setAdditionalMass(0, false); for (let i = 0; i < rigidbody.numColliders(); i++) { const col = rigidbody.collider(i); col.setDensity(1); } rigidbody.recomputeMassPropertiesFromColliders(); } else { rigidbody.setAdditionalMass(rb.mass, false); for (let i = 0; i < rigidbody.numColliders(); i++) { const col = rigidbody.collider(i); col.setDensity(0.0000001); } rigidbody.recomputeMassPropertiesFromColliders(); } // https://rapier.rs/docs/user_guides/javascript/rigid_bodies#mass-properties // rigidbody.setAdditionalMass(rb.mass, true); // for (let i = 0; i < rigidbody.numColliders(); i++) { // const collider = rigidbody.collider(i); // if (collider) { // collider.setMass(rb.mass); // // const density = rb.mass / collider.shape.computeMassProperties().mass; // } // } // lock rotations rigidbody.setEnabledRotations(!rb.lockRotationX, !rb.lockRotationY, !rb.lockRotationZ, false); rigidbody.setEnabledTranslations(!rb.lockPositionX, !rb.lockPositionY, !rb.lockPositionZ, false); if (rb.isKinematic) { rigidbody.setBodyType(MODULES.RAPIER_PHYSICS.MODULE.RigidBodyType.KinematicPositionBased, false); } else { rigidbody.setBodyType(MODULES.RAPIER_PHYSICS.MODULE.RigidBodyType.Dynamic, false); } } // private _lastStepTime: number | undefined = 0; lines; step(dt) { if (!this.world) return; if (!this.enabled) return; this._isUpdatingPhysicsWorld = true; if (!this.eventQueue) { this.eventQueue = new MODULES.RAPIER_PHYSICS.MODULE.EventQueue(false); } if (dt === undefined || dt <= 0) { this._isUpdatingPhysicsWorld = false; return; } else if (dt !== undefined) { // if we make to sudden changes to the timestep the physics can get unstable // https://rapier.rs/docs/user_guides/javascript/integration_parameters/#dt this.world.timestep = Mathf.lerp(this.world.timestep, dt, 0.8); } try { this.world.step(this.eventQueue); } catch (e) { console.warn("Error running physics step", e); } this._isUpdatingPhysicsWorld = false; } postStep() { if (!this.world) return; if (!this.enabled) return; this._isUpdatingPhysicsWorld = true; this.syncObjects(); this._isUpdatingPhysicsWorld = false; if (this.eventQueue && !this.collisionHandler) { this.collisionHandler = new PhysicsCollisionHandler(this.world, this.eventQueue); } if (this.collisionHandler) { this.collisionHandler.handleCollisionEvents(); this.collisionHandler.update(); } this.updateDebugRendering(this.world); } updateDebugRendering(world) { if (debugPhysics || debugColliderPlacement || showColliders || this.debugRenderColliders === true) { if (!this.lines) { const material = new LineBasicMaterial({ color: 0x77dd77, fog: false, // vertexColors: VertexColors }); const geometry = new BufferGeometry(); this.lines = new LineSegments(geometry, material); this.lines.layers.disableAll(); this.lines.layers.enable(2); } if (this.lines.parent !== this.context?.scene) this.context?.scene.add(this.lines); const buffers = world.debugRender(); this.lines.geometry.setAttribute('position', new BufferAttribute(buffers.vertices, 3)); this.lines.geometry.setAttribute('color', new BufferAttribute(buffers.colors, 4)); // If a scene has no colliders at all at the start of the scene // the bounding sphere radius will be 0 and the lines will not be rendered // so we need to update the bounding sphere (perhaps it's enough to do this once...) if (this.context.time.frame % 30 === 0 || this.lines.geometry.boundingSphere?.radius === 0) { this.lines.geometry.computeBoundingSphere(); } } else { if (this.lines) { this.context?.scene.remove(this.lines); } } } /** sync rendered objects with physics world (except for colliders without rigidbody) */ syncObjects() { if (debugColliderPlacement) return; for (let i = 0; i < this.bodies.length; i++) { const obj = this.objects[i]; const body = this.bodies[i]; // if the collider is not attached to a rigidbody // it means that its kinematic so we need to update its position const col = obj; if (col?.isCollider === true && !col.attachedRigidbody) { const rigidbody = body.parent(); if (rigidbody) this.syncPhysicsBody(obj.gameObject, rigidbody, true, true); else this.syncPhysicsBody(obj.gameObject, body, true, true); continue; } // sync const pos = body.translation(); const rot = body.rotation(); if (Number.isNaN(pos.x) || Number.isNaN(rot.x)) { if (!col["__COLLIDER_NAN"] && isDevEnvironment()) { console.warn("Collider has NaN values", col.name, col.gameObject, body); col["__COLLIDER_NAN"] = true; } continue; } // make sure to keep the collider offset const center = obj["center"]; if (center && center.isVector3) { this._tempQuaternion.set(rot.x, rot.y, rot.z, rot.w); const offset = this._tempPosition.copy(center).applyQuaternion(this._tempQuaternion); const scale = getWorldScale(obj.gameObject); offset.multiply(scale); pos.x -= offset.x; pos.y -= offset.y; pos.z -= offset.z; } setWorldPositionXYZ(obj.gameObject, pos.x, pos.y, pos.z); setWorldQuaternionXYZW(obj.gameObject, rot.x, rot.y, rot.z, rot.w); } } syncPhysicsBody(obj, body, translation, rotation) { // const bodyType = body.bodyType(); // const previous = physicsBody.translation(); // const vel = physicsBody.linvel(); if (body instanceof MODULES.RAPIER_PHYSICS.MODULE.RigidBody) { const worldPosition = getWorldPosition(obj, this._tempPosition); const worldQuaternion = getWorldQuaternion(obj, this._tempQuaternion); const type = body.bodyType(); switch (type) {