@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
JavaScript
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) {