@babylonjs/core
Version:
Getting started? Play directly with the Babylon.js API using our [playground](https://playground.babylonjs.com/). It also contains a lot of samples to learn how to use it.
1,046 lines • 135 kB
JavaScript
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/naming-convention */
import { Matrix, Quaternion, TmpVectors, Vector3 } from "../../../Maths/math.vector.js";
import { PhysicsPrestepType, } from "../IPhysicsEnginePlugin.js";
import { PhysicsRaycastResult } from "../../physicsRaycastResult.js";
import { Logger } from "../../../Misc/logger.js";
import { PhysicsShape } from "../physicsShape.js";
import { BoundingBox } from "../../../Culling/boundingBox.js";
import { Mesh } from "../../../Meshes/mesh.js";
import { InstancedMesh } from "../../../Meshes/instancedMesh.js";
import { VertexBuffer } from "../../../Buffers/buffer.js";
import { BuildArray } from "../../../Misc/arrayTools.js";
import { Observable } from "../../../Misc/observable.js";
import { FloatingOriginCurrentScene } from "../../../Materials/floatingOriginMatrixOverrides.js";
class MeshAccumulator {
/**
* Constructor of the mesh accumulator
* @param mesh - The mesh used to compute the world matrix.
* @param collectIndices - use mesh indices
* @param scene - The scene used to determine the right handed system.
*
* Merge mesh and its children so whole hierarchy can be used as a mesh shape or convex hull
*/
constructor(mesh, collectIndices, scene) {
this._vertices = []; /// Vertices in body space
this._indices = [];
this._isRightHanded = scene.useRightHandedSystem;
this._collectIndices = collectIndices;
}
/**
* Adds a mesh to the physics engine.
* @param mesh The mesh to add.
* @param includeChildren Whether to include the children of the mesh.
*
* This method adds a mesh to the physics engine by computing the world matrix,
* multiplying it with the body from world matrix, and then transforming the
* coordinates of the mesh's vertices. It also adds the indices of the mesh
* to the physics engine. If includeChildren is true, it will also add the
* children of the mesh to the physics engine, ignoring any children which
* have a physics impostor. This is useful for creating a physics engine
* that accurately reflects the mesh and its children.
*/
addNodeMeshes(mesh, includeChildren) {
// Force absoluteScaling to be computed; we're going to use that to bake
// the scale of any parent nodes into this shape, as physics engines
// usually use rigid transforms, so can't handle arbitrary scale.
mesh.computeWorldMatrix(true);
const rootScaled = TmpVectors.Matrix[0];
Matrix.ScalingToRef(mesh.absoluteScaling.x, mesh.absoluteScaling.y, mesh.absoluteScaling.z, rootScaled);
if (mesh instanceof Mesh) {
this._addMesh(mesh, rootScaled);
}
else if (mesh instanceof InstancedMesh) {
this._addMesh(mesh.sourceMesh, rootScaled);
}
if (includeChildren) {
const worldToRoot = TmpVectors.Matrix[1];
mesh.computeWorldMatrix().invertToRef(worldToRoot);
const worldToRootScaled = TmpVectors.Matrix[2];
worldToRoot.multiplyToRef(rootScaled, worldToRootScaled);
const children = mesh.getChildMeshes(false);
// Ignore any children which have a physics body.
// Other plugin implementations do not have this check, which appears to be
// a bug, as otherwise, the mesh will have a duplicate collider
const transformNodes = children.filter((m) => !m.physicsBody);
for (const m of transformNodes) {
const childToWorld = m.computeWorldMatrix();
const childToRootScaled = TmpVectors.Matrix[3];
childToWorld.multiplyToRef(worldToRootScaled, childToRootScaled);
if (m instanceof Mesh) {
this._addMesh(m, childToRootScaled);
}
else if (m instanceof InstancedMesh) {
this._addMesh(m.sourceMesh, childToRootScaled);
}
}
}
}
_addMesh(mesh, meshToRoot) {
const vertexData = mesh.getVerticesData(VertexBuffer.PositionKind) || [];
const numVerts = vertexData.length / 3;
const indexOffset = this._vertices.length;
for (let v = 0; v < numVerts; v++) {
const pos = new Vector3(vertexData[v * 3 + 0], vertexData[v * 3 + 1], vertexData[v * 3 + 2]);
this._vertices.push(Vector3.TransformCoordinates(pos, meshToRoot));
}
if (this._collectIndices) {
const meshIndices = mesh.getIndices();
if (meshIndices) {
for (let i = 0; i < meshIndices.length; i += 3) {
// Havok wants the correct triangle winding to enable the interior triangle optimization
if (this._isRightHanded) {
this._indices.push(meshIndices[i + 0] + indexOffset);
this._indices.push(meshIndices[i + 1] + indexOffset);
this._indices.push(meshIndices[i + 2] + indexOffset);
}
else {
this._indices.push(meshIndices[i + 2] + indexOffset);
this._indices.push(meshIndices[i + 1] + indexOffset);
this._indices.push(meshIndices[i + 0] + indexOffset);
}
}
}
}
}
/**
* Allocate and populate the vertex positions inside the physics plugin.
*
* @param plugin - The plugin to allocate the memory in.
* @returns An array of floats, whose backing memory is inside the plugin. The array contains the
* positions of the mesh vertices, where a position is defined by three floats. You must call
* freeBuffer() on the returned array once you have finished with it, in order to free the
* memory inside the plugin..
*/
getVertices(plugin) {
const nFloats = this._vertices.length * 3;
const bytesPerFloat = 4;
const nBytes = nFloats * bytesPerFloat;
const bufferBegin = plugin._malloc(nBytes);
const ret = new Float32Array(plugin.HEAPU8.buffer, bufferBegin, nFloats);
for (let i = 0; i < this._vertices.length; i++) {
ret[i * 3 + 0] = this._vertices[i].x;
ret[i * 3 + 1] = this._vertices[i].y;
ret[i * 3 + 2] = this._vertices[i].z;
}
return { offset: bufferBegin, numObjects: nFloats };
}
freeBuffer(plugin, arr) {
plugin._free(arr.offset);
}
/**
* Allocate and populate the triangle indices inside the physics plugin
*
* @param plugin - The plugin to allocate the memory in.
* @returns A new Int32Array, whose backing memory is inside the plugin. The array contains the indices
* of the triangle positions, where a single triangle is defined by three indices. You must call
* freeBuffer() on this array once you have finished with it, to free the memory inside the plugin..
*/
getTriangles(plugin) {
const bytesPerInt = 4;
const nBytes = this._indices.length * bytesPerInt;
const bufferBegin = plugin._malloc(nBytes);
const ret = new Int32Array(plugin.HEAPU8.buffer, bufferBegin, this._indices.length);
for (let i = 0; i < this._indices.length; i++) {
ret[i] = this._indices[i];
}
return { offset: bufferBegin, numObjects: this._indices.length };
}
}
class BodyPluginData {
constructor(bodyId) {
this.hpBodyId = bodyId;
this.userMassProps = { centerOfMass: undefined, mass: undefined, inertia: undefined, inertiaOrientation: undefined };
}
}
/*
class ShapePath
{
public colliderId: number;
public pathData: number;
}
*/
class CollisionContactPoint {
constructor() {
this.bodyId = BigInt(0); //0,2
//public colliderId: number = 0; //2,4
//public shapePath: ShapePath = new ShapePath(); //4,8
this.position = new Vector3(); //8,11
this.normal = new Vector3(); //11,14
//public triIdx: number = 0; //14,15
}
}
class CollisionEvent {
constructor() {
this.contactOnA = new CollisionContactPoint(); //1
this.contactOnB = new CollisionContactPoint();
this.impulseApplied = 0;
this.type = 0;
}
// eslint-disable-next-line @typescript-eslint/naming-convention
static readToRef(buffer, offset, eventOut) {
const intBuf = new Int32Array(buffer, offset);
const floatBuf = new Float32Array(buffer, offset);
const offA = 2;
eventOut.contactOnA.bodyId = BigInt(intBuf[offA]); //<todo Need to get the high+low words!
eventOut.contactOnA.position.set(floatBuf[offA + 8], floatBuf[offA + 9], floatBuf[offA + 10]);
eventOut.contactOnA.normal.set(floatBuf[offA + 11], floatBuf[offA + 12], floatBuf[offA + 13]);
const offB = 18;
eventOut.contactOnB.bodyId = BigInt(intBuf[offB]);
eventOut.contactOnB.position.set(floatBuf[offB + 8], floatBuf[offB + 9], floatBuf[offB + 10]);
eventOut.contactOnB.normal.set(floatBuf[offB + 11], floatBuf[offB + 12], floatBuf[offB + 13]);
eventOut.impulseApplied = floatBuf[offB + 13 + 3];
eventOut.type = intBuf[0];
}
}
class TriggerEvent {
constructor() {
this.bodyIdA = BigInt(0);
this.bodyIdB = BigInt(0);
this.type = 0;
}
// eslint-disable-next-line @typescript-eslint/naming-convention
static readToRef(buffer, offset, eventOut) {
const intBuf = new Int32Array(buffer, offset);
eventOut.type = intBuf[0];
eventOut.bodyIdA = BigInt(intBuf[2]);
eventOut.bodyIdB = BigInt(intBuf[6]);
}
}
/**
* The Havok Physics plugin
*/
export class HavokPlugin {
/**
* Finds an existing world region that contains the given world position,
* or creates a new world region centered at that position.
*
* When floatingOriginMode is enabled, we use multiple Havok worlds to maintain
* float32 precision across a large world. Each world region has its own fixed
* floating origin, and bodies within that region are simulated relative to it.
*
* @param worldPosition - The world position of the body being created
* @returns The world region to use for this body
*/
_getOrCreateWorldRegion(worldPosition) {
// Check if floating origin mode is enabled
const scene = FloatingOriginCurrentScene.getScene();
if (!scene?.floatingOriginMode) {
// When floating origin mode is disabled, use the default world region
return this._worldRegions[0];
}
// Find an existing region that contains this position
for (const region of this._worldRegions) {
const distance = Vector3.Distance(worldPosition, region.floatingOrigin);
if (distance <= this._floatingOriginWorldRadius) {
return region;
}
}
// No existing region found - create a new one centered at this position
const newWorld = this._hknp.HP_World_Create()[1];
// Apply stored gravity to new world
this._hknp.HP_World_SetGravity(newWorld, this._currentGravity);
// Copy velocity limits from main world
this._hknp.HP_World_SetSpeedLimit(newWorld, this.getMaxLinearVelocity(), this.getMaxAngularVelocity());
const newRegion = {
world: newWorld,
floatingOrigin: worldPosition.clone(),
gravity: [...this._currentGravity],
};
this._worldRegions.push(newRegion);
return newRegion;
}
/**
* Checks if a body's world position has left its current region and, if so,
* moves it to the correct region (existing or newly created).
* This preserves linear and angular velocity across the transition.
*
* @param pluginData - The plugin data for the body (or instance) to check
*/
_reRegionBodyPluginData(pluginData) {
const currentRegion = pluginData.worldRegion;
if (!currentRegion) {
return;
}
// Read the body's current local transform from Havok
const bodyTransform = this._hknp.HP_Body_GetQTransform(pluginData.hpBodyId)[1];
const localPos = bodyTransform[0];
const orientation = bodyTransform[1];
// Compute world position = local position + floating origin
const worldPos = TmpVectors.Vector3[2];
worldPos.set(localPos[0] + currentRegion.floatingOrigin._x, localPos[1] + currentRegion.floatingOrigin._y, localPos[2] + currentRegion.floatingOrigin._z);
// Check if still within the current region.
// Use a 20% hysteresis margin so that bodies near the boundary
// don't oscillate between regions every frame.
const distToCurrent = Vector3.Distance(worldPos, currentRegion.floatingOrigin);
if (distToCurrent <= this._floatingOriginWorldRadius * 1.2) {
return; // still in region (with hysteresis), nothing to do
}
// Body has left its region. Use velocity to look ahead and find the best
// region to join. This prevents creating throwaway intermediate regions
// when a fast body is heading toward an existing region (e.g. a target).
const linVel = this._hknp.HP_Body_GetLinearVelocity(pluginData.hpBodyId)[1];
const angVel = this._hknp.HP_Body_GetAngularVelocity(pluginData.hpBodyId)[1];
// Project position forward by one second of travel to find a suitable existing region.
// This is purely for region selection — the body's actual position is not changed.
const lookAheadPos = TmpVectors.Vector3[3];
lookAheadPos.set(worldPos._x + linVel[0], worldPos._y + linVel[1], worldPos._z + linVel[2]);
// Try to find the best region: first check if look-ahead position falls in an existing region
let newRegion = this._findExistingRegion(lookAheadPos);
if (!newRegion || newRegion === currentRegion) {
// Fall back to current position
newRegion = this._findExistingRegion(worldPos);
}
if (!newRegion || newRegion === currentRegion) {
// No existing region works — create one at the current position
newRegion = this._getOrCreateWorldRegion(worldPos);
}
if (newRegion === currentRegion) {
return;
}
// Remove from old world
this._hknp.HP_World_RemoveBody(currentRegion.world, pluginData.hpBodyId);
// Set transform relative to the new region's floating origin
const newOffset = newRegion.floatingOrigin;
const newLocalPos = [worldPos._x - newOffset._x, worldPos._y - newOffset._y, worldPos._z - newOffset._z];
this._hknp.HP_Body_SetQTransform(pluginData.hpBodyId, [newLocalPos, orientation]);
// Add to new world
this._hknp.HP_World_AddBody(newRegion.world, pluginData.hpBodyId, false);
// Restore velocity
this._hknp.HP_Body_SetLinearVelocity(pluginData.hpBodyId, linVel);
this._hknp.HP_Body_SetAngularVelocity(pluginData.hpBodyId, angVel);
// Update the region reference and world transform offset
pluginData.worldRegion = newRegion;
pluginData.worldTransformOffset = this._hknp.HP_Body_GetWorldTransformOffset(pluginData.hpBodyId)[1];
// Garbage-collect the old region if it has no bodies left and it's not the default world
if (currentRegion !== this._worldRegions[0]) {
const bodyCount = this._hknp.HP_World_GetNumBodies(currentRegion.world)[1];
if (bodyCount === 0) {
this._hknp.HP_World_Release(currentRegion.world);
const idx = this._worldRegions.indexOf(currentRegion);
if (idx > 0) {
this._worldRegions.splice(idx, 1);
}
}
}
}
/**
* Searches existing world regions for one that contains the given position.
* @param worldPosition - The world position to find a region for
* @returns null if no existing region contains it (does NOT create a new one).
*/
_findExistingRegion(worldPosition) {
const scene = FloatingOriginCurrentScene.getScene();
if (!scene?.floatingOriginMode) {
return this._worldRegions[0];
}
for (const region of this._worldRegions) {
const distance = Vector3.Distance(worldPosition, region.floatingOrigin);
if (distance <= this._floatingOriginWorldRadius) {
return region;
}
}
return null;
}
constructor(_useDeltaForWorldStep = true, hpInjection = HK, parameters = {}) {
this._useDeltaForWorldStep = _useDeltaForWorldStep;
/**
* Reference to the WASM library
*/
this._hknp = {};
/**
* Name of the plugin
*/
this.name = "HavokPlugin";
this._multiQueryCollector = undefined;
this._fixedTimeStep = 1 / 60;
this._maxQueryCollectorHits = 1;
this._tmpVec3 = BuildArray(3, Vector3.Zero);
this._bodies = new Map();
this._shapes = new Map();
this._bodyCollisionObservable = new Map();
// Map from constraint id to the pair of bodies, where the first is the parent and the second is the child
this._constraintToBodyIdPair = new Map();
this._bodyCollisionEndedObservable = new Map();
/**
* Array of world regions. The first region is always the default world with origin at Vector3.Zero.
* Additional regions are created as needed for floating origin mode.
*/
this._worldRegions = [];
/**
* Stored gravity value to apply to new world regions.
*/
this._currentGravity = [0, -9.81, 0];
/**
* Radius of each floating origin world region.
* Bodies within this radius of a world region's origin will use that world.
*/
this._floatingOriginWorldRadius = 100000;
/**
* Observable for collision started and collision continued events
*/
this.onCollisionObservable = new Observable();
/**
* Observable for collision ended events
*/
this.onCollisionEndedObservable = new Observable();
/**
* Observable for trigger entered and trigger exited events
*/
this.onTriggerCollisionObservable = new Observable();
if (typeof hpInjection === "function") {
Logger.Error("Havok is not ready. Please make sure you await HK() before using the plugin.");
return;
}
else {
this._hknp = hpInjection;
}
if (!this.isSupported()) {
Logger.Error("Havok is not available. Please make sure you included the js file.");
return;
}
this.world = this._hknp.HP_World_Create()[1];
// Add the default world as the first region with origin at zero
this._worldRegions.push({
world: this.world,
floatingOrigin: Vector3.Zero(),
gravity: [...this._currentGravity],
});
this._queryCollector = this._hknp.HP_QueryCollector_Create(1)[1];
this.setMaxQueryCollectorHits(parameters.maxQueryCollectorHits ?? 1);
this._floatingOriginWorldRadius = parameters.floatingOriginWorldRadius ?? 100000;
}
/**
* If this plugin is supported
* @returns true if its supported
*/
isSupported() {
return this._hknp !== undefined;
}
/**
* Sets the gravity of the physics world.
*
* @param gravity - The gravity vector to set.
* @param worldPosition - Optional world position to specify which region's gravity to set.
* If provided, only the region containing this position will be updated.
* If not provided, all regions will be updated (default behavior).
* This is useful for planetary scenarios where gravity direction varies by location.
*/
setGravity(gravity, worldPosition) {
const gravityArray = this._bVecToV3(gravity);
if (worldPosition) {
// Set gravity for a specific region based on world position
const region = this._getOrCreateWorldRegion(worldPosition);
region.gravity = gravityArray;
this._hknp.HP_World_SetGravity(region.world, gravityArray);
}
else {
// Set gravity for all regions (default behavior)
this._currentGravity = gravityArray;
for (const region of this._worldRegions) {
region.gravity = gravityArray;
this._hknp.HP_World_SetGravity(region.world, gravityArray);
}
}
}
/**
* Gets the gravity of the physics world or a specific region.
*
* @param worldPosition - Optional world position to get the gravity for that region.
* If not provided, returns the default gravity.
* @returns The gravity vector.
*/
getGravity(worldPosition) {
if (worldPosition) {
const region = this._getOrCreateWorldRegion(worldPosition);
return new Vector3(region.gravity[0], region.gravity[1], region.gravity[2]);
}
return new Vector3(this._currentGravity[0], this._currentGravity[1], this._currentGravity[2]);
}
/**
* Sets the fixed time step for the physics engine.
*
* @param timeStep - The fixed time step to use for the physics engine.
*
*/
setTimeStep(timeStep) {
this._fixedTimeStep = timeStep;
}
/**
* Gets the fixed time step used by the physics engine.
*
* @returns The fixed time step used by the physics engine.
*
*/
getTimeStep() {
return this._fixedTimeStep;
}
/**
* Sets the maximum number of raycast hits to process.
*
* @param maxQueryCollectorHits - The maximum number of raycast hits to process.
*/
setMaxQueryCollectorHits(maxQueryCollectorHits) {
if (maxQueryCollectorHits === this._maxQueryCollectorHits) {
return;
}
if (this._multiQueryCollector) {
this._hknp.HP_QueryCollector_Release(this._multiQueryCollector);
this._multiQueryCollector = undefined;
}
if (maxQueryCollectorHits > 1) {
this._multiQueryCollector = this._hknp.HP_QueryCollector_Create(maxQueryCollectorHits)[1];
}
}
/**
* Gets the maximum number of raycast hits to process.
*
* @returns The maximum number of raycast hits to process.
*/
getMaxQueryCollectorHits() {
return this._maxQueryCollectorHits;
}
/**
* Executes a single step of the physics engine.
*
* @param delta The time delta in seconds since the last step.
* @param physicsBodies An array of physics bodies to be simulated.
*
* This method is useful for simulating the physics engine. It sets the physics body transformation,
* steps the world, syncs the physics body, and notifies collisions. This allows for the physics engine
* to accurately simulate the physics bodies in the world.
*/
executeStep(delta, physicsBodies) {
// Re-region bodies that have moved outside their current world region
// BEFORE pre-step and stepping, so the body participates in the correct
// world's step and its body buffer transform is valid when sync reads it.
if (this._worldRegions.length > 1 || FloatingOriginCurrentScene.getScene()?.floatingOriginMode) {
for (const physicsBody of physicsBodies) {
if (physicsBody._pluginDataInstances.length > 0) {
for (const instance of physicsBody._pluginDataInstances) {
this._reRegionBodyPluginData(instance);
}
}
else if (physicsBody._pluginData) {
this._reRegionBodyPluginData(physicsBody._pluginData);
}
}
}
for (const physicsBody of physicsBodies) {
if (physicsBody.disablePreStep) {
continue;
}
this.setPhysicsBodyTransformation(physicsBody, physicsBody.transformNode);
}
const deltaTime = this._useDeltaForWorldStep ? delta : this._fixedTimeStep;
// Step all world regions
for (const region of this._worldRegions) {
this._hknp.HP_World_SetIdealStepTime(region.world, deltaTime);
this._hknp.HP_World_Step(region.world, deltaTime);
}
for (const physicsBody of physicsBodies) {
if (!physicsBody.disableSync) {
this.sync(physicsBody);
}
}
// Notify collisions and triggers for all world regions
for (const region of this._worldRegions) {
this._notifyCollisions(region.world);
this._notifyTriggers(region.world);
}
}
/**
* Returns the version of the physics engine plugin.
*
* @returns The version of the physics engine plugin.
*
* This method is useful for determining the version of the physics engine plugin that is currently running.
*/
getPluginVersion() {
return 2;
}
/**
* Set the maximum allowed linear and angular velocities
* @param maxLinearVelocity maximum allowed linear velocity
* @param maxAngularVelocity maximum allowed angular velocity
*/
setVelocityLimits(maxLinearVelocity, maxAngularVelocity) {
for (const region of this._worldRegions) {
this._hknp.HP_World_SetSpeedLimit(region.world, maxLinearVelocity, maxAngularVelocity);
}
}
/**
* @returns maximum allowed linear velocity
*/
getMaxLinearVelocity() {
const limits = this._hknp.HP_World_GetSpeedLimit(this.world);
return limits[1];
}
/**
* @returns maximum allowed angular velocity
*/
getMaxAngularVelocity() {
const limits = this._hknp.HP_World_GetSpeedLimit(this.world);
return limits[2];
}
/**
* Initializes a physics body with the given position and orientation.
*
* @param body - The physics body to initialize.
* @param motionType - The motion type of the body.
* @param position - The position of the body.
* @param orientation - The orientation of the body.
* This code is useful for initializing a physics body with the given position and orientation.
* It creates a plugin data for the body and adds it to the world. It then converts the position
* and orientation to a transform and sets the body's transform to the given values.
*/
initBody(body, motionType, position, orientation) {
body._pluginData = new BodyPluginData(this._hknp.HP_Body_Create()[1]);
this._internalSetMotionType(body._pluginData, motionType);
// Get the world region for this body's position
const worldRegion = this._getOrCreateWorldRegion(position);
body._pluginData.worldRegion = worldRegion;
const offset = worldRegion.floatingOrigin;
const transform = [[position._x - offset._x, position._y - offset._y, position._z - offset._z], this._bQuatToV4(orientation)];
this._hknp.HP_Body_SetQTransform(body._pluginData.hpBodyId, transform);
this._hknp.HP_World_AddBody(worldRegion.world, body._pluginData.hpBodyId, body.startAsleep);
this._bodies.set(body._pluginData.hpBodyId[0], { body: body, index: 0 });
}
/**
* Removes a body from the world. To dispose of a body, it is necessary to remove it from the world first.
*
* @param body - The body to remove.
*/
removeBody(body) {
if (body._pluginDataInstances && body._pluginDataInstances.length > 0) {
for (const instance of body._pluginDataInstances) {
this._bodyCollisionObservable.delete(instance.hpBodyId[0]);
this._hknp.HP_World_RemoveBody(instance.worldRegion.world, instance.hpBodyId);
this._bodies.delete(instance.hpBodyId[0]);
}
}
if (body._pluginData) {
this._bodyCollisionObservable.delete(body._pluginData.hpBodyId[0]);
this._hknp.HP_World_RemoveBody(body._pluginData.worldRegion.world, body._pluginData.hpBodyId);
this._bodies.delete(body._pluginData.hpBodyId[0]);
}
}
/**
* Initializes the body instances for a given physics body and mesh.
*
* @param body - The physics body to initialize.
* @param motionType - How the body will be handled by the engine
* @param mesh - The mesh to initialize.
*
* This code is useful for creating a physics body from a mesh. It creates a
* body instance for each instance of the mesh and adds it to the world. It also
* sets the position of the body instance to the position of the mesh instance.
* This allows for the physics engine to accurately simulate the mesh in the
* world.
*/
initBodyInstances(body, motionType, mesh) {
const instancesCount = mesh._thinInstanceDataStorage?.instancesCount ?? 0;
const matrixData = mesh._thinInstanceDataStorage.matrixData;
if (!matrixData) {
return; // TODO: error handling
}
this._createOrUpdateBodyInstances(body, motionType, matrixData, 0, instancesCount, false);
for (let index = 0; index < body._pluginDataInstances.length; index++) {
const bodyId = body._pluginDataInstances[index];
this._bodies.set(bodyId.hpBodyId[0], { body: body, index: index });
}
}
_createOrUpdateBodyInstances(body, motionType, matrixData, startIndex, endIndex, update) {
const rotation = TmpVectors.Quaternion[0];
const rotationMatrix = Matrix.Identity();
const worldPos = TmpVectors.Vector3[0];
for (let i = startIndex; i < endIndex; i++) {
// Get world position for this instance
worldPos.set(matrixData[i * 16 + 12], matrixData[i * 16 + 13], matrixData[i * 16 + 14]);
let hkbody;
let pluginData;
if (!update) {
hkbody = this._hknp.HP_Body_Create()[1];
pluginData = new BodyPluginData(hkbody);
if (body._pluginDataInstances.length) {
// If an instance already exists, copy any user-provided mass properties
pluginData.userMassProps = body._pluginDataInstances[0].userMassProps;
}
}
else {
pluginData = body._pluginDataInstances[i];
hkbody = pluginData.hpBodyId;
}
// Get the world region for this instance's position
const worldRegion = this._getOrCreateWorldRegion(worldPos);
const offset = worldRegion.floatingOrigin;
// Subtract floating origin offset to get small coordinates for Havok (float32 precision)
const position = [worldPos._x - offset._x, worldPos._y - offset._y, worldPos._z - offset._z];
rotationMatrix.setRowFromFloats(0, matrixData[i * 16 + 0], matrixData[i * 16 + 1], matrixData[i * 16 + 2], 0);
rotationMatrix.setRowFromFloats(1, matrixData[i * 16 + 4], matrixData[i * 16 + 5], matrixData[i * 16 + 6], 0);
rotationMatrix.setRowFromFloats(2, matrixData[i * 16 + 8], matrixData[i * 16 + 9], matrixData[i * 16 + 10], 0);
Quaternion.FromRotationMatrixToRef(rotationMatrix, rotation);
const transform = [position, [rotation.x, rotation.y, rotation.z, rotation.w]];
this._hknp.HP_Body_SetQTransform(hkbody, transform);
if (!update) {
this._internalSetMotionType(pluginData, motionType);
this._internalUpdateMassProperties(pluginData);
body._pluginDataInstances.push(pluginData);
// Add to the appropriate world
pluginData.worldRegion = worldRegion;
this._hknp.HP_World_AddBody(worldRegion.world, hkbody, body.startAsleep);
pluginData.worldTransformOffset = this._hknp.HP_Body_GetWorldTransformOffset(hkbody)[1];
}
}
}
/**
* Update the internal body instances for a given physics body to match the instances in a mesh.
* @param body the body that will be updated
* @param mesh the mesh with reference instances
*/
updateBodyInstances(body, mesh) {
const instancesCount = mesh._thinInstanceDataStorage?.instancesCount ?? 0;
const matrixData = mesh._thinInstanceDataStorage.matrixData;
if (!matrixData) {
return; // TODO: error handling
}
const pluginInstancesCount = body._pluginDataInstances.length;
const motionType = this.getMotionType(body);
if (instancesCount > pluginInstancesCount) {
this._createOrUpdateBodyInstances(body, motionType, matrixData, pluginInstancesCount, instancesCount, false);
const firstBodyShape = this._hknp.HP_Body_GetShape(body._pluginDataInstances[0].hpBodyId)[1];
// firstBodyShape[0] might be 0 in the case where thin instances data is set (with thinInstancesSetBuffer call) after body creation
// in that case, use the shape provided at body creation.
if (!firstBodyShape[0]) {
firstBodyShape[0] = body.shape?._pluginData[0];
}
for (let i = pluginInstancesCount; i < instancesCount; i++) {
this._hknp.HP_Body_SetShape(body._pluginDataInstances[i].hpBodyId, firstBodyShape);
this._internalUpdateMassProperties(body._pluginDataInstances[i]);
this._bodies.set(body._pluginDataInstances[i].hpBodyId[0], { body: body, index: i });
}
}
else if (instancesCount < pluginInstancesCount) {
const instancesToRemove = pluginInstancesCount - instancesCount;
for (let i = 0; i < instancesToRemove; i++) {
const hkbody = body._pluginDataInstances.pop();
this._bodies.delete(hkbody.hpBodyId[0]);
this._hknp.HP_World_RemoveBody(hkbody.worldRegion.world, hkbody.hpBodyId);
this._hknp.HP_Body_Release(hkbody.hpBodyId);
}
this._createOrUpdateBodyInstances(body, motionType, matrixData, 0, instancesCount, true);
}
}
/**
* Synchronizes the transform of a physics body with its transform node.
* @param body - The physics body to synchronize.
*
* This function is useful for keeping the physics body's transform in sync with its transform node.
* This is important for ensuring that the physics body is accurately represented in the physics engine.
*/
sync(body) {
this.syncTransform(body, body.transformNode);
}
/**
* Synchronizes the transform of a physics body with the transform of its
* corresponding transform node.
*
* @param body - The physics body to synchronize.
* @param transformNode - The destination Transform Node.
*
* This code is useful for synchronizing the position and orientation of a
* physics body with the position and orientation of its corresponding
* transform node. This is important for ensuring that the physics body and
* the transform node are in the same position and orientation in the scene.
* This is necessary for the physics engine to accurately simulate the
* physical behavior of the body.
*/
syncTransform(body, transformNode) {
// Get the floating origin offset - this was subtracted when positions were sent to Havok
// We need to add it back to get the correct world position
if (body._pluginDataInstances.length) {
// instances
const m = transformNode;
const matrixData = m._thinInstanceDataStorage.matrixData;
if (!matrixData) {
return; // TODO: error handling
}
const instancesCount = body._pluginDataInstances.length;
for (let i = 0; i < instancesCount; i++) {
const pluginData = body._pluginDataInstances[i];
// Use instance's world region offset
const instanceOffset = pluginData.worldRegion.floatingOrigin;
// Get body buffer from the instance's own world region (not always the default world)
const bodyBuffer = this._hknp.HP_World_GetBodyBuffer(pluginData.worldRegion.world)[1];
const bufOffset = pluginData.worldTransformOffset;
const transformBuffer = new Float32Array(this._hknp.HEAPU8.buffer, bodyBuffer + bufOffset, 16);
const index = i * 16;
for (let mi = 0; mi < 15; mi++) {
if ((mi & 3) != 3) {
matrixData[index + mi] = transformBuffer[mi];
}
}
// Add back the floating origin offset to get world position
// (Havok stores position - offset, so we add offset back)
matrixData[index + 12] += instanceOffset._x;
matrixData[index + 13] += instanceOffset._y;
matrixData[index + 14] += instanceOffset._z;
matrixData[index + 15] = 1;
}
m.thinInstanceBufferUpdated("matrix");
}
else {
try {
const bodyTransform = this._hknp.HP_Body_GetQTransform(body._pluginData.hpBodyId)[1];
const bodyTranslation = bodyTransform[0];
const bodyOrientation = bodyTransform[1];
const quat = TmpVectors.Quaternion[0];
// Use body's world region offset
const offset = body._pluginData.worldRegion.floatingOrigin;
quat.set(bodyOrientation[0], bodyOrientation[1], bodyOrientation[2], bodyOrientation[3]);
// Add back the floating origin offset to get world position
const worldX = bodyTranslation[0] + offset._x;
const worldY = bodyTranslation[1] + offset._y;
const worldZ = bodyTranslation[2] + offset._z;
const parent = transformNode.parent;
// transform position/orientation in parent space
if (parent && !parent.getWorldMatrix().isIdentity()) {
parent.computeWorldMatrix(true);
// Save scaling for future use
TmpVectors.Vector3[1].copyFrom(transformNode.scaling);
quat.normalize();
const finalTransform = TmpVectors.Matrix[0];
const finalTranslation = TmpVectors.Vector3[0];
finalTranslation.copyFromFloats(worldX, worldY, worldZ);
Matrix.ComposeToRef(transformNode.absoluteScaling, quat, finalTranslation, finalTransform);
const parentInverseTransform = TmpVectors.Matrix[1];
parent.getWorldMatrix().invertToRef(parentInverseTransform);
const localTransform = TmpVectors.Matrix[2];
finalTransform.multiplyToRef(parentInverseTransform, localTransform);
localTransform.decomposeToTransformNode(transformNode);
transformNode.rotationQuaternion?.normalize();
// Keep original scaling. Re-injecting scaling can introduce discontinuity between frames. Basically, it grows or shrinks.
transformNode.scaling.copyFrom(TmpVectors.Vector3[1]);
}
else {
transformNode.position.set(worldX, worldY, worldZ);
if (transformNode.rotationQuaternion) {
transformNode.rotationQuaternion.copyFrom(quat);
}
else {
quat.toEulerAnglesToRef(transformNode.rotation);
}
}
}
catch (e) {
Logger.Error(`Syncing transform failed for node ${transformNode.name}: ${e.message}...`);
}
}
}
/**
* Sets the shape of a physics body.
* @param body - The physics body to set the shape for.
* @param shape - The physics shape to set.
*
* This function is used to set the shape of a physics body. It is useful for
* creating a physics body with a specific shape, such as a box or a sphere,
* which can then be used to simulate physical interactions in a physics engine.
* This function is especially useful for meshes with multiple instances, as it
* will set the shape for each instance of the mesh.
*/
setShape(body, shape) {
const shapeHandle = shape && shape._pluginData ? shape._pluginData : BigInt(0);
if (!(body.transformNode instanceof Mesh) || !body.transformNode._thinInstanceDataStorage?.matrixData) {
this._hknp.HP_Body_SetShape(body._pluginData.hpBodyId, shapeHandle);
this._internalUpdateMassProperties(body._pluginData);
return;
}
const m = body.transformNode;
const instancesCount = m._thinInstanceDataStorage?.instancesCount ?? 0;
for (let i = 0; i < instancesCount; i++) {
this._hknp.HP_Body_SetShape(body._pluginDataInstances[i].hpBodyId, shapeHandle);
this._internalUpdateMassProperties(body._pluginDataInstances[i]);
}
}
/**
* Returns a reference to the first instance of the plugin data for a physics body.
* @param body
* @param instanceIndex
* @returns a reference to the first instance
*/
_getPluginReference(body, instanceIndex) {
return body._pluginDataInstances?.length ? body._pluginDataInstances[instanceIndex ?? 0] : body._pluginData;
}
/**
* Gets the shape of a physics body. This will create a new shape object
*
* @param body - The physics body.
* @returns The shape of the physics body.
*
*/
getShape(body) {
const pluginRef = this._getPluginReference(body);
const shapePluginData = this._hknp.HP_Body_GetShape(pluginRef.hpBodyId)[1];
if (shapePluginData != 0) {
const scene = body.transformNode.getScene();
return new PhysicsShape({ pluginData: shapePluginData }, scene);
}
return null;
}
/**
* Gets the type of a physics shape.
* @param shape - The physics shape to get the type for.
* @returns The type of the physics shape.
*
*/
getShapeType(shape) {
if (shape.type) {
return shape.type;
}
else {
//<todo This returns a native type!
return this._hknp.HP_Shape_GetType(shape._pluginData);
}
}
/**
* Sets the event mask of a physics body.
* @param body - The physics body to set the event mask for.
* @param eventMask - The event mask to set.
* @param instanceIndex - The index of the instance to set the event mask for
*
* This function is useful for setting the event mask of a physics body, which is used to determine which events the body will respond to. This is important for ensuring that the physics engine is able to accurately simulate the behavior of the body in the game world.
*/
setEventMask(body, eventMask, instanceIndex) {
this._applyToBodyOrInstances(body, (bodyPluginData) => {
this._hknp.HP_Body_SetEventMask(bodyPluginData.hpBodyId, eventMask);
}, instanceIndex);
}
/**
* Retrieves the event mask of a physics body.
*
* @param body - The physics body to retrieve the event mask from.
* @param instanceIndex - The index of the instance to retrieve the event mask from.
* @returns The event mask of the physics body.
*
*/
getEventMask(body, instanceIndex) {
const pluginRef = this._getPluginReference(body, instanceIndex);
return this._hknp.HP_Body_GetEventMask(pluginRef.hpBodyId)[1];
}
_fromMassPropertiesTuple(massPropsTuple) {
return {
centerOfMass: Vector3.FromArray(massPropsTuple[0]),
mass: massPropsTuple[1],
inertia: Vector3.FromArray(massPropsTuple[2]),
inertiaOrientation: Quaternion.FromArray(massPropsTuple[3]),
};
}
_internalUpdateMassProperties(pluginData) {
// Recompute the mass based on the shape
const newProps = this._internalComputeMassProperties(pluginData);
const massProps = pluginData.userMassProps;
// Override the computed values with any the user has set
if (massProps.centerOfMass) {
newProps[0] = massProps.centerOfMass.asArray();
}
if (massProps.mass != undefined) {
newProps[1] = massProps.mass;
}
if (massProps.inertia) {
newProps[2] = massProps.inertia.asArray();
}
if (massProps.inertiaOrientation) {
newProps[3] = massProps.inertiaOrientation.asArray();
}
this._hknp.HP_Body_SetMassProperties(pluginData.hpBodyId, newProps);
}
_internalSetMotionType(pluginData, motionType) {
switch (motionType) {
case 0 /* PhysicsMotionType.STATIC */:
this._hknp.HP_Body_SetMotionType(pluginData.hpBodyId, this._hknp.MotionType.STATIC);
break;
case 1 /* PhysicsMotionType.ANIMATED */:
this._hknp.HP_Body_SetMotionType(pluginData.hpBodyId, this._hknp.MotionType.KINEMATIC);
break;
case 2 /* PhysicsMotionType.DYNAMIC */:
this._hknp.HP_Body_SetMotionType(pluginData.hpBodyId, this._hknp.MotionType.DYNAMIC);
break;
}
}
/**
* sets the motion type of a physics body.
* @param body - The physics body to set the motion type for.
* @param motionType - The motion type to set.
* @param instanceIndex - The index of the instance to set the motion type for. If undefined, the motion type of all the bodies will be set.
*/
setMotionType(body, motionType, instanceIndex) {
this._applyToBodyOrInstances(body, (pluginData) => {
this._internalSetMotionType(pluginData, motionType);
}, instanceIndex);
}
/**
* Gets the motion type of a physics body.
* @param body - The physics body to get the motion type from.
* @param instanceIndex - The index of the instance to get the motion type from. If not specified, the motion type of the first instance will be returned.
* @returns The motion type of the physics body.
*/
getMotionType(body, instanceIndex) {
const pluginRef = this._getPluginReference(body, instanceIndex);
const type = this._hknp.HP_Body_GetMotionType(pluginRef.hpBodyId)[1];
switch (type) {
case this._hknp.MotionType.STATIC:
return 0 /* PhysicsMotionType.STATIC */;
case this._hknp.MotionType.KINEMATIC:
return 1 /* PhysicsMotionType.ANIMATED */;
case this._hknp.MotionType.DYNAMIC:
return 2 /* PhysicsMotionType.DYNAMIC */;
}
throw new Error("Unknown motion type: " + type);
}
/**
* sets the activation control mode of a physics body, for instance if you need the body to never sleep.
* @param body - The physics body to set the activation control mode.
* @param controlMode - The activation control mode.
*/
setActivationControl(body, controlMode) {
switch (controlMode) {
case 1 /* PhysicsActivationControl.ALWAYS_ACTIVE */:
this._hknp.HP_Body_SetActivationControl(body._pluginData.hpBodyId, this._hknp.ActivationControl.ALWAYS_ACTIVE);
break;
case 2 /* PhysicsActivationControl.ALWAYS_INACTIVE */:
this._hknp.HP_Body_SetActivationControl(body._pluginData.hpBodyId, this._hknp.ActivationControl.ALWAYS_INACTIVE);
break;
case 0 /* PhysicsActivationControl.SIMULATION_CONTROLLED */:
this._hknp.HP_Body_SetActivationControl(body._pluginData.hpBodyId, this._hknp.ActivationControl.SIMULATION_CONTROLLED);
break;
}
}
_internalComputeMassProperties(pluginData) {
const shapeRes = this._hknp.HP_Body_GetShape(pluginData.hpBodyId);
if (shapeRes[0] == this._hknp.Result.RESULT_OK) {
const shapeMass = this._hknp.HP_Shape_BuildMassProperties(shapeRes[1]);
if (shapeMass[0] == this._hknp.Result.RESULT_OK) {
return shapeMass[1];
}
}
// Failed; return a unit inertia
return [[0, 0, 0], 1, [1, 1, 1], [0, 0, 0, 1]];
}
/**
* Computes the mass properties of a physics body, from it's shape
*
* @param body - The physics body to copmute the mass properties of
* @param instanceIndex - The index of the instance to compute the mass properties of.
* @returns The mass properties of the physics body.
*/
computeMassProperties(body, instanceIndex) {
const pluginRef = this._getPluginReference(body, instanceIndex);
const computed = this._internalComputeMassProperties(pluginRef);
return this._fromMassPropertiesTuple(computed);
}
/**
* Sets the mass properties of a physics body.
*
* @param body - The physics body to set the mass properties of.
* @param massProps - The mass properties to set.
* @param instanceIndex - T