@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,002 lines • 67.9 kB
JavaScript
import { Quaternion, Vector3, Matrix } from "../../../Maths/math.vector.js";
import { Logger } from "../../../Misc/logger.js";
import { PhysicsImpostor } from "../physicsImpostor.js";
import { PhysicsJoint } from "../physicsJoint.js";
import { VertexBuffer } from "../../../Buffers/buffer.js";
import { VertexData } from "../../../Meshes/mesh.vertexData.js";
import { ExtrudeShape } from "../../../Meshes/Builders/shapeBuilder.js";
import { CreateLines } from "../../../Meshes/Builders/linesBuilder.js";
import { PhysicsRaycastResult } from "../../physicsRaycastResult.js";
import { WithinEpsilon } from "../../../Maths/math.scalar.functions.js";
import { Epsilon } from "../../../Maths/math.constants.js";
/**
* AmmoJS Physics plugin
* @see https://doc.babylonjs.com/features/featuresDeepDive/physics/usingPhysicsEngine
* @see https://github.com/kripken/ammo.js/
*/
export class AmmoJSPlugin {
/**
* Initializes the ammoJS plugin
* @param _useDeltaForWorldStep if the time between frames should be used when calculating physics steps (Default: true)
* @param ammoInjection can be used to inject your own ammo reference
* @param overlappingPairCache can be used to specify your own overlapping pair cache
*/
constructor(_useDeltaForWorldStep = true, ammoInjection = Ammo, overlappingPairCache = null) {
this._useDeltaForWorldStep = _useDeltaForWorldStep;
/**
* Reference to the Ammo library
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
this.bjsAMMO = {};
/**
* Name of the plugin
*/
this.name = "AmmoJSPlugin";
this._timeStep = 1 / 60;
this._fixedTimeStep = 1 / 60;
this._maxSteps = 5;
this._tmpQuaternion = new Quaternion();
this._tmpContactCallbackResult = false;
this._tmpContactPoint = new Vector3();
this._tmpContactNormal = new Vector3();
this._tmpVec3 = new Vector3();
this._tmpMatrix = new Matrix();
if (typeof ammoInjection === "function") {
Logger.Error("AmmoJS is not ready. Please make sure you await Ammo() before using the plugin.");
return;
}
else {
this.bjsAMMO = ammoInjection;
}
if (!this.isSupported()) {
Logger.Error("AmmoJS is not available. Please make sure you included the js file.");
return;
}
// Initialize the physics world
this._collisionConfiguration = new this.bjsAMMO.btSoftBodyRigidBodyCollisionConfiguration();
this._dispatcher = new this.bjsAMMO.btCollisionDispatcher(this._collisionConfiguration);
this._overlappingPairCache = overlappingPairCache || new this.bjsAMMO.btDbvtBroadphase();
this._solver = new this.bjsAMMO.btSequentialImpulseConstraintSolver();
this._softBodySolver = new this.bjsAMMO.btDefaultSoftBodySolver();
this.world = new this.bjsAMMO.btSoftRigidDynamicsWorld(this._dispatcher, this._overlappingPairCache, this._solver, this._collisionConfiguration, this._softBodySolver);
this._tmpAmmoConcreteContactResultCallback = new this.bjsAMMO.ConcreteContactResultCallback();
this._tmpAmmoConcreteContactResultCallback.addSingleResult = (contactPoint) => {
contactPoint = this.bjsAMMO.wrapPointer(contactPoint, this.bjsAMMO.btManifoldPoint);
const worldPoint = contactPoint.getPositionWorldOnA();
const worldNormal = contactPoint.m_normalWorldOnB;
this._tmpContactPoint.x = worldPoint.x();
this._tmpContactPoint.y = worldPoint.y();
this._tmpContactPoint.z = worldPoint.z();
this._tmpContactNormal.x = worldNormal.x();
this._tmpContactNormal.y = worldNormal.y();
this._tmpContactNormal.z = worldNormal.z();
this._tmpContactImpulse = contactPoint.getAppliedImpulse();
this._tmpContactDistance = contactPoint.getDistance();
this._tmpContactCallbackResult = true;
};
this._raycastResult = new PhysicsRaycastResult();
// Create temp ammo variables
this._tmpAmmoTransform = new this.bjsAMMO.btTransform();
this._tmpAmmoTransform.setIdentity();
this._tmpAmmoQuaternion = new this.bjsAMMO.btQuaternion(0, 0, 0, 1);
this._tmpAmmoVectorA = new this.bjsAMMO.btVector3(0, 0, 0);
this._tmpAmmoVectorB = new this.bjsAMMO.btVector3(0, 0, 0);
this._tmpAmmoVectorC = new this.bjsAMMO.btVector3(0, 0, 0);
this._tmpAmmoVectorD = new this.bjsAMMO.btVector3(0, 0, 0);
}
/**
*
* @returns plugin version
*/
getPluginVersion() {
return 1;
}
/**
* Sets the gravity of the physics world (m/(s^2))
* @param gravity Gravity to set
*/
setGravity(gravity) {
this._tmpAmmoVectorA.setValue(gravity.x, gravity.y, gravity.z);
this.world.setGravity(this._tmpAmmoVectorA);
this.world.getWorldInfo().set_m_gravity(this._tmpAmmoVectorA);
}
/**
* Amount of time to step forward on each frame (only used if useDeltaForWorldStep is false in the constructor)
* @param timeStep timestep to use in seconds
*/
setTimeStep(timeStep) {
this._timeStep = timeStep;
}
/**
* Increment to step forward in the physics engine (If timeStep is set to 1/60 and fixedTimeStep is set to 1/120 the physics engine should run 2 steps per frame) (Default: 1/60)
* @param fixedTimeStep fixedTimeStep to use in seconds
*/
setFixedTimeStep(fixedTimeStep) {
this._fixedTimeStep = fixedTimeStep;
}
/**
* Sets the maximum number of steps by the physics engine per frame (Default: 5)
* @param maxSteps the maximum number of steps by the physics engine per frame
*/
setMaxSteps(maxSteps) {
this._maxSteps = maxSteps;
}
/**
* Gets the current timestep (only used if useDeltaForWorldStep is false in the constructor)
* @returns the current timestep in seconds
*/
getTimeStep() {
return this._timeStep;
}
// Ammo's contactTest and contactPairTest take a callback that runs synchronously, wrap them so that they are easier to consume
_isImpostorInContact(impostor) {
this._tmpContactCallbackResult = false;
this.world.contactTest(impostor.physicsBody, this._tmpAmmoConcreteContactResultCallback);
return this._tmpContactCallbackResult;
}
// Ammo's collision events have some weird quirks
// contactPairTest fires too many events as it fires events even when objects are close together but contactTest does not
// so only fire event if both contactTest and contactPairTest have a hit
_isImpostorPairInContact(impostorA, impostorB) {
this._tmpContactCallbackResult = false;
this.world.contactPairTest(impostorA.physicsBody, impostorB.physicsBody, this._tmpAmmoConcreteContactResultCallback);
return this._tmpContactCallbackResult;
}
// Ammo's behavior when maxSteps > 0 does not behave as described in docs
// @see http://www.bulletphysics.org/mediawiki-1.5.8/index.php/Stepping_The_World
//
// When maxSteps is 0 do the entire simulation in one step
// When maxSteps is > 0, run up to maxStep times, if on the last step the (remaining step - fixedTimeStep) is < fixedTimeStep, the remainder will be used for the step. (eg. if remainder is 1.001 and fixedTimeStep is 1 the last step will be 1.001, if instead it did 2 steps (1, 0.001) issues occuered when having a tiny step in ammo)
// Note: To get deterministic physics, timeStep would always need to be divisible by fixedTimeStep
_stepSimulation(timeStep = 1 / 60, maxSteps = 10, fixedTimeStep = 1 / 60) {
if (maxSteps == 0) {
this.world.stepSimulation(timeStep, 0);
}
else {
while (maxSteps > 0 && timeStep > 0) {
if (timeStep - fixedTimeStep < fixedTimeStep) {
this.world.stepSimulation(timeStep, 0);
timeStep = 0;
}
else {
timeStep -= fixedTimeStep;
this.world.stepSimulation(fixedTimeStep, 0);
}
maxSteps--;
}
}
}
/**
* Moves the physics simulation forward delta seconds and updates the given physics imposters
* Prior to the step the imposters physics location is set to the position of the babylon meshes
* After the step the babylon meshes are set to the position of the physics imposters
* @param delta amount of time to step forward
* @param impostors array of imposters to update before/after the step
*/
executeStep(delta, impostors) {
for (const impostor of impostors) {
// Update physics world objects to match babylon world
if (!impostor.soft) {
impostor.beforeStep();
}
}
this._stepSimulation(this._useDeltaForWorldStep ? delta : this._timeStep, this._maxSteps, this._fixedTimeStep);
for (const mainImpostor of impostors) {
// After physics update make babylon world objects match physics world objects
if (mainImpostor.soft) {
this._afterSoftStep(mainImpostor);
}
else {
mainImpostor.afterStep();
}
// Handle collision event
if (mainImpostor._onPhysicsCollideCallbacks.length > 0) {
if (this._isImpostorInContact(mainImpostor)) {
for (const collideCallback of mainImpostor._onPhysicsCollideCallbacks) {
for (const otherImpostor of collideCallback.otherImpostors) {
if (mainImpostor.physicsBody.isActive() || otherImpostor.physicsBody.isActive()) {
if (this._isImpostorPairInContact(mainImpostor, otherImpostor)) {
mainImpostor.onCollide({
body: otherImpostor.physicsBody,
point: this._tmpContactPoint,
distance: this._tmpContactDistance,
impulse: this._tmpContactImpulse,
normal: this._tmpContactNormal,
});
otherImpostor.onCollide({
body: mainImpostor.physicsBody,
point: this._tmpContactPoint,
distance: this._tmpContactDistance,
impulse: this._tmpContactImpulse,
normal: this._tmpContactNormal,
});
}
}
}
}
}
}
}
}
/**
* Update babylon mesh to match physics world object
* @param impostor imposter to match
*/
_afterSoftStep(impostor) {
if (impostor.type === PhysicsImpostor.RopeImpostor) {
this._ropeStep(impostor);
}
else {
this._softbodyOrClothStep(impostor);
}
}
/**
* Update babylon mesh vertices vertices to match physics world softbody or cloth
* @param impostor imposter to match
*/
_ropeStep(impostor) {
const bodyVertices = impostor.physicsBody.get_m_nodes();
const nbVertices = bodyVertices.size();
let node;
let nodePositions;
let x, y, z;
const path = [];
for (let n = 0; n < nbVertices; n++) {
node = bodyVertices.at(n);
nodePositions = node.get_m_x();
x = nodePositions.x();
y = nodePositions.y();
z = nodePositions.z();
path.push(new Vector3(x, y, z));
}
const object = impostor.object;
const shape = impostor.getParam("shape");
if (impostor._isFromLine) {
impostor.object = CreateLines("lines", { points: path, instance: object });
}
else {
impostor.object = ExtrudeShape("ext", { shape: shape, path: path, instance: object });
}
}
/**
* Update babylon mesh vertices vertices to match physics world softbody or cloth
* @param impostor imposter to match
*/
_softbodyOrClothStep(impostor) {
const normalDirection = impostor.type === PhysicsImpostor.ClothImpostor ? 1 : -1;
const object = impostor.object;
let vertexPositions = object.getVerticesData(VertexBuffer.PositionKind);
if (!vertexPositions) {
vertexPositions = [];
}
let vertexNormals = object.getVerticesData(VertexBuffer.NormalKind);
if (!vertexNormals) {
vertexNormals = [];
}
const nbVertices = vertexPositions.length / 3;
const bodyVertices = impostor.physicsBody.get_m_nodes();
let node;
let nodePositions;
let x, y, z;
let nx, ny, nz;
for (let n = 0; n < nbVertices; n++) {
node = bodyVertices.at(n);
nodePositions = node.get_m_x();
x = nodePositions.x();
y = nodePositions.y();
z = nodePositions.z() * normalDirection;
const nodeNormals = node.get_m_n();
nx = nodeNormals.x();
ny = nodeNormals.y();
nz = nodeNormals.z() * normalDirection;
vertexPositions[3 * n] = x;
vertexPositions[3 * n + 1] = y;
vertexPositions[3 * n + 2] = z;
vertexNormals[3 * n] = nx;
vertexNormals[3 * n + 1] = ny;
vertexNormals[3 * n + 2] = nz;
}
const vertexData = new VertexData();
vertexData.positions = vertexPositions;
vertexData.normals = vertexNormals;
vertexData.uvs = object.getVerticesData(VertexBuffer.UVKind);
vertexData.colors = object.getVerticesData(VertexBuffer.ColorKind);
if (object && object.getIndices) {
vertexData.indices = object.getIndices();
}
vertexData.applyToMesh(object);
}
/**
* Applies an impulse on the imposter
* @param impostor imposter to apply impulse to
* @param force amount of force to be applied to the imposter
* @param contactPoint the location to apply the impulse on the imposter
*/
applyImpulse(impostor, force, contactPoint) {
if (!impostor.soft) {
impostor.physicsBody.activate();
const worldPoint = this._tmpAmmoVectorA;
const impulse = this._tmpAmmoVectorB;
// Convert contactPoint relative to center of mass
if (impostor.object && impostor.object.getWorldMatrix) {
contactPoint.subtractInPlace(impostor.object.getWorldMatrix().getTranslation());
}
worldPoint.setValue(contactPoint.x, contactPoint.y, contactPoint.z);
impulse.setValue(force.x, force.y, force.z);
impostor.physicsBody.applyImpulse(impulse, worldPoint);
}
else {
Logger.Warn("Cannot be applied to a soft body");
}
}
/**
* Applies a force on the imposter
* @param impostor imposter to apply force
* @param force amount of force to be applied to the imposter
* @param contactPoint the location to apply the force on the imposter
*/
applyForce(impostor, force, contactPoint) {
if (!impostor.soft) {
impostor.physicsBody.activate();
const worldPoint = this._tmpAmmoVectorA;
const impulse = this._tmpAmmoVectorB;
// Convert contactPoint relative to center of mass
if (impostor.object && impostor.object.getWorldMatrix) {
const localTranslation = impostor.object.getWorldMatrix().getTranslation();
worldPoint.setValue(contactPoint.x - localTranslation.x, contactPoint.y - localTranslation.y, contactPoint.z - localTranslation.z);
}
else {
worldPoint.setValue(contactPoint.x, contactPoint.y, contactPoint.z);
}
impulse.setValue(force.x, force.y, force.z);
impostor.physicsBody.applyForce(impulse, worldPoint);
}
else {
Logger.Warn("Cannot be applied to a soft body");
}
}
/**
* Creates a physics body using the plugin
* @param impostor the imposter to create the physics body on
*/
generatePhysicsBody(impostor) {
// Note: this method will not be called on child imposotrs for compound impostors
impostor._pluginData.toDispose = [];
//parent-child relationship
if (impostor.parent) {
if (impostor.physicsBody) {
this.removePhysicsBody(impostor);
impostor.forceUpdate();
}
return;
}
if (impostor.isBodyInitRequired()) {
const colShape = this._createShape(impostor);
const mass = impostor.getParam("mass");
impostor._pluginData.mass = mass;
if (impostor.soft) {
colShape.get_m_cfg().set_collisions(0x11);
colShape.get_m_cfg().set_kDP(impostor.getParam("damping"));
this.bjsAMMO.castObject(colShape, this.bjsAMMO.btCollisionObject).getCollisionShape().setMargin(impostor.getParam("margin"));
colShape.setActivationState(AmmoJSPlugin._DISABLE_DEACTIVATION_FLAG);
this.world.addSoftBody(colShape, 1, -1);
impostor.physicsBody = colShape;
impostor._pluginData.toDispose.push(colShape);
this.setBodyPressure(impostor, 0);
if (impostor.type === PhysicsImpostor.SoftbodyImpostor) {
this.setBodyPressure(impostor, impostor.getParam("pressure"));
}
this.setBodyStiffness(impostor, impostor.getParam("stiffness"));
this.setBodyVelocityIterations(impostor, impostor.getParam("velocityIterations"));
this.setBodyPositionIterations(impostor, impostor.getParam("positionIterations"));
}
else {
const localInertia = new this.bjsAMMO.btVector3(0, 0, 0);
const startTransform = new this.bjsAMMO.btTransform();
impostor.object.computeWorldMatrix(true);
startTransform.setIdentity();
if (mass !== 0) {
colShape.calculateLocalInertia(mass, localInertia);
}
this._tmpAmmoVectorA.setValue(impostor.object.position.x, impostor.object.position.y, impostor.object.position.z);
this._tmpAmmoQuaternion.setValue(impostor.object.rotationQuaternion.x, impostor.object.rotationQuaternion.y, impostor.object.rotationQuaternion.z, impostor.object.rotationQuaternion.w);
startTransform.setOrigin(this._tmpAmmoVectorA);
startTransform.setRotation(this._tmpAmmoQuaternion);
const myMotionState = new this.bjsAMMO.btDefaultMotionState(startTransform);
const rbInfo = new this.bjsAMMO.btRigidBodyConstructionInfo(mass, myMotionState, colShape, localInertia);
const body = new this.bjsAMMO.btRigidBody(rbInfo);
// Make objects kinematic if it's mass is 0
if (mass === 0) {
body.setCollisionFlags(body.getCollisionFlags() | AmmoJSPlugin._KINEMATIC_FLAG);
body.setActivationState(AmmoJSPlugin._DISABLE_DEACTIVATION_FLAG);
}
// Disable collision if NoImpostor, but keep collision if shape is btCompoundShape
if (impostor.type == PhysicsImpostor.NoImpostor && !colShape.getChildShape) {
body.setCollisionFlags(body.getCollisionFlags() | AmmoJSPlugin._DISABLE_COLLISION_FLAG);
}
// compute delta position: compensate the difference between shape center and mesh origin
if (impostor.type !== PhysicsImpostor.MeshImpostor && impostor.type !== PhysicsImpostor.NoImpostor) {
const boundingInfo = impostor.object.getBoundingInfo();
this._tmpVec3.copyFrom(impostor.object.getAbsolutePosition());
this._tmpVec3.subtractInPlace(boundingInfo.boundingBox.centerWorld);
this._tmpVec3.x /= impostor.object.scaling.x;
this._tmpVec3.y /= impostor.object.scaling.y;
this._tmpVec3.z /= impostor.object.scaling.z;
impostor.setDeltaPosition(this._tmpVec3);
}
const group = impostor.getParam("group");
const mask = impostor.getParam("mask");
if (group && mask) {
this.world.addRigidBody(body, group, mask);
}
else {
this.world.addRigidBody(body);
}
impostor.physicsBody = body;
impostor._pluginData.toDispose = impostor._pluginData.toDispose.concat([body, rbInfo, myMotionState, startTransform, localInertia, colShape]);
}
this.setBodyRestitution(impostor, impostor.getParam("restitution"));
this.setBodyFriction(impostor, impostor.getParam("friction"));
}
}
/**
* Removes the physics body from the imposter and disposes of the body's memory
* @param impostor imposter to remove the physics body from
*/
removePhysicsBody(impostor) {
if (this.world) {
if (impostor.soft) {
this.world.removeSoftBody(impostor.physicsBody);
}
else {
this.world.removeRigidBody(impostor.physicsBody);
}
if (impostor._pluginData) {
for (const d of impostor._pluginData.toDispose) {
this.bjsAMMO.destroy(d);
}
impostor._pluginData.toDispose = [];
}
}
}
/**
* Generates a joint
* @param impostorJoint the imposter joint to create the joint with
*/
generateJoint(impostorJoint) {
const mainBody = impostorJoint.mainImpostor.physicsBody;
const connectedBody = impostorJoint.connectedImpostor.physicsBody;
if (!mainBody || !connectedBody) {
return;
}
// if the joint is already created, don't create it again for preventing memory leaks
if (impostorJoint.joint.physicsJoint) {
return;
}
const jointData = impostorJoint.joint.jointData;
if (!jointData.mainPivot) {
jointData.mainPivot = new Vector3(0, 0, 0);
}
if (!jointData.connectedPivot) {
jointData.connectedPivot = new Vector3(0, 0, 0);
}
let joint;
switch (impostorJoint.joint.type) {
case PhysicsJoint.DistanceJoint: {
const distance = jointData.maxDistance;
if (distance) {
jointData.mainPivot = new Vector3(0, -distance / 2, 0);
jointData.connectedPivot = new Vector3(0, distance / 2, 0);
}
const mainPivot = this._tmpAmmoVectorA;
mainPivot.setValue(jointData.mainPivot.x, jointData.mainPivot.y, jointData.mainPivot.z);
const connectedPivot = this._tmpAmmoVectorB;
connectedPivot.setValue(jointData.connectedPivot.x, jointData.connectedPivot.y, jointData.connectedPivot.z);
joint = new this.bjsAMMO.btPoint2PointConstraint(mainBody, connectedBody, mainPivot, connectedPivot);
break;
}
case PhysicsJoint.HingeJoint: {
if (!jointData.mainAxis) {
jointData.mainAxis = new Vector3(0, 0, 0);
}
if (!jointData.connectedAxis) {
jointData.connectedAxis = new Vector3(0, 0, 0);
}
const mainPivot = this._tmpAmmoVectorA;
mainPivot.setValue(jointData.mainPivot.x, jointData.mainPivot.y, jointData.mainPivot.z);
const connectedPivot = this._tmpAmmoVectorB;
connectedPivot.setValue(jointData.connectedPivot.x, jointData.connectedPivot.y, jointData.connectedPivot.z);
const mainAxis = this._tmpAmmoVectorC;
mainAxis.setValue(jointData.mainAxis.x, jointData.mainAxis.y, jointData.mainAxis.z);
const connectedAxis = this._tmpAmmoVectorD;
connectedAxis.setValue(jointData.connectedAxis.x, jointData.connectedAxis.y, jointData.connectedAxis.z);
joint = new this.bjsAMMO.btHingeConstraint(mainBody, connectedBody, mainPivot, connectedPivot, mainAxis, connectedAxis);
break;
}
case PhysicsJoint.BallAndSocketJoint: {
const mainPivot = this._tmpAmmoVectorA;
mainPivot.setValue(jointData.mainPivot.x, jointData.mainPivot.y, jointData.mainPivot.z);
const connectedPivot = this._tmpAmmoVectorB;
connectedPivot.setValue(jointData.connectedPivot.x, jointData.connectedPivot.y, jointData.connectedPivot.z);
joint = new this.bjsAMMO.btPoint2PointConstraint(mainBody, connectedBody, mainPivot, connectedPivot);
break;
}
default: {
Logger.Warn("JointType not currently supported by the Ammo plugin, falling back to PhysicsJoint.BallAndSocketJoint");
const mainPivot = this._tmpAmmoVectorA;
mainPivot.setValue(jointData.mainPivot.x, jointData.mainPivot.y, jointData.mainPivot.z);
const connectedPivot = this._tmpAmmoVectorB;
connectedPivot.setValue(jointData.connectedPivot.x, jointData.connectedPivot.y, jointData.connectedPivot.z);
joint = new this.bjsAMMO.btPoint2PointConstraint(mainBody, connectedBody, mainPivot, connectedPivot);
break;
}
}
this.world.addConstraint(joint, !impostorJoint.joint.jointData.collision);
impostorJoint.joint.physicsJoint = joint;
}
/**
* Removes a joint
* @param impostorJoint the imposter joint to remove the joint from
*/
removeJoint(impostorJoint) {
if (this.world) {
this.world.removeConstraint(impostorJoint.joint.physicsJoint);
}
this.bjsAMMO.destroy(impostorJoint.joint.physicsJoint);
}
// adds all verticies (including child verticies) to the triangle mesh
_addMeshVerts(btTriangleMesh, topLevelObject, object) {
let triangleCount = 0;
if (object && object.getIndices && object.getWorldMatrix && object.getChildMeshes) {
let indices = object.getIndices();
if (!indices) {
indices = [];
}
let vertexPositions = object.getVerticesData(VertexBuffer.PositionKind);
if (!vertexPositions) {
vertexPositions = [];
}
let localMatrix;
if (topLevelObject && topLevelObject !== object) {
// top level matrix used for shape transform doesn't take scale into account.
// Moreover, every children vertex position must be in that space.
// So, each vertex position here is transform by (mesh world matrix * toplevelMatrix -1)
let topLevelQuaternion;
if (topLevelObject.rotationQuaternion) {
topLevelQuaternion = topLevelObject.rotationQuaternion;
}
else if (topLevelObject.rotation) {
topLevelQuaternion = Quaternion.FromEulerAngles(topLevelObject.rotation.x, topLevelObject.rotation.y, topLevelObject.rotation.z);
}
else {
topLevelQuaternion = Quaternion.Identity();
}
const topLevelMatrix = Matrix.Compose(Vector3.One(), topLevelQuaternion, topLevelObject.position);
topLevelMatrix.invertToRef(this._tmpMatrix);
const wm = object.computeWorldMatrix(false);
localMatrix = wm.multiply(this._tmpMatrix);
}
else {
// current top level is same as object level -> only use local scaling
Matrix.ScalingToRef(object.scaling.x, object.scaling.y, object.scaling.z, this._tmpMatrix);
localMatrix = this._tmpMatrix;
}
const faceCount = indices.length / 3;
for (let i = 0; i < faceCount; i++) {
const triPoints = [];
for (let point = 0; point < 3; point++) {
let v = new Vector3(vertexPositions[indices[i * 3 + point] * 3 + 0], vertexPositions[indices[i * 3 + point] * 3 + 1], vertexPositions[indices[i * 3 + point] * 3 + 2]);
v = Vector3.TransformCoordinates(v, localMatrix);
let vec;
if (point == 0) {
vec = this._tmpAmmoVectorA;
}
else if (point == 1) {
vec = this._tmpAmmoVectorB;
}
else {
vec = this._tmpAmmoVectorC;
}
vec.setValue(v.x, v.y, v.z);
triPoints.push(vec);
}
btTriangleMesh.addTriangle(triPoints[0], triPoints[1], triPoints[2]);
triangleCount++;
}
const childMeshes = object.getChildMeshes();
for (const m of childMeshes) {
triangleCount += this._addMeshVerts(btTriangleMesh, topLevelObject, m);
}
}
return triangleCount;
}
/**
* Initialise the soft body vertices to match its object's (mesh) vertices
* Softbody vertices (nodes) are in world space and to match this
* The object's position and rotation is set to zero and so its vertices are also then set in world space
* @param impostor to create the softbody for
* @returns the number of vertices added to the softbody
*/
_softVertexData(impostor) {
const object = impostor.object;
if (object && object.getIndices && object.getWorldMatrix && object.getChildMeshes) {
let indices = object.getIndices();
if (!indices) {
indices = [];
}
let vertexPositions = object.getVerticesData(VertexBuffer.PositionKind);
if (!vertexPositions) {
vertexPositions = [];
}
let vertexNormals = object.getVerticesData(VertexBuffer.NormalKind);
if (!vertexNormals) {
vertexNormals = [];
}
object.computeWorldMatrix(false);
const newPoints = [];
const newNorms = [];
for (let i = 0; i < vertexPositions.length; i += 3) {
let v = new Vector3(vertexPositions[i], vertexPositions[i + 1], vertexPositions[i + 2]);
let n = new Vector3(vertexNormals[i], vertexNormals[i + 1], vertexNormals[i + 2]);
v = Vector3.TransformCoordinates(v, object.getWorldMatrix());
n = Vector3.TransformNormal(n, object.getWorldMatrix());
newPoints.push(v.x, v.y, v.z);
newNorms.push(n.x, n.y, n.z);
}
const vertexData = new VertexData();
vertexData.positions = newPoints;
vertexData.normals = newNorms;
vertexData.uvs = object.getVerticesData(VertexBuffer.UVKind);
vertexData.colors = object.getVerticesData(VertexBuffer.ColorKind);
if (object && object.getIndices) {
vertexData.indices = object.getIndices();
}
vertexData.applyToMesh(object);
object.position = Vector3.Zero();
object.rotationQuaternion = null;
object.rotation = Vector3.Zero();
object.computeWorldMatrix(true);
return vertexData;
}
return VertexData.ExtractFromMesh(object);
}
/**
* Create an impostor's soft body
* @param impostor to create the softbody for
* @returns the softbody
*/
_createSoftbody(impostor) {
const object = impostor.object;
if (object && object.getIndices) {
let indices = object.getIndices();
if (!indices) {
indices = [];
}
const vertexData = this._softVertexData(impostor);
const vertexPositions = vertexData.positions;
const vertexNormals = vertexData.normals;
if (vertexPositions === null || vertexNormals === null) {
return new this.bjsAMMO.btCompoundShape();
}
else {
const triPoints = [];
const triNorms = [];
for (let i = 0; i < vertexPositions.length; i += 3) {
const v = new Vector3(vertexPositions[i], vertexPositions[i + 1], vertexPositions[i + 2]);
const n = new Vector3(vertexNormals[i], vertexNormals[i + 1], vertexNormals[i + 2]);
triPoints.push(v.x, v.y, -v.z);
triNorms.push(n.x, n.y, -n.z);
}
const softBody = new this.bjsAMMO.btSoftBodyHelpers().CreateFromTriMesh(this.world.getWorldInfo(), triPoints, object.getIndices(), indices.length / 3, true);
const nbVertices = vertexPositions.length / 3;
const bodyVertices = softBody.get_m_nodes();
let node;
let nodeNormals;
for (let i = 0; i < nbVertices; i++) {
node = bodyVertices.at(i);
nodeNormals = node.get_m_n();
nodeNormals.setX(triNorms[3 * i]);
nodeNormals.setY(triNorms[3 * i + 1]);
nodeNormals.setZ(triNorms[3 * i + 2]);
}
return softBody;
}
}
}
/**
* Create cloth for an impostor
* @param impostor to create the softbody for
* @returns the cloth
*/
_createCloth(impostor) {
const object = impostor.object;
if (object && object.getIndices) {
let indices = object.getIndices();
if (!indices) {
indices = [];
}
const vertexData = this._softVertexData(impostor);
const vertexPositions = vertexData.positions;
const vertexNormals = vertexData.normals;
if (vertexPositions === null || vertexNormals === null) {
return new this.bjsAMMO.btCompoundShape();
}
else {
const len = vertexPositions.length;
const segments = Math.sqrt(len / 3);
impostor.segments = segments;
const segs = segments - 1;
this._tmpAmmoVectorA.setValue(vertexPositions[0], vertexPositions[1], vertexPositions[2]);
this._tmpAmmoVectorB.setValue(vertexPositions[3 * segs], vertexPositions[3 * segs + 1], vertexPositions[3 * segs + 2]);
this._tmpAmmoVectorD.setValue(vertexPositions[len - 3], vertexPositions[len - 2], vertexPositions[len - 1]);
this._tmpAmmoVectorC.setValue(vertexPositions[len - 3 - 3 * segs], vertexPositions[len - 2 - 3 * segs], vertexPositions[len - 1 - 3 * segs]);
const clothBody = new this.bjsAMMO.btSoftBodyHelpers().CreatePatch(this.world.getWorldInfo(), this._tmpAmmoVectorA, this._tmpAmmoVectorB, this._tmpAmmoVectorC, this._tmpAmmoVectorD, segments, segments, impostor.getParam("fixedPoints"), true);
return clothBody;
}
}
}
/**
* Create rope for an impostor
* @param impostor to create the softbody for
* @returns the rope
*/
_createRope(impostor) {
let len;
let segments;
const vertexData = this._softVertexData(impostor);
const vertexPositions = vertexData.positions;
const vertexNormals = vertexData.normals;
if (vertexPositions === null || vertexNormals === null) {
return new this.bjsAMMO.btCompoundShape();
}
//force the mesh to be updatable
vertexData.applyToMesh(impostor.object, true);
impostor._isFromLine = true;
// If in lines mesh all normals will be zero
const vertexSquared = vertexNormals.map((x) => x * x);
const reducer = (accumulator, currentValue) => accumulator + currentValue;
const reduced = vertexSquared.reduce(reducer);
if (reduced === 0) {
// line mesh
len = vertexPositions.length;
segments = len / 3 - 1;
this._tmpAmmoVectorA.setValue(vertexPositions[0], vertexPositions[1], vertexPositions[2]);
this._tmpAmmoVectorB.setValue(vertexPositions[len - 3], vertexPositions[len - 2], vertexPositions[len - 1]);
}
else {
//extruded mesh
impostor._isFromLine = false;
const pathVectors = impostor.getParam("path");
const shape = impostor.getParam("shape");
if (shape === null) {
Logger.Warn("No shape available for extruded mesh");
return new this.bjsAMMO.btCompoundShape();
}
len = pathVectors.length;
segments = len - 1;
this._tmpAmmoVectorA.setValue(pathVectors[0].x, pathVectors[0].y, pathVectors[0].z);
this._tmpAmmoVectorB.setValue(pathVectors[len - 1].x, pathVectors[len - 1].y, pathVectors[len - 1].z);
}
impostor.segments = segments;
let fixedPoints = impostor.getParam("fixedPoints");
fixedPoints = fixedPoints > 3 ? 3 : fixedPoints;
const ropeBody = new this.bjsAMMO.btSoftBodyHelpers().CreateRope(this.world.getWorldInfo(), this._tmpAmmoVectorA, this._tmpAmmoVectorB, segments - 1, fixedPoints);
ropeBody.get_m_cfg().set_collisions(0x11);
return ropeBody;
}
/**
* Create a custom physics impostor shape using the plugin's onCreateCustomShape handler
* @param impostor to create the custom physics shape for
* @returns the custom physics shape
*/
_createCustom(impostor) {
let returnValue = null;
if (this.onCreateCustomShape) {
returnValue = this.onCreateCustomShape(impostor);
}
if (returnValue == null) {
returnValue = new this.bjsAMMO.btCompoundShape();
}
return returnValue;
}
// adds all verticies (including child verticies) to the convex hull shape
_addHullVerts(btConvexHullShape, topLevelObject, object) {
let triangleCount = 0;
if (object && object.getIndices && object.getWorldMatrix && object.getChildMeshes) {
let indices = object.getIndices();
if (!indices) {
indices = [];
}
let vertexPositions = object.getVerticesData(VertexBuffer.PositionKind);
if (!vertexPositions) {
vertexPositions = [];
}
object.computeWorldMatrix(false);
const faceCount = indices.length / 3;
for (let i = 0; i < faceCount; i++) {
const triPoints = [];
for (let point = 0; point < 3; point++) {
let v = new Vector3(vertexPositions[indices[i * 3 + point] * 3 + 0], vertexPositions[indices[i * 3 + point] * 3 + 1], vertexPositions[indices[i * 3 + point] * 3 + 2]);
// Adjust for initial scaling
Matrix.ScalingToRef(object.scaling.x, object.scaling.y, object.scaling.z, this._tmpMatrix);
v = Vector3.TransformCoordinates(v, this._tmpMatrix);
let vec;
if (point == 0) {
vec = this._tmpAmmoVectorA;
}
else if (point == 1) {
vec = this._tmpAmmoVectorB;
}
else {
vec = this._tmpAmmoVectorC;
}
vec.setValue(v.x, v.y, v.z);
triPoints.push(vec);
}
btConvexHullShape.addPoint(triPoints[0], true);
btConvexHullShape.addPoint(triPoints[1], true);
btConvexHullShape.addPoint(triPoints[2], true);
triangleCount++;
}
const childMeshes = object.getChildMeshes();
for (const m of childMeshes) {
triangleCount += this._addHullVerts(btConvexHullShape, topLevelObject, m);
}
}
return triangleCount;
}
_createShape(impostor, ignoreChildren = false) {
const object = impostor.object;
let returnValue;
const impostorExtents = impostor.getObjectExtents();
if (!ignoreChildren) {
const meshChildren = impostor.object.getChildMeshes ? impostor.object.getChildMeshes(true) : [];
returnValue = new this.bjsAMMO.btCompoundShape();
// Add shape of all children to the compound shape
let childrenAdded = 0;
for (const childMesh of meshChildren) {
const childImpostor = childMesh.getPhysicsImpostor();
if (childImpostor) {
if (childImpostor.type == PhysicsImpostor.MeshImpostor) {
// eslint-disable-next-line no-throw-literal
throw "A child MeshImpostor is not supported. Only primitive impostors are supported as children (eg. box or sphere)";
}
const shape = this._createShape(childImpostor);
// Position needs to be scaled based on parent's scaling
const parentMat = childMesh.parent.getWorldMatrix().clone();
const s = new Vector3();
parentMat.decompose(s);
this._tmpAmmoTransform.getOrigin().setValue(childMesh.position.x * s.x, childMesh.position.y * s.y, childMesh.position.z * s.z);
this._tmpAmmoQuaternion.setValue(childMesh.rotationQuaternion.x, childMesh.rotationQuaternion.y, childMesh.rotationQuaternion.z, childMesh.rotationQuaternion.w);
this._tmpAmmoTransform.setRotation(this._tmpAmmoQuaternion);
returnValue.addChildShape(this._tmpAmmoTransform, shape);
childImpostor.dispose();
childrenAdded++;
}
}
if (childrenAdded > 0) {
// Add parents shape as a child if present
if (impostor.type != PhysicsImpostor.NoImpostor) {
const shape = this._createShape(impostor, true);
if (shape) {
this._tmpAmmoTransform.getOrigin().setValue(0, 0, 0);
this._tmpAmmoQuaternion.setValue(0, 0, 0, 1);
this._tmpAmmoTransform.setRotation(this._tmpAmmoQuaternion);
returnValue.addChildShape(this._tmpAmmoTransform, shape);
}
}
return returnValue;
}
else {
// If no children with impostors create the actual shape below instead
this.bjsAMMO.destroy(returnValue);
returnValue = null;
}
}
switch (impostor.type) {
case PhysicsImpostor.SphereImpostor:
// Is there a better way to compare floats number? With an epsilon or with a Math function
if (WithinEpsilon(impostorExtents.x, impostorExtents.y, 0.0001) && WithinEpsilon(impostorExtents.x, impostorExtents.z, 0.0001)) {
returnValue = new this.bjsAMMO.btSphereShape(impostorExtents.x / 2);
}
else {
// create a btMultiSphereShape because it's not possible to set a local scaling on a btSphereShape
this._tmpAmmoVectorA.setValue(0, 0, 0);
const positions = [this._tmpAmmoVectorA];
const radii = [1];
returnValue = new this.bjsAMMO.btMultiSphereShape(positions, radii, 1);
this._tmpAmmoVectorA.setValue(impostorExtents.x / 2, impostorExtents.y / 2, impostorExtents.z / 2);
returnValue.setLocalScaling(this._tmpAmmoVectorA);
}
break;
case PhysicsImpostor.CapsuleImpostor:
{
// https://pybullet.org/Bullet/BulletFull/classbtCapsuleShape.html#details
// Height is just the height between the center of each 'sphere' of the capsule caps
const capRadius = impostorExtents.x / 2;
returnValue = new this.bjsAMMO.btCapsuleShape(capRadius, impostorExtents.y - capRadius * 2);
}
break;
case PhysicsImpostor.CylinderImpostor:
this._tmpAmmoVectorA.setValue(impostorExtents.x / 2, impostorExtents.y / 2, impostorExtents.z / 2);
returnValue = new this.bjsAMMO.btCylinderShape(this._tmpAmmoVectorA);
break;
case PhysicsImpostor.PlaneImpostor:
case PhysicsImpostor.BoxImpostor:
this._tmpAmmoVectorA.setValue(impostorExtents.x / 2, impostorExtents.y / 2, impostorExtents.z / 2);
returnValue = new this.bjsAMMO.btBoxShape(this._tmpAmmoVectorA);
break;
case PhysicsImpostor.MeshImpostor: {
if (impostor.getParam("mass") == 0) {
// Only create btBvhTriangleMeshShape if the impostor is static
// See https://pybullet.org/Bullet/phpBB3/viewtopic.php?t=7283
if (this.onCreateCustomMeshImpostor) {
returnValue = this.onCreateCustomMeshImpostor(impostor);
}
else {
const triMesh = new this.bjsAMMO.btTriangleMesh();
impostor._pluginData.toDispose.push(triMesh);
const triangleCount = this._addMeshVerts(triMesh, object, object);
if (triangleCount == 0) {
returnValue = new this.bjsAMMO.btCompoundShape();
}
else {
returnValue = new this.bjsAMMO.btBvhTriangleMeshShape(triMesh);
}
}
break;
}
}
// Otherwise create convexHullImpostor
// eslint-disable-next-line no-fallthrough
case PhysicsImpostor.ConvexHullImpostor: {
if (this.onCreateCustomConvexHullImpostor) {
returnValue = this.onCreateCustomConvexHullImpostor(impostor);
}
else {
const convexHull = new this.bjsAMMO.btConvexHullShape();
const triangleCount = this._addHullVerts(convexHull, object, object);
if (triangleCount == 0) {
// Cleanup Unused Convex Hull Shape
impostor._pluginData.toDispose.push(convexHull);
returnValue = new this.bjsAMMO.btCompoundShape();
}
else {
returnValue = convexHull;
}
}
break;
}
case PhysicsImpostor.NoImpostor:
// Fill with sphere but collision is disabled on the rigid body in generatePhysicsBody, using an empty shape caused unexpected movement with joints
returnValue = new this.bjsAMMO.btSphereShape(impostorExtents.x / 2);
break;
case PhysicsImpostor.CustomImpostor:
// Only usable when the plugin's onCreateCustomShape is set
returnValue = this._createCustom(impostor);
break;
case PhysicsImpostor.SoftbodyImpostor:
// Only usable with a mesh that has sufficient and shared vertices
returnValue = this._createSoftbody(impostor);
break;
case PhysicsImpostor.ClothImpostor:
// Only usable with a ground mesh that has sufficient and shared vertices
returnValue = this._createCloth(impostor);
break;
case PhysicsImpostor.RopeImpostor:
// Only usable with a line mesh or an extruded mesh that is updatable
returnValue = this._createRope(impostor);
break;
default:
Logger.Warn("The impostor type is not currently supported by the ammo plugin.");
break;
}
return returnValue;
}
/**
* Sets the mesh body position/rotation from the babylon impostor
* @param impostor imposter containing the physics body and babylon object
*/
setTransformationFromPhysicsBody(impostor) {
impostor.physicsBody.getMotionState().getWorldTransform(this._tmpAmmoTransform);
impostor.object.position.set(this._tmpAmmoTransform.getOrigin().x(), this._tmpAmmoTransform.getOri