UNPKG

planck-js

Version:

2D physics engine for JavaScript/HTML5 game development

869 lines (711 loc) 24.9 kB
/* * Copyright (c) 2016 Ali Shakiba http://shakiba.me/planck.js * Copyright (c) 2006-2011 Erin Catto http://www.box2d.org * * This software is provided 'as-is', without any express or implied * warranty. In no event will the authors be held liable for any damages * arising from the use of this software. * Permission is granted to anyone to use this software for any purpose, * including commercial applications, and to alter it and redistribute it * freely, subject to the following restrictions: * 1. The origin of this software must not be misrepresented; you must not * claim that you wrote the original software. If you use this software * in a product, an acknowledgment in the product documentation would be * appreciated but is not required. * 2. Altered source versions must be plainly marked as such, and must not be * misrepresented as being the original software. * 3. This notice may not be removed or altered from any source distribution. */ module.exports = Solver; module.exports.TimeStep = TimeStep; var Settings = require('./Settings'); var Timer = require('./util/Timer'); var Vec2 = require('./common/Vec2'); var Math = require('./common/Math'); var Body = require('./Body'); var TimeOfImpact = require('./collision/TimeOfImpact'); var TOIInput = TimeOfImpact.Input; var TOIOutput = TimeOfImpact.Output; var Distance = require('./collision/Distance'); var DistanceInput = Distance.Input; var DistanceOutput = Distance.Output; var DistanceProxy = Distance.Proxy; var SimplexCache = Distance.Cache; /** * Profiling data. Times are in milliseconds. */ function Profile() { this.solveInit; this.solveVelocity; this.solvePosition; }; function TimeStep(dt) { this.dt = 0; // time step this.inv_dt = 0; // inverse time step (0 if dt == 0) this.velocityIterations = 0; this.positionIterations = 0; this.warmStarting = false; this.blockSolve = true; // timestep ratio for variable timestep // TODO: why? only used inwarm starting this.inv_dt0 = 0.0; this.dtRatio = 1; // dt * inv_dt0 } TimeStep.prototype.Reset = function(dt) { 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; } /** * Finds and solves islands. An island is a connected subset of the world. * * @param {World} world * @param {Profile} profile */ function Solver(world) { this.m_world = world; this.m_profile = new Profile(); this.m_stack = []; this.m_bodies = []; this.m_contacts = []; this.m_joints = []; } Solver.prototype.Clear = function() { this.m_bodies.length = 0; this.m_contacts.length = 0; this.m_joints.length = 0; } Solver.prototype.AddBody = function(body) { this.m_bodies.push(body); }; Solver.prototype.AddContact = function(contact) { this.m_contacts.push(contact); }; Solver.prototype.AddJoint = function(joint) { this.m_joints.push(joint); }; /** * @param {TimeStep} step */ Solver.prototype.SolveWorld = function(step) { var world = this.m_world; var profile = this.m_profile; profile.solveInit = 0; profile.solveVelocity = 0; profile.solvePosition = 0; // Clear all the island flags. for (var b = world.m_bodyList; b; b = b.m_next) { b.m_islandFlag = false; } for (var c = world.m_contactList; c; c = c.m_next) { c.m_islandFlag = false; } for (var j = world.m_jointList; j; j = j.m_next) { j.m_islandFlag = false; } // Build and simulate all awake islands. var stack = this.m_stack; for (var seed = world.m_bodyList; seed; seed = seed.m_next) { 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.length = 0; 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. var b = stack.pop(); Assert(b.IsActive() == true); this.AddBody(b); // Make sure the body is awake. b.SetAwake(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 (var ce = b.m_contactList; ce; ce = ce.next) { var 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. var sensorA = contact.m_fixtureA.m_isSensor; var sensorB = contact.m_fixtureB.m_isSensor; if (sensorA || sensorB) { continue; } this.AddContact(contact); contact.m_islandFlag = true; var other = ce.other; // Was the other body already added to this island? if (other.m_islandFlag) { continue; } // Assert(stack.length < world.m_bodyCount); stack.push(other); other.m_islandFlag = true; } // Search all joints connect to this body. for (var je = b.m_jointList; je; je = je.next) { if (je.joint.m_islandFlag == true) { continue; } var 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; } // Assert(stack.length < world.m_bodyCount); stack.push(other); other.m_islandFlag = true; } } this.SolveIsland(step); // Post solve cleanup. for (var i = 0; i < this.m_bodyCount; ++i) { // Allow static bodies to participate in other islands. // TODO: are they added at all? var b = this.m_bodies[i]; if (b.IsStatic()) { b.m_islandFlag = false; } } } } /** * @param {TimeStep} step */ Solver.prototype.SolveIsland = function(step) { var world = this.m_world; var profile = this.m_profile; var gravity = world.m_gravity; var allowSleep = world.m_allowSleep; var timer = Timer.now(); var h = step.dt; // Integrate velocities and apply damping. Initialize the body state. for (var i = 0; i < this.m_bodies.length; ++i) { var body = this.m_bodies[i]; var c = Vec2(body.m_sweep.c); var a = body.m_sweep.a; var v = Vec2(body.m_linearVelocity); var w = body.m_angularVelocity; // Store positions for continuous collision. body.m_sweep.c0.Set(body.m_sweep.c); body.m_sweep.a0 = body.m_sweep.a; if (body.IsDynamic()) { // Integrate velocities. v.WAdd(h * body.m_gravityScale, gravity); v.WAdd(h * body.m_invMass, body.m_force); w += h * body.m_invI * body.m_torque; // 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) v.Mul(1.0 / (1.0 + h * body.m_linearDamping)); w *= 1.0 / (1.0 + h * body.m_angularDamping); } body.c_position.c = c; body.c_position.a = a; body.c_velocity.v = v; body.c_velocity.w = w; } timer = Timer.now(); // constrain creation was here for (var i = 0; i < this.m_contacts.length; ++i) { var contact = this.m_contacts[i]; contact.v_K.SetZero(); contact.v_normalMass.SetZero(); } for (var i = 0; i < this.m_contacts.length; ++i) { this.m_contacts[i].InitVelocityConstraint(step); } if (step.warmStarting) { for (var i = 0; i < this.m_contacts.length; ++i) { this.m_contacts[i].WarmStartConstraint(step); } } for (var i = 0; i < this.m_joints.length; ++i) { this.m_joints[i].InitVelocityConstraints(step); } profile.solveInit += Timer.diff(timer); // Solve velocity constraints timer = Timer.now(); for (var i = 0; i < step.velocityIterations; ++i) { for (var j = 0; j < this.m_joints.length; ++j) { this.m_joints[j].SolveVelocityConstraints(step); } for (var j = 0; j < this.m_contacts.length; ++j) { this.m_contacts[j].SolveVelocityConstraint(step); } } // Store impulses for warm starting for (var i = 0; i < this.m_contacts.length; ++i) { this.m_contacts[i].StoreConstraintImpulses(step); } profile.solveVelocity += Timer.diff(timer); // Integrate positions for (var i = 0; i < this.m_bodies.length; ++i) { var body = this.m_bodies[i]; var c = body.c_position.c; var a = body.c_position.a; var v = body.c_velocity.v; var w = body.c_velocity.w; // Check for large velocities var translation = Vec2.Mul(h, v); if (translation.LengthSquared() > Settings.maxTranslationSquared) { var ratio = Settings.maxTranslation / translation.Length(); v.Mul(ratio); } var rotation = h * w; if (rotation * rotation > Settings.maxRotationSquared) { var ratio = Settings.maxRotation / Math.abs(rotation); w *= ratio; } // Integrate c.Add(Vec2.Mul(h, v)); a += h * w; body.c_position.c = c; body.c_position.a = a; body.c_velocity.v = v; body.c_velocity.w = w; } // Solve position constraints timer = Timer.now(); var positionSolved = false; for (var i = 0; i < step.positionIterations; ++i) { // Sequential solver. var minSeparation = 0.0; for (var j = 0; j < this.m_contacts.length; ++j) { var separation = this.m_contacts[j].SolvePositionConstraint(step); minSeparation = Math.min(minSeparation, separation); } // We can't expect minSpeparation >= -Settings.linearSlop because we don't // push the separation above -Settings.linearSlop. var contactsOkay = minSeparation >= -3.0 * Settings.linearSlop; var jointsOkay = true; for (var j = 0; j < this.m_joints.length; ++j) { var jointOkay = this.m_joints[j].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 (var i = 0; i < this.m_bodies.length; ++i) { var body = this.m_bodies[i]; body.m_sweep.c.Set(body.c_position.c); body.m_sweep.a = body.c_position.a; body.m_linearVelocity.Set(body.c_velocity.v); body.m_angularVelocity = body.c_velocity.w; body.SynchronizeTransform(); } profile.solvePosition += Timer.diff(timer); this.PostSolveIsland(); if (allowSleep) { var minSleepTime = Infinity; var linTolSqr = Settings.linearSleepToleranceSqr; var angTolSqr = Settings.angularSleepToleranceSqr; for (var i = 0; i < this.m_bodies.length; ++i) { var body = this.m_bodies[i]; if (body.IsStatic()) { continue; } if (body.m_autoSleepFlag == false || (body.m_angularVelocity * body.m_angularVelocity > angTolSqr) || (Vec2.LengthSquared(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 (var i = 0; i < this.m_bodies.length; ++i) { this.m_bodies[i].SetAwake(false); } } } }; var s_subStep = new TimeStep(); // reuse /** * Find TOI contacts and solve them. * * @param {TimeStep} step */ Solver.prototype.SolveWorldTOI = function(step) { var world = this.m_world; var profile = this.m_profile; if (this.m_stepComplete) { for (var b = world.m_bodyList; b; b = b.m_next) { b.m_islandFlag = false; b.m_sweep.alpha0 = 0.0; } for (var 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. for (;;) { // Find the first TOI. var minContact = null; // Contact var minAlpha = 1.0; for (var 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; } var alpha = 1.0; if (c.m_toiFlag) { // This contact has a valid cached TOI. alpha = c.m_toi; } else { var fA = c.GetFixtureA(); var fB = c.GetFixtureB(); // Is there a sensor? if (fA.IsSensor() || fB.IsSensor()) { continue; } var bA = fA.GetBody(); var bB = fB.GetBody(); Assert(bA.IsDynamic() || bB.IsDynamic()); var activeA = bA.IsAwake() && !bA.IsStatic(); var activeB = bB.IsAwake() && !bB.IsStatic(); // Is at least one body active (awake and dynamic or kinematic)? if (activeA == false && activeB == false) { continue; } var collideA = bA.IsBullet() || !bA.IsDynamic(); var 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. var 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); } Assert(alpha0 < 1.0); var indexA = c.GetChildIndexA(); var indexB = c.GetChildIndexB(); // Compute the time of impact in interval [0, minTOI] var input = new TOIInput(); // TODO: reuse 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; var output = new TOIOutput(); // TODO: reuse TimeOfImpact(output, input); // console.log(output.t, output.state); // Beta is the fraction of the remaining portion of the [time?]. var beta = output.t; if (output.state == TOIOutput.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 * Math.EPSILON < minAlpha) { // No more TOI events. Done! world.m_stepComplete = true; break; } // Advance the bodies to the TOI. var fA = minContact.GetFixtureA(); var fB = minContact.GetFixtureB(); var bA = fA.GetBody(); var bB = fB.GetBody(); var backup1 = bA.m_sweep.Clone(); var backup2 = bB.m_sweep.Clone(); 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. var bodies = [ bA, bB ]; for (var i = 0; i < bodies.length; ++i) { var body = bodies[i]; if (body.IsDynamic()) { for (var ce = body.m_contactList; ce; ce = ce.next) { // if (this.m_bodyCount == this.m_bodyCapacity) { break; } // if (this.m_contactCount == this.m_contactCapacity) { break; } var 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. var other = ce.other; if (other.IsDynamic() && !body.IsBullet() && !other.IsBullet()) { continue; } // Skip sensors. var sensorA = contact.m_fixtureA.m_isSensor; var sensorB = contact.m_fixtureB.m_isSensor; if (sensorA || sensorB) { continue; } // Tentatively advance the body to the TOI. var backup = other.m_sweep.Clone(); 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.AddContact(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 (var i = 0; i < island.m_bodyCount; ++i) { var body = island.m_bodies[i]; body.m_islandFlag = false; if (!body.IsDynamic()) { continue; } body.SynchronizeFixtures(); // Invalidate all contact TOIs on this displaced body. for (var 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; } } } /** * @param {TimeStep} subStep * @param toiA * @param toiB */ Solver.prototype.SolveIslandTOI = function(subStep, toiA, toiB) { var world = this.m_world; var profile = this.m_profile; // Initialize the body state. for (var i = 0; i < this.m_bodies.length; ++i) { var body = this.m_bodies[i]; body.c_position.c = body.m_sweep.c; body.c_position.a = body.m_sweep.a; body.c_velocity.v = body.m_linearVelocity; body.c_velocity.w = body.m_angularVelocity; } // constrain creation was here // Solve position constraints. for (var i = 0; i < subStep.positionIterations; ++i) { // Sequential position solver for position constraints. var minSeparation = 0.0; for (var i = 0; i < this.m_contacts.length; ++i) { var separation = this.m_contacts[i].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. var contactsOkay = minSeparation >= -1.5 * Settings.linearSlop; if (contactsOkay) { break; } } if (false) { // Is the new position really safe? for (var i = 0; i < this.m_contacts.length; ++i) { var c = this.m_contacts[i]; var fA = c.GetFixtureA(); var fB = c.GetFixtureB(); var bA = fA.GetBody(); var bB = fB.GetBody(); var indexA = c.GetChildIndexA(); var indexB = c.GetChildIndexB(); var input = new DistanceInput(); input.proxyA.Set(fA.GetShape(), indexA); input.proxyB.Set(fB.GetShape(), indexB); input.transformA = bA.GetTransform(); input.transformB = bB.GetTransform(); input.useRadii = false; var output = new DistanceOutput(); var cache = new SimplexCache(); Distance(output, cache, input); if (output.distance == 0 || cache.count == 3) { cache.count += 0; } } } // Leap of faith to new safe state. toiA.m_sweep.c0 = toiA.c_position.c; toiA.m_sweep.a0 = toiA.c_position.a; toiB.m_sweep.c0 = toiB.c_velocity.c; toiB.m_sweep.a0 = toiB.c_velocity.a; // No warm starting is needed for TOI events because warm // starting impulses were applied in the discrete solver. for (var i = 0; i < this.m_contacts.length; ++i) { this.m_contacts[i].InitVelocityConstraint(subStep); } // Solve velocity constraints. for (var i = 0; i < subStep.velocityIterations; ++i) { for (var i = 0; i < this.m_contacts.length; ++i) { this.m_contacts[i].SolveVelocityConstraint(subStep); } } // Don't store the TOI contact forces for warm starting // because they can be quite large. var h = subStep.dt; // Integrate positions for (var i = 0; i < this.m_bodies.length; ++i) { var body = this.m_bodies[i]; var c = body.c_position.c; var a = body.c_position.a; var v = body.c_velocity.v; var w = body.c_velocity.w; // Check for large velocities var translation = Vec2.Mul(h, v); translation.Clamp(Settings.maxTranslation); var rotation = Math.clamp(h * w, -Settings.maxRotation, Settings.maxRotation); // Integrate c.Add(translation); a += rotation; body.c_position.c.Set(c); body.c_position.a = a; body.c_velocity.v.Set(v); body.c_velocity.w = w; // Sync bodies body.m_sweep.c.Set(c); body.m_sweep.a = a; body.m_linearVelocity.Set(v); body.m_angularVelocity = w; body.SynchronizeTransform(); } this.PostSolveIsland(); }; /** * 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. */ function ContactImpulse() { this.normalImpulses = []; this.tangentImpulses = []; this.count = 0; }; Solver.prototype.PostSolveIsland = function() { return; // TODO: report contact.v_points instead of new object? var impulse = new ContactImpulse(); for (var c = 0; c < this.m_contacts.length; ++c) { var contact = this.m_contacts[c]; impulse.count = contact.v_point.length; for (var p = 0; p < contact.v_point.length; ++p) { impulse.normalImpulses[p] = contact.v_points[p].normalImpulse; impulse.tangentImpulses[p] = contact.v_points[p].tangentImpulse; } this.m_world.PostSolve(contact, impulse); } };