UNPKG

planck-js

Version:

2D JavaScript/TypeScript physics engine for cross-platform HTML5 game development

892 lines (734 loc) 26.4 kB
/* * Planck.js * * Copyright (c) Erin Catto, Ali Shakiba * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import * as matrix from "../common/Matrix"; import { SettingsInternal as Settings } from "../Settings"; import { EPSILON } from "../common/Math"; import { Body } from "./Body"; import type { Contact } from "./Contact"; import { Joint } from "./Joint"; import { TimeOfImpact, TOIInput, TOIOutput, TOIOutputState } from "../collision/TimeOfImpact"; import { Distance, DistanceInput, DistanceOutput, SimplexCache } from "../collision/Distance"; import { World } from "./World"; import { Sweep } from "../common/Sweep"; /** @internal */ const _ASSERT = typeof ASSERT === "undefined" ? false : ASSERT; /** @internal */ const math_abs = Math.abs; /** @internal */ const math_sqrt = Math.sqrt; /** @internal */ const math_min = Math.min; export class TimeStep { /** time step */ dt: number = 0; /** inverse time step (0 if dt == 0) */ inv_dt: number = 0; velocityIterations: number = 0; positionIterations: number = 0; warmStarting: boolean = false; blockSolve: boolean = true; /** timestep ratio for variable timestep */ inv_dt0: number = 0.0; /** dt * inv_dt0 */ dtRatio: number = 1; reset(dt: number): void { if (this.dt > 0.0) { this.inv_dt0 = this.inv_dt; } this.dt = dt; this.inv_dt = dt == 0 ? 0 : 1 / dt; this.dtRatio = dt * this.inv_dt0; } } // reuse /** @internal */ const s_subStep = new TimeStep(); /** @internal */ const c = matrix.vec2(0, 0); /** @internal */ const v = matrix.vec2(0, 0); /** @internal */ const translation = matrix.vec2(0, 0); /** @internal */ const input = new TOIInput(); /** @internal */ const output = new TOIOutput(); /** @internal */ const backup = new Sweep(); /** @internal */ const backup1 = new Sweep(); /** @internal */ const backup2 = new Sweep(); /** * Contact impulses for reporting. Impulses are used instead of forces because * sub-step forces may approach infinity for rigid body collisions. These match * up one-to-one with the contact points in Manifold. */ export class ContactImpulse { // TODO: merge with Contact class? private readonly contact: Contact; private readonly normals: number[]; private readonly tangents: number[]; constructor(contact: Contact) { this.contact = contact; this.normals = []; this.tangents = []; } recycle() { this.normals.length = 0; this.tangents.length = 0; } get normalImpulses(): number[] { const contact = this.contact; const normals = this.normals; normals.length = 0; for (let p = 0; p < contact.v_points.length; ++p) { normals.push(contact.v_points[p].normalImpulse); } return normals; } get tangentImpulses(): number[] { const contact = this.contact; const tangents = this.tangents; tangents.length = 0; for (let p = 0; p < contact.v_points.length; ++p) { tangents.push(contact.v_points[p].tangentImpulse); } return tangents; } } /** * Finds and solves islands. An island is a connected subset of the world. */ export class Solver { m_world: World; m_stack: Body[]; m_bodies: Body[]; m_contacts: Contact[]; m_joints: Joint[]; constructor(world: World) { this.m_world = world; this.m_stack = []; this.m_bodies = []; this.m_contacts = []; this.m_joints = []; } clear(): void { this.m_stack.length = 0; this.m_bodies.length = 0; this.m_contacts.length = 0; this.m_joints.length = 0; } addBody(body: Body): void { if (_ASSERT) console.assert(body instanceof Body, "Not a Body!", body); this.m_bodies.push(body); // why? // body.c_position.c.setZero(); // body.c_position.a = 0; // body.c_velocity.v.setZero(); // body.c_velocity.w = 0; } addContact(contact: Contact): void { // if (_ASSERT) console.assert(contact instanceof Contact, 'Not a Contact!', contact); this.m_contacts.push(contact); } addJoint(joint: Joint): void { if (_ASSERT) console.assert(joint instanceof Joint, "Not a Joint!", joint); this.m_joints.push(joint); } solveWorld(step: TimeStep): void { const world = this.m_world; // Clear all the island flags. for (let b = world.m_bodyList; b; b = b.m_next) { b.m_islandFlag = false; } for (let c = world.m_contactList; c; c = c.m_next) { c.m_islandFlag = false; } for (let j = world.m_jointList; j; j = j.m_next) { j.m_islandFlag = false; } // Build and simulate all awake islands. const stack = this.m_stack; let loop = -1; for (let seed = world.m_bodyList; seed; seed = seed.m_next) { loop++; if (seed.m_islandFlag) { continue; } if (seed.isAwake() == false || seed.isActive() == false) { continue; } // The seed can be dynamic or kinematic. if (seed.isStatic()) { continue; } // Reset island and stack. this.clear(); stack.push(seed); seed.m_islandFlag = true; // Perform a depth first search (DFS) on the constraint graph. while (stack.length > 0) { // Grab the next body off the stack and add it to the island. const b = stack.pop(); if (_ASSERT) console.assert(b.isActive() == true); this.addBody(b); // Make sure the body is awake (without resetting sleep timer). b.m_awakeFlag = true; // To keep islands as small as possible, we don't // propagate islands across static bodies. if (b.isStatic()) { continue; } // Search all contacts connected to this body. for (let ce = b.m_contactList; ce; ce = ce.next) { const contact = ce.contact; // Has this contact already been added to an island? if (contact.m_islandFlag) { continue; } // Is this contact solid and touching? if (contact.isEnabled() == false || contact.isTouching() == false) { continue; } // Skip sensors. const sensorA = contact.m_fixtureA.m_isSensor; const sensorB = contact.m_fixtureB.m_isSensor; if (sensorA || sensorB) { continue; } this.addContact(contact); contact.m_islandFlag = true; const other = ce.other; // Was the other body already added to this island? if (other.m_islandFlag) { continue; } // if (_ASSERT) console.assert(stack.length < world.m_bodyCount); stack.push(other); other.m_islandFlag = true; } // Search all joints connect to this body. for (let je = b.m_jointList; je; je = je.next) { if (je.joint.m_islandFlag == true) { continue; } const other = je.other; // Don't simulate joints connected to inactive bodies. if (other.isActive() == false) { continue; } this.addJoint(je.joint); je.joint.m_islandFlag = true; if (other.m_islandFlag) { continue; } // if (_ASSERT) console.assert(stack.length < world.m_bodyCount); stack.push(other); other.m_islandFlag = true; } } this.solveIsland(step); // Post solve cleanup. for (let i = 0; i < this.m_bodies.length; ++i) { // Allow static bodies to participate in other islands. // TODO: are they added at all? const b = this.m_bodies[i]; if (b.isStatic()) { b.m_islandFlag = false; } } } } solveIsland(step: TimeStep): void { // B2: Island Solve const world = this.m_world; const gravity = world.m_gravity; const allowSleep = world.m_allowSleep; const h = step.dt; // Integrate velocities and apply damping. Initialize the body state. for (let i = 0; i < this.m_bodies.length; ++i) { const body = this.m_bodies[i]; matrix.copyVec2(c, body.m_sweep.c); const a = body.m_sweep.a; matrix.copyVec2(v, body.m_linearVelocity); let w = body.m_angularVelocity; // Store positions for continuous collision. matrix.copyVec2(body.m_sweep.c0, body.m_sweep.c); body.m_sweep.a0 = body.m_sweep.a; if (body.isDynamic()) { // Integrate velocities. matrix.plusScaleVec2(v, h * body.m_gravityScale, gravity); matrix.plusScaleVec2(v, h * body.m_invMass, body.m_force); w += h * body.m_invI * body.m_torque; /** * <pre> * Apply damping. * ODE: dv/dt + c * v = 0 * Solution: v(t) = v0 * exp(-c * t) * Time step: v(t + dt) = v0 * exp(-c * (t + dt)) = v0 * exp(-c * t) * exp(-c * dt) = v * exp(-c * dt) * v2 = exp(-c * dt) * v1 * Pade approximation: * v2 = v1 * 1 / (1 + c * dt) * </pre> */ matrix.scaleVec2(v, 1.0 / (1.0 + h * body.m_linearDamping), v); w *= 1.0 / (1.0 + h * body.m_angularDamping); } matrix.copyVec2(body.c_position.c, c); body.c_position.a = a; matrix.copyVec2(body.c_velocity.v, v); body.c_velocity.w = w; } for (let i = 0; i < this.m_contacts.length; ++i) { const contact = this.m_contacts[i]; contact.initConstraint(step); } for (let i = 0; i < this.m_contacts.length; ++i) { const contact = this.m_contacts[i]; contact.initVelocityConstraint(step); } if (step.warmStarting) { // Warm start. for (let i = 0; i < this.m_contacts.length; ++i) { const contact = this.m_contacts[i]; contact.warmStartConstraint(step); } } for (let i = 0; i < this.m_joints.length; ++i) { const joint = this.m_joints[i]; joint.initVelocityConstraints(step); } // Solve velocity constraints for (let i = 0; i < step.velocityIterations; ++i) { for (let j = 0; j < this.m_joints.length; ++j) { const joint = this.m_joints[j]; joint.solveVelocityConstraints(step); } for (let j = 0; j < this.m_contacts.length; ++j) { const contact = this.m_contacts[j]; contact.solveVelocityConstraint(step); } } // Store impulses for warm starting for (let i = 0; i < this.m_contacts.length; ++i) { const contact = this.m_contacts[i]; contact.storeConstraintImpulses(step); } // Integrate positions for (let i = 0; i < this.m_bodies.length; ++i) { const body = this.m_bodies[i]; matrix.copyVec2(c, body.c_position.c); let a = body.c_position.a; matrix.copyVec2(v, body.c_velocity.v); let w = body.c_velocity.w; // Check for large velocities matrix.scaleVec2(translation, h, v); const translationLengthSqr = matrix.lengthSqrVec2(translation); if (translationLengthSqr > Settings.maxTranslationSquared) { const ratio = Settings.maxTranslation / math_sqrt(translationLengthSqr); matrix.mulVec2(v, ratio); } const rotation = h * w; if (rotation * rotation > Settings.maxRotationSquared) { const ratio = Settings.maxRotation / math_abs(rotation); w *= ratio; } // Integrate matrix.plusScaleVec2(c, h, v); a += h * w; matrix.copyVec2(body.c_position.c, c); body.c_position.a = a; matrix.copyVec2(body.c_velocity.v, v); body.c_velocity.w = w; } // Solve position constraints let positionSolved = false; for (let i = 0; i < step.positionIterations; ++i) { let minSeparation = 0.0; for (let j = 0; j < this.m_contacts.length; ++j) { const contact = this.m_contacts[j]; const separation = contact.solvePositionConstraint(step); minSeparation = math_min(minSeparation, separation); } // We can't expect minSpeparation >= -Settings.linearSlop because we don't // push the separation above -Settings.linearSlop. const contactsOkay = minSeparation >= -3.0 * Settings.linearSlop; let jointsOkay = true; for (let j = 0; j < this.m_joints.length; ++j) { const joint = this.m_joints[j]; const jointOkay = joint.solvePositionConstraints(step); jointsOkay = jointsOkay && jointOkay; } if (contactsOkay && jointsOkay) { // Exit early if the position errors are small. positionSolved = true; break; } } // Copy state buffers back to the bodies for (let i = 0; i < this.m_bodies.length; ++i) { const body = this.m_bodies[i]; matrix.copyVec2(body.m_sweep.c, body.c_position.c); body.m_sweep.a = body.c_position.a; matrix.copyVec2(body.m_linearVelocity, body.c_velocity.v); body.m_angularVelocity = body.c_velocity.w; body.synchronizeTransform(); } this.postSolveIsland(); if (allowSleep) { let minSleepTime = Infinity; const linTolSqr = Settings.linearSleepToleranceSqr; const angTolSqr = Settings.angularSleepToleranceSqr; for (let i = 0; i < this.m_bodies.length; ++i) { const body = this.m_bodies[i]; if (body.isStatic()) { continue; } if ((body.m_autoSleepFlag == false) || (body.m_angularVelocity * body.m_angularVelocity > angTolSqr) || (matrix.lengthSqrVec2(body.m_linearVelocity) > linTolSqr)) { body.m_sleepTime = 0.0; minSleepTime = 0.0; } else { body.m_sleepTime += h; minSleepTime = math_min(minSleepTime, body.m_sleepTime); } } if (minSleepTime >= Settings.timeToSleep && positionSolved) { for (let i = 0; i < this.m_bodies.length; ++i) { const body = this.m_bodies[i]; body.setAwake(false); } } } } /** * Find TOI contacts and solve them. */ solveWorldTOI(step: TimeStep): void { const world = this.m_world; if (world.m_stepComplete) { for (let b = world.m_bodyList; b; b = b.m_next) { b.m_islandFlag = false; b.m_sweep.alpha0 = 0.0; } for (let c = world.m_contactList; c; c = c.m_next) { // Invalidate TOI c.m_toiFlag = false; c.m_islandFlag = false; c.m_toiCount = 0; c.m_toi = 1.0; } } // Find TOI events and solve them. while (true) { // Find the first TOI. let minContact: Contact | null = null; let minAlpha = 1.0; for (let c = world.m_contactList; c; c = c.m_next) { // Is this contact disabled? if (c.isEnabled() == false) { continue; } // Prevent excessive sub-stepping. if (c.m_toiCount > Settings.maxSubSteps) { continue; } let alpha = 1.0; if (c.m_toiFlag) { // This contact has a valid cached TOI. alpha = c.m_toi; } else { const fA = c.getFixtureA(); const fB = c.getFixtureB(); // Is there a sensor? if (fA.isSensor() || fB.isSensor()) { continue; } const bA = fA.getBody(); const bB = fB.getBody(); if (_ASSERT) console.assert(bA.isDynamic() || bB.isDynamic()); const activeA = bA.isAwake() && !bA.isStatic(); const activeB = bB.isAwake() && !bB.isStatic(); // Is at least one body active (awake and dynamic or kinematic)? if (activeA == false && activeB == false) { continue; } const collideA = bA.isBullet() || !bA.isDynamic(); const collideB = bB.isBullet() || !bB.isDynamic(); // Are these two non-bullet dynamic bodies? if (collideA == false && collideB == false) { continue; } // Compute the TOI for this contact. // Put the sweeps onto the same time interval. let alpha0 = bA.m_sweep.alpha0; if (bA.m_sweep.alpha0 < bB.m_sweep.alpha0) { alpha0 = bB.m_sweep.alpha0; bA.m_sweep.advance(alpha0); } else if (bB.m_sweep.alpha0 < bA.m_sweep.alpha0) { alpha0 = bA.m_sweep.alpha0; bB.m_sweep.advance(alpha0); } if (_ASSERT) console.assert(alpha0 < 1.0); const indexA = c.getChildIndexA(); const indexB = c.getChildIndexB(); const sweepA = bA.m_sweep; const sweepB = bB.m_sweep; // Compute the time of impact in interval [0, minTOI] input.proxyA.set(fA.getShape(), indexA); input.proxyB.set(fB.getShape(), indexB); input.sweepA.set(bA.m_sweep); input.sweepB.set(bB.m_sweep); input.tMax = 1.0; TimeOfImpact(output, input); // Beta is the fraction of the remaining portion of the [time?]. const beta = output.t; if (output.state == TOIOutputState.e_touching) { alpha = math_min(alpha0 + (1.0 - alpha0) * beta, 1.0); } else { alpha = 1.0; } c.m_toi = alpha; c.m_toiFlag = true; } if (alpha < minAlpha) { // This is the minimum TOI found so far. minContact = c; minAlpha = alpha; } } if (minContact == null || 1.0 - 10.0 * EPSILON < minAlpha) { // No more TOI events. Done! world.m_stepComplete = true; break; } // Advance the bodies to the TOI. const fA = minContact.getFixtureA(); const fB = minContact.getFixtureB(); const bA = fA.getBody(); const bB = fB.getBody(); backup1.set(bA.m_sweep); backup2.set(bB.m_sweep); bA.advance(minAlpha); bB.advance(minAlpha); // The TOI contact likely has some new contact points. minContact.update(world); minContact.m_toiFlag = false; ++minContact.m_toiCount; // Is the contact solid? if (minContact.isEnabled() == false || minContact.isTouching() == false) { // Restore the sweeps. minContact.setEnabled(false); bA.m_sweep.set(backup1); bB.m_sweep.set(backup2); bA.synchronizeTransform(); bB.synchronizeTransform(); continue; } bA.setAwake(true); bB.setAwake(true); // Build the island this.clear(); this.addBody(bA); this.addBody(bB); this.addContact(minContact); bA.m_islandFlag = true; bB.m_islandFlag = true; minContact.m_islandFlag = true; // Get contacts on bodyA and bodyB. const bodies = [ bA, bB ]; for (let i = 0; i < bodies.length; ++i) { const body = bodies[i]; if (body.isDynamic()) { for (let ce = body.m_contactList; ce; ce = ce.next) { // if (this.m_bodyCount == this.m_bodyCapacity) { break; } // if (this.m_contactCount == this.m_contactCapacity) { break; } const contact = ce.contact; // Has this contact already been added to the island? if (contact.m_islandFlag) { continue; } // Only add if either is static, kinematic or bullet. const other = ce.other; if (other.isDynamic() && !body.isBullet() && !other.isBullet()) { continue; } // Skip sensors. const sensorA = contact.m_fixtureA.m_isSensor; const sensorB = contact.m_fixtureB.m_isSensor; if (sensorA || sensorB) { continue; } // Tentatively advance the body to the TOI. backup.set(other.m_sweep); if (other.m_islandFlag == false) { other.advance(minAlpha); } // Update the contact points contact.update(world); // Was the contact disabled by the user? // Are there contact points? if (contact.isEnabled() == false || contact.isTouching() == false) { other.m_sweep.set(backup); other.synchronizeTransform(); continue; } // Add the contact to the island contact.m_islandFlag = true; this.addContact(contact); // Has the other body already been added to the island? if (other.m_islandFlag) { continue; } // Add the other body to the island. other.m_islandFlag = true; if (!other.isStatic()) { other.setAwake(true); } this.addBody(other); } } } s_subStep.reset((1.0 - minAlpha) * step.dt); s_subStep.dtRatio = 1.0; s_subStep.positionIterations = 20; s_subStep.velocityIterations = step.velocityIterations; s_subStep.warmStarting = false; this.solveIslandTOI(s_subStep, bA, bB); // Reset island flags and synchronize broad-phase proxies. for (let i = 0; i < this.m_bodies.length; ++i) { const body = this.m_bodies[i]; body.m_islandFlag = false; if (!body.isDynamic()) { continue; } body.synchronizeFixtures(); // Invalidate all contact TOIs on this displaced body. for (let ce = body.m_contactList; ce; ce = ce.next) { ce.contact.m_toiFlag = false; ce.contact.m_islandFlag = false; } } // Commit fixture proxy movements to the broad-phase so that new contacts // are created. // Also, some contacts can be destroyed. world.findNewContacts(); if (world.m_subStepping) { world.m_stepComplete = false; break; } } } solveIslandTOI(subStep: TimeStep, toiA: Body, toiB: Body): void { // Initialize the body state. for (let i = 0; i < this.m_bodies.length; ++i) { const body = this.m_bodies[i]; matrix.copyVec2(body.c_position.c, body.m_sweep.c); body.c_position.a = body.m_sweep.a; matrix.copyVec2(body.c_velocity.v, body.m_linearVelocity); body.c_velocity.w = body.m_angularVelocity; } for (let i = 0; i < this.m_contacts.length; ++i) { const contact = this.m_contacts[i]; contact.initConstraint(subStep); } // Solve position constraints. for (let i = 0; i < subStep.positionIterations; ++i) { let minSeparation = 0.0; for (let j = 0; j < this.m_contacts.length; ++j) { const contact = this.m_contacts[j]; const separation = contact.solvePositionConstraintTOI(subStep, toiA, toiB); minSeparation = math_min(minSeparation, separation); } // We can't expect minSpeparation >= -Settings.linearSlop because we don't // push the separation above -Settings.linearSlop. const contactsOkay = minSeparation >= -1.5 * Settings.linearSlop; if (contactsOkay) { break; } } if (false) { // Is the new position really safe? for (let i = 0; i < this.m_contacts.length; ++i) { const c = this.m_contacts[i]; const fA = c.getFixtureA(); const fB = c.getFixtureB(); const bA = fA.getBody(); const bB = fB.getBody(); const indexA = c.getChildIndexA(); const indexB = c.getChildIndexB(); const input = new DistanceInput(); input.proxyA.set(fA.getShape(), indexA); input.proxyB.set(fB.getShape(), indexB); input.transformA.set(bA.getTransform()); input.transformB.set(bB.getTransform()); input.useRadii = false; const output = new DistanceOutput(); const cache = new SimplexCache(); Distance(output, cache, input); if (output.distance == 0 || cache.count == 3) { cache.count += 0; } } } // Leap of faith to new safe state. matrix.copyVec2(toiA.m_sweep.c0, toiA.c_position.c); toiA.m_sweep.a0 = toiA.c_position.a; matrix.copyVec2(toiB.m_sweep.c0, toiB.c_position.c); toiB.m_sweep.a0 = toiB.c_position.a; // No warm starting is needed for TOI events because warm // starting impulses were applied in the discrete solver. for (let i = 0; i < this.m_contacts.length; ++i) { const contact = this.m_contacts[i]; contact.initVelocityConstraint(subStep); } // Solve velocity constraints. for (let i = 0; i < subStep.velocityIterations; ++i) { for (let j = 0; j < this.m_contacts.length; ++j) { const contact = this.m_contacts[j]; contact.solveVelocityConstraint(subStep); } } // Don't store the TOI contact forces for warm starting // because they can be quite large. const h = subStep.dt; // Integrate positions for (let i = 0; i < this.m_bodies.length; ++i) { const body = this.m_bodies[i]; matrix.copyVec2(c, body.c_position.c); let a = body.c_position.a; matrix.copyVec2(v, body.c_velocity.v); let w = body.c_velocity.w; // Check for large velocities matrix.scaleVec2(translation, h, v); const translationLengthSqr = matrix.lengthSqrVec2(translation); if (translationLengthSqr > Settings.maxTranslationSquared) { const ratio = Settings.maxTranslation / math_sqrt(translationLengthSqr); matrix.mulVec2(v, ratio); } const rotation = h * w; if (rotation * rotation > Settings.maxRotationSquared) { const ratio = Settings.maxRotation / math_abs(rotation); w *= ratio; } // Integrate matrix.plusScaleVec2(c, h, v); a += h * w; matrix.copyVec2(body.c_position.c, c); body.c_position.a = a; matrix.copyVec2(body.c_velocity.v, v); body.c_velocity.w = w; // Sync bodies matrix.copyVec2(body.m_sweep.c, c); body.m_sweep.a = a; matrix.copyVec2(body.m_linearVelocity, v); body.m_angularVelocity = w; body.synchronizeTransform(); } this.postSolveIsland(); } /** @internal */ postSolveIsland(): void { for (let c = 0; c < this.m_contacts.length; ++c) { const contact = this.m_contacts[c]; this.m_world.postSolve(contact, contact.m_impulse); } } } // @ts-ignore Solver.TimeStep = TimeStep;