@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,133 lines (1,002 loc) • 70.3 kB
text/typescript
import type { Ball, Collider, ColliderDesc, Cuboid, EventQueue, QueryFilterFlags, Ray, RigidBody, RigidBodyDesc, World } from '@dimforge/rapier3d-compat';
import { BufferAttribute, BufferGeometry, InterleavedBufferAttribute, LineBasicMaterial, LineSegments, Matrix4, Mesh, Object3D, 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 type {
IBoxCollider,
ICollider,
IComponent,
IContext,
IGameObject,
IPhysicsEngine,
IRigidbody,
ISphereCollider,
Vec2,
Vec3,
} from './engine_types.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");
declare const NEEDLE_USE_RAPIER: boolean;
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
});
}
declare type PhysicsBody = {
translation(): { x: number, y: number, z: number }
rotation(): { x: number, y: number, z: number, w: number }
}
export class RapierPhysics implements IPhysicsEngine {
debugRenderColliders: boolean = false;
debugRenderRaycasts: boolean = false;
removeBody(obj: IComponent) {
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 as Collider;
this.world?.removeCollider(rapierCollider, true);
// also remove the rigidbody if it doesnt have colliders anymore
const rapierRigidbody: RigidBody | null = rapierCollider.parent();
if (rapierRigidbody && rapierRigidbody.numColliders() <= 0) {
const rigidbody = rapierRigidbody[$componentKey] as IRigidbody;
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: ICollider | IRigidbody, translation: boolean, rotation: boolean) {
this.validate();
if (!this.enabled) return;
if (comp.destroyed || !comp.gameObject) return;
if (!translation && !rotation) return;
if ((comp as ICollider).isCollider === true) {
// const collider = comp as ICollider;
console.warn("TODO: implement updating collider position");
}
else {
const rigidbody = comp as IRigidbody;
const body = rigidbody[$bodyKey];
if (body) {
this.syncPhysicsBody(rigidbody.gameObject, body, translation, rotation);
}
}
}
updateProperties(obj: IRigidbody | ICollider) {
this.validate();
if ((obj as ICollider).isCollider) {
const col = obj as ICollider;
const body = col[$bodyKey];
if (body) {
this.internalUpdateColliderProperties(col, body);
if (col.sharedMaterial)
this.updatePhysicsMaterial(col);
}
}
else {
const rb = obj as IRigidbody;
const physicsBody = this.internal_getRigidbody(rb);
if (physicsBody) {
this.internalUpdateRigidbodyProperties(rb, physicsBody);
}
}
}
addForce(rigidbody: IRigidbody, force: Vec3, wakeup: boolean) {
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: IRigidbody, force: Vec3, wakeup: boolean) {
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: IRigidbody | ICollider): Vec3 | null {
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: IRigidbody): Vec3 | null {
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: IRigidbody, wakeup: boolean) {
this.validate();
const body = this.internal_getRigidbody(rb);
body?.resetForces(wakeup);
}
resetTorques(rb: IRigidbody, wakeup: boolean) {
this.validate();
const body = this.internal_getRigidbody(rb);
body?.resetTorques(wakeup);
}
applyImpulse(rb: IRigidbody, vec: Vec3, wakeup: boolean) {
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: IRigidbody) {
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: IRigidbody) {
this.validate();
const body = this.internal_getRigidbody(rb);
return body?.isSleeping();
}
setAngularVelocity(rb: IRigidbody, vec: Vec3, wakeup: boolean) {
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: IRigidbody, vec: Vec3, wakeup: boolean) {
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?)");
}
private readonly context?: IContext;
private _initializePromise?: Promise<boolean>;
private _isInitialized: boolean = false;
constructor(ctx: IContext) {
this.context = ctx;
}
get isInitialized() { return this._isInitialized; }
async initialize() {
if (!this._initializePromise)
this._initializePromise = this.internalInitialization();
return this._initializePromise;
}
private 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 */
private 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");
}
}
}
}
private rapierRay!: Ray;
private raycastVectorsBuffer = new CircularBuffer(() => new Vector3(), 10);
public raycast(origin?: Vec2 | Vec3, direction?: Vec3, options?: {
maxDistance?: number,
/** True if you want to also hit objects when the raycast starts from inside a collider */
solid?: boolean,
queryFilterFlags?: QueryFilterFlags,
filterGroups?: number,
/** Return false to ignore this collider */
filterPredicate?: (c: ICollider) => boolean,
/** When enabled the hit object's layer will be tested. If layer 2 is enabled the object will be ignored (Layer 2 == IgnoreRaycast)
* If not set the raycast will ignore objects in the IgnoreRaycast layer (default: true)
* @default undefined
*/
useIgnoreRaycastLayer?: boolean
})
: null | { point: Vector3, collider: ICollider } {
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;
}
public raycastAndGetNormal(origin?: Vec2 | Vec3, direction?: Vec3, options?: {
maxDistance?: number,
/** True if you want to also hit objects when the raycast starts from inside a collider */
solid?: boolean,
queryFilterFlags?: QueryFilterFlags,
filterGroups?: number,
/** Return false to ignore this collider */
filterPredicate?: (c: ICollider) => boolean,
/** When enabled the hit object's layer will be tested. If layer 2 is enabled the object will be ignored (Layer 2 == IgnoreRaycast)
* If not set the raycast will ignore objects in the IgnoreRaycast layer (default: true)
* @default undefined
*/
useIgnoreRaycastLayer?: boolean
})
: null | { point: Vector3, normal: Vector3, collider: ICollider } {
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;
}
private getPhysicsRay(ray: Ray, origin?: Vec2 | Vec3, direction?: Vec3): Ray | null {
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 as Vec3;
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;
}
private rapierSphere: Ball | null = null;
private readonly rapierColliderArray: Array<SphereOverlapResult> = [];
private readonly rapierIdentityRotation = { x: 0, y: 0, z: 0, w: 1 };
private readonly 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
*/
public sphereOverlap(point: Vector3, radius: number): Array<SphereOverlapResult> {
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] as ICollider
// 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] as ICollider
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: boolean = false;
/** Get access to the rapier world */
public get world(): World | undefined { return this._world };
private _tempPosition: Vector3 = new Vector3();
private _tempQuaternion: Quaternion = new Quaternion();
private _tempScale: Vector3 = new Vector3();
private _tempMatrix: Matrix4 = new Matrix4();
private static _didLoadPhysicsEngine: boolean = false;
private _isUpdatingPhysicsWorld: boolean = false;
get isUpdating(): boolean { return this._isUpdatingPhysicsWorld; }
private _world?: World;
private _hasCreatedWorld: boolean = false;
private eventQueue?: EventQueue;
private collisionHandler?: PhysicsCollisionHandler;
private objects: IComponent[] = [];
private bodies: PhysicsBody[] = [];
private _meshCache: Map<string, Float32Array> = new Map<string, Float32Array>();
private _gravity = { x: 0.0, y: -9.81, z: 0.0 };
get gravity() {
return this.world?.gravity ?? this._gravity;
}
set gravity(value: Vec3) {
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: ICollider, size: Vector3) {
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: ICollider) {
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: ICollider, height: number, radius: number) {
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: ICollider, mesh: Mesh, convex: boolean, extraScale?: Vector3) {
// 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: Float32Array | null = null;
const positionAttribute = geo.getAttribute("position") as BufferAttribute | InterleavedBufferAttribute;
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 as Float32Array;
}
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 as Uint32Array;
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: ICollider) {
if (!col) return;
const physicsMaterial = col.sharedMaterial;
const rapier_collider = col[$bodyKey] as Collider;
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: ICollider | IRigidbody): null | any {
if (!obj) return null;
const body = obj[$bodyKey];
return body;
}
/** Get the Needle Engine component for a rapier object */
getComponent(rapierObject: object): IComponent | null {
if (!rapierObject) return null;
const component = rapierObject[$componentKey];
return component;
}
private createCollider(collider: ICollider, desc: ColliderDesc) {
if (!this.world) throw new Error("Physics world not initialized");
const matrix = this._tempMatrix;
let rigidBody: RigidBody | undefined = 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.
*/
private updateColliderCollisionGroups(collider: ICollider) {
const body = collider[$bodyKey] as Collider;
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);
}
private getRigidbody(collider: ICollider, _matrix: Matrix4): RigidBody {
if (!this.world) throw new Error("Physics world not initialized");
let rigidBody: RigidBody | null = 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()) as RigidBodyDesc;
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;
}
private internal_getRigidbody(rb: IRigidbody | ICollider): RigidBody | null {
if ((rb as ICollider).isCollider === true) return rb[$colliderRigidbody] as RigidBody;
return rb[$bodyKey] as RigidBody;
}
private internalUpdateColliderProperties(col: ICollider, collider: Collider) {
const shape = collider.shape;
let sizeHasChanged = false;
switch (shape.type) {
// Sphere Collider
case MODULES.RAPIER_PHYSICS.MODULE.ShapeType.Ball:
{
const ball = shape as Ball;
const sc = col as ISphereCollider;
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 as Cuboid;
const sc = col as IBoxCollider;
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) as RigidBody
ph?.recomputeMassPropertiesFromColliders();
}
}
this.updateColliderCollisionGroups(col);
if (col.isTrigger !== collider.isSensor())
collider.setSensor(col.isTrigger);
}
private internalUpdateRigidbodyProperties(rb: IRigidbody, rigidbody: 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;
private lines?: LineSegments;
public step(dt?: number) {
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;
}
public 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);
}
private updateDebugRendering(world: 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)