UNPKG

@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
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