UNPKG

planck-js

Version:

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

502 lines (409 loc) 14.7 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 { stats } from "../util/stats"; import Timer from "../util/Timer"; import { Sweep } from "../common/Sweep"; import { Transform } from "../common/Transform"; import { Distance, DistanceInput, DistanceOutput, DistanceProxy, SimplexCache } from "./Distance"; /** @internal */ const _ASSERT = typeof ASSERT === "undefined" ? false : ASSERT; /** @internal */ const math_abs = Math.abs; /** @internal */ const math_max = Math.max; /** * Input parameters for TimeOfImpact. */ export class TOIInput { proxyA = new DistanceProxy(); proxyB = new DistanceProxy(); sweepA = new Sweep(); sweepB = new Sweep(); /** defines sweep interval [0, tMax] */ tMax: number; recycle() { this.proxyA.recycle(); this.proxyB.recycle(); this.sweepA.recycle(); this.sweepB.recycle(); this.tMax = -1; } } export enum TOIOutputState { e_unset = -1, e_unknown = 0, e_failed = 1, e_overlapped = 2, e_touching = 3, e_separated = 4, } /** * Output parameters for TimeOfImpact. */ export class TOIOutput { state = TOIOutputState.e_unset; t = -1; recycle() { this.state = TOIOutputState.e_unset; this.t = -1; } } stats.toiTime = 0; stats.toiMaxTime = 0; stats.toiCalls = 0; stats.toiIters = 0; stats.toiMaxIters = 0; stats.toiRootIters = 0; stats.toiMaxRootIters = 0; /** @internal */ const distanceInput = new DistanceInput(); /** @internal */ const distanceOutput = new DistanceOutput(); // this is passed to Distance and SeparationFunction /** @internal */ const cache = new SimplexCache(); /** @internal */ const xfA = matrix.transform(0, 0, 0); /** @internal */ const xfB = matrix.transform(0, 0, 0); /** @internal */ const temp = matrix.vec2(0, 0); /** @internal */ const pointA = matrix.vec2(0, 0); /** @internal */ const pointB = matrix.vec2(0, 0); /** @internal */ const normal = matrix.vec2(0, 0); /** @internal */ const axisA = matrix.vec2(0, 0); /** @internal */ const axisB = matrix.vec2(0, 0); /** @internal */ const localPointA = matrix.vec2(0, 0); /** @internal */ const localPointB = matrix.vec2(0, 0); /** * Compute the upper bound on time before two shapes penetrate. Time is * represented as a fraction between [0,tMax]. This uses a swept separating axis * and may miss some intermediate, non-tunneling collisions. If you change the * time interval, you should call this function again. * * Note: use Distance to compute the contact point and normal at the time of * impact. * * CCD via the local separating axis method. This seeks progression by computing * the largest time at which separation is maintained. */ export const TimeOfImpact = function (output: TOIOutput, input: TOIInput): void { const timer = Timer.now(); ++stats.toiCalls; output.state = TOIOutputState.e_unknown; output.t = input.tMax; const proxyA = input.proxyA; // DistanceProxy const proxyB = input.proxyB; // DistanceProxy const sweepA = input.sweepA; // Sweep const sweepB = input.sweepB; // Sweep // Large rotations can make the root finder fail, so we normalize the // sweep angles. sweepA.normalize(); sweepB.normalize(); const tMax = input.tMax; const totalRadius = proxyA.m_radius + proxyB.m_radius; const target = math_max(Settings.linearSlop, totalRadius - 3.0 * Settings.linearSlop); const tolerance = 0.25 * Settings.linearSlop; if (_ASSERT) console.assert(target > tolerance); let t1 = 0.0; const k_maxIterations = Settings.maxTOIIterations; let iter = 0; // Prepare input for distance query. // const cache = new SimplexCache(); cache.recycle(); distanceInput.proxyA.setVertices(proxyA.m_vertices, proxyA.m_count, proxyA.m_radius); distanceInput.proxyB.setVertices(proxyB.m_vertices, proxyB.m_count, proxyB.m_radius); distanceInput.useRadii = false; // The outer loop progressively attempts to compute new separating axes. // This loop terminates when an axis is repeated (no progress is made). while (true) { sweepA.getTransform(xfA, t1); sweepB.getTransform(xfB, t1); // Get the distance between shapes. We can also use the results // to get a separating axis. matrix.copyTransform(distanceInput.transformA, xfA); matrix.copyTransform(distanceInput.transformB, xfB); Distance(distanceOutput, cache, distanceInput); // If the shapes are overlapped, we give up on continuous collision. if (distanceOutput.distance <= 0.0) { // Failure! output.state = TOIOutputState.e_overlapped; output.t = 0.0; break; } if (distanceOutput.distance < target + tolerance) { // Victory! output.state = TOIOutputState.e_touching; output.t = t1; break; } // Initialize the separating axis. separationFunction.initialize(cache, proxyA, sweepA, proxyB, sweepB, t1); // if (false) { // // Dump the curve seen by the root finder // const N = 100; // const dx = 1.0 / N; // const xs = []; // [ N + 1 ]; // const fs = []; // [ N + 1 ]; // const x = 0.0; // for (const i = 0; i <= N; ++i) { // sweepA.getTransform(xfA, x); // sweepB.getTransform(xfB, x); // const f = fcn.evaluate(xfA, xfB) - target; // printf("%g %g\n", x, f); // xs[i] = x; // fs[i] = f; // x += dx; // } // } // Compute the TOI on the separating axis. We do this by successively // resolving the deepest point. This loop is bounded by the number of // vertices. let done = false; let t2 = tMax; let pushBackIter = 0; while (true) { // Find the deepest point at t2. Store the witness point indices. let s2 = separationFunction.findMinSeparation(t2); // Is the final configuration separated? if (s2 > target + tolerance) { // Victory! output.state = TOIOutputState.e_separated; output.t = tMax; done = true; break; } // Has the separation reached tolerance? if (s2 > target - tolerance) { // Advance the sweeps t1 = t2; break; } // Compute the initial separation of the witness points. let s1 = separationFunction.evaluate(t1); // Check for initial overlap. This might happen if the root finder // runs out of iterations. if (s1 < target - tolerance) { output.state = TOIOutputState.e_failed; output.t = t1; done = true; break; } // Check for touching if (s1 <= target + tolerance) { // Victory! t1 should hold the TOI (could be 0.0). output.state = TOIOutputState.e_touching; output.t = t1; done = true; break; } // Compute 1D root of: f(x) - target = 0 let rootIterCount = 0; let a1 = t1; let a2 = t2; while (true) { // Use a mix of the secant rule and bisection. let t; if (rootIterCount & 1) { // Secant rule to improve convergence. t = a1 + (target - s1) * (a2 - a1) / (s2 - s1); } else { // Bisection to guarantee progress. t = 0.5 * (a1 + a2); } ++rootIterCount; ++stats.toiRootIters; const s = separationFunction.evaluate(t); if (math_abs(s - target) < tolerance) { // t2 holds a tentative value for t1 t2 = t; break; } // Ensure we continue to bracket the root. if (s > target) { a1 = t; s1 = s; } else { a2 = t; s2 = s; } if (rootIterCount === 50) { break; } } stats.toiMaxRootIters = math_max(stats.toiMaxRootIters, rootIterCount); ++pushBackIter; if (pushBackIter === Settings.maxPolygonVertices) { break; } } ++iter; ++stats.toiIters; if (done) { break; } if (iter === k_maxIterations) { // Root finder got stuck. Semi-victory. output.state = TOIOutputState.e_failed; output.t = t1; break; } } stats.toiMaxIters = math_max(stats.toiMaxIters, iter); const time = Timer.diff(timer); stats.toiMaxTime = math_max(stats.toiMaxTime, time); stats.toiTime += time; separationFunction.recycle(); }; enum SeparationFunctionType { e_unset = -1, e_points = 1, e_faceA = 2, e_faceB = 3, } class SeparationFunction { // input cache // todo: maybe assign by copy instead of reference? m_proxyA: DistanceProxy = null; m_proxyB: DistanceProxy = null; m_sweepA: Sweep = null; m_sweepB: Sweep = null; // initialize cache m_type = SeparationFunctionType.e_unset; m_localPoint = matrix.vec2(0, 0); m_axis = matrix.vec2(0, 0); // compute output indexA = -1; indexB = -1; recycle() { this.m_proxyA = null; this.m_proxyB = null; this.m_sweepA = null; this.m_sweepB = null; this.m_type = SeparationFunctionType.e_unset; matrix.zeroVec2(this.m_localPoint); matrix.zeroVec2(this.m_axis); this.indexA = -1; this.indexB = -1; } // TODO_ERIN might not need to return the separation initialize(cache: SimplexCache, proxyA: DistanceProxy, sweepA: Sweep, proxyB: DistanceProxy, sweepB: Sweep, t1: number): number { const count = cache.count; if (_ASSERT) console.assert(0 < count && count < 3); this.m_proxyA = proxyA; this.m_proxyB = proxyB; this.m_sweepA = sweepA; this.m_sweepB = sweepB; this.m_sweepA.getTransform(xfA, t1); this.m_sweepB.getTransform(xfB, t1); if (count === 1) { this.m_type = SeparationFunctionType.e_points; const localPointA = this.m_proxyA.getVertex(cache.indexA[0]); const localPointB = this.m_proxyB.getVertex(cache.indexB[0]); matrix.transformVec2(pointA, xfA, localPointA); matrix.transformVec2(pointB, xfB, localPointB); matrix.subVec2(this.m_axis, pointB, pointA); const s = matrix.normalizeVec2Length(this.m_axis); return s; } else if (cache.indexA[0] === cache.indexA[1]) { // Two points on B and one on A. this.m_type = SeparationFunctionType.e_faceB; const localPointB1 = proxyB.getVertex(cache.indexB[0]); const localPointB2 = proxyB.getVertex(cache.indexB[1]); matrix.crossVec2Num(this.m_axis, matrix.subVec2(temp, localPointB2, localPointB1), 1.0); matrix.normalizeVec2(this.m_axis); matrix.rotVec2(normal, xfB.q, this.m_axis); matrix.combine2Vec2(this.m_localPoint, 0.5, localPointB1, 0.5, localPointB2); matrix.transformVec2(pointB, xfB, this.m_localPoint); const localPointA = proxyA.getVertex(cache.indexA[0]); const pointA = Transform.mulVec2(xfA, localPointA); let s = matrix.dotVec2(pointA, normal) - matrix.dotVec2(pointB, normal); if (s < 0.0) { matrix.negVec2(this.m_axis); s = -s; } return s; } else { // Two points on A and one or two points on B. this.m_type = SeparationFunctionType.e_faceA; const localPointA1 = this.m_proxyA.getVertex(cache.indexA[0]); const localPointA2 = this.m_proxyA.getVertex(cache.indexA[1]); matrix.crossVec2Num(this.m_axis, matrix.subVec2(temp, localPointA2, localPointA1), 1.0); matrix.normalizeVec2(this.m_axis); matrix.rotVec2(normal, xfA.q, this.m_axis); matrix.combine2Vec2(this.m_localPoint, 0.5, localPointA1, 0.5, localPointA2); matrix.transformVec2(pointA, xfA, this.m_localPoint); const localPointB = this.m_proxyB.getVertex(cache.indexB[0]); matrix.transformVec2(pointB, xfB, localPointB); let s = matrix.dotVec2(pointB, normal) - matrix.dotVec2(pointA, normal); if (s < 0.0) { matrix.negVec2(this.m_axis); s = -s; } return s; } } compute(find: boolean, t: number): number { // It was findMinSeparation and evaluate this.m_sweepA.getTransform(xfA, t); this.m_sweepB.getTransform(xfB, t); switch (this.m_type) { case SeparationFunctionType.e_points: { if (find) { matrix.derotVec2(axisA, xfA.q, this.m_axis); matrix.derotVec2(axisB, xfB.q, matrix.scaleVec2(temp, -1, this.m_axis)); this.indexA = this.m_proxyA.getSupport(axisA); this.indexB = this.m_proxyB.getSupport(axisB); } matrix.copyVec2(localPointA, this.m_proxyA.getVertex(this.indexA)); matrix.copyVec2(localPointB, this.m_proxyB.getVertex(this.indexB)); matrix.transformVec2(pointA, xfA, localPointA); matrix.transformVec2(pointB, xfB, localPointB); const sep = matrix.dotVec2(pointB, this.m_axis) - matrix.dotVec2(pointA, this.m_axis); return sep; } case SeparationFunctionType.e_faceA: { matrix.rotVec2(normal, xfA.q, this.m_axis); matrix.transformVec2(pointA, xfA, this.m_localPoint); if (find) { matrix.derotVec2(axisB, xfB.q, matrix.scaleVec2(temp, -1, normal)); this.indexA = -1; this.indexB = this.m_proxyB.getSupport(axisB); } matrix.copyVec2(localPointB, this.m_proxyB.getVertex(this.indexB)); matrix.transformVec2(pointB, xfB, localPointB); const sep = matrix.dotVec2(pointB, normal) - matrix.dotVec2(pointA, normal); return sep; } case SeparationFunctionType.e_faceB: { matrix.rotVec2(normal, xfB.q, this.m_axis); matrix.transformVec2(pointB, xfB, this.m_localPoint); if (find) { matrix.derotVec2(axisA, xfA.q, matrix.scaleVec2(temp, -1, normal)); this.indexB = -1; this.indexA = this.m_proxyA.getSupport(axisA); } matrix.copyVec2(localPointA, this.m_proxyA.getVertex(this.indexA)); matrix.transformVec2(pointA, xfA, localPointA); const sep = matrix.dotVec2(pointA, normal) - matrix.dotVec2(pointB, normal); return sep; } default: if (_ASSERT) console.assert(false); if (find) { this.indexA = -1; this.indexB = -1; } return 0.0; } } findMinSeparation(t: number): number { return this.compute(true, t); } evaluate(t: number): number { return this.compute(false, t); } } /** @internal */ const separationFunction = new SeparationFunction(); // legacy exports TimeOfImpact.Input = TOIInput; TimeOfImpact.Output = TOIOutput;