UNPKG

planck-js

Version:

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

506 lines (439 loc) 16.5 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 { TransformValue } from "../../common/Transform"; import { Vec2, Vec2Value } from "../../common/Vec2"; import { SettingsInternal as Settings } from "../../Settings"; import { Contact } from "../../dynamics/Contact"; import { Manifold, clipSegmentToLine, ClipVertex, ContactFeatureType, ManifoldType } from "../Manifold"; import { EdgeShape } from "./EdgeShape"; import { ChainShape } from "./ChainShape"; import { PolygonShape } from "./PolygonShape"; import { Fixture } from "../../dynamics/Fixture"; /** @internal */ const _ASSERT = typeof ASSERT === "undefined" ? false : ASSERT; /** @internal */ const math_min = Math.min; Contact.addType(EdgeShape.TYPE, PolygonShape.TYPE, EdgePolygonContact); Contact.addType(ChainShape.TYPE, PolygonShape.TYPE, ChainPolygonContact); /** @internal */ function EdgePolygonContact(manifold: Manifold, xfA: TransformValue, fA: Fixture, indexA: number, xfB: TransformValue, fB: Fixture, indexB: number): void { if (_ASSERT) console.assert(fA.getType() == EdgeShape.TYPE); if (_ASSERT) console.assert(fB.getType() == PolygonShape.TYPE); CollideEdgePolygon(manifold, fA.getShape() as EdgeShape, xfA, fB.getShape() as PolygonShape, xfB); } // reused /** @internal */ const edge_reuse = new EdgeShape(); /** @internal */ function ChainPolygonContact(manifold: Manifold, xfA: TransformValue, fA: Fixture, indexA: number, xfB: TransformValue, fB: Fixture, indexB: number): void { if (_ASSERT) console.assert(fA.getType() == ChainShape.TYPE); if (_ASSERT) console.assert(fB.getType() == PolygonShape.TYPE); const chain = fA.getShape() as ChainShape; chain.getChildEdge(edge_reuse, indexA); CollideEdgePolygon(manifold, edge_reuse, xfA, fB.getShape() as PolygonShape, xfB); } /** @internal */ enum EPAxisType { e_unknown = -1, e_edgeA = 1, e_edgeB = 2, } // unused? /** @internal */ enum VertexType { e_isolated = 0, e_concave = 1, e_convex = 2, } /** * This structure is used to keep track of the best separating axis. */ /** @internal */ class EPAxis { type: EPAxisType; index: number; separation: number; } /** * This holds polygon B expressed in frame A. */ /** @internal */ class TempPolygon { vertices: Vec2Value[] = []; // [Settings.maxPolygonVertices] normals: Vec2Value[] = []; // [Settings.maxPolygonVertices]; count: number = 0; constructor() { for (let i = 0; i < Settings.maxPolygonVertices; i++) { this.vertices.push(matrix.vec2(0, 0)); this.normals.push(matrix.vec2(0, 0)); } } } /** * Reference face used for clipping */ /** @internal */ class ReferenceFace { i1: number; i2: number; readonly v1 = matrix.vec2(0 ,0); readonly v2 = matrix.vec2(0 ,0); readonly normal = matrix.vec2(0 ,0); readonly sideNormal1 = matrix.vec2(0 ,0); sideOffset1: number; readonly sideNormal2 = matrix.vec2(0 ,0); sideOffset2: number; recycle() { matrix.zeroVec2(this.v1); matrix.zeroVec2(this.v2); matrix.zeroVec2(this.normal); matrix.zeroVec2(this.sideNormal1); matrix.zeroVec2(this.sideNormal2); } } // reused /** @internal */ const clipPoints1 = [ new ClipVertex(), new ClipVertex() ]; /** @internal */ const clipPoints2 = [ new ClipVertex(), new ClipVertex() ]; /** @internal */ const ie = [ new ClipVertex(), new ClipVertex() ]; /** @internal */ const edgeAxis = new EPAxis(); /** @internal */ const polygonAxis = new EPAxis(); /** @internal */ const polygonBA = new TempPolygon(); /** @internal */ const rf = new ReferenceFace(); /** @internal */ const centroidB = matrix.vec2(0, 0); /** @internal */ const edge0 = matrix.vec2(0, 0); /** @internal */ const edge1 = matrix.vec2(0, 0); /** @internal */ const edge2 = matrix.vec2(0, 0); /** @internal */ const xf = matrix.transform(0, 0, 0); /** @internal */ const normal = matrix.vec2(0, 0); /** @internal */ const normal0 = matrix.vec2(0, 0); /** @internal */ const normal1 = matrix.vec2(0, 0); /** @internal */ const normal2 = matrix.vec2(0, 0); /** @internal */ const lowerLimit = matrix.vec2(0, 0); /** @internal */ const upperLimit = matrix.vec2(0, 0); /** @internal */ const perp = matrix.vec2(0, 0); /** @internal */ const n = matrix.vec2(0, 0); /** * This function collides and edge and a polygon, taking into account edge * adjacency. */ export const CollideEdgePolygon = function (manifold: Manifold, edgeA: EdgeShape, xfA: TransformValue, polygonB: PolygonShape, xfB: TransformValue): void { // Algorithm: // 1. Classify v1 and v2 // 2. Classify polygon centroid as front or back // 3. Flip normal if necessary // 4. Initialize normal range to [-pi, pi] about face normal // 5. Adjust normal range according to adjacent edges // 6. Visit each separating axes, only accept axes within the range // 7. Return if _any_ axis indicates separation // 8. Clip // let m_type1: VertexType; // let m_type2: VertexType; matrix.detransformTransform(xf, xfA, xfB); matrix.transformVec2(centroidB, xf, polygonB.m_centroid); const v0 = edgeA.m_vertex0; const v1 = edgeA.m_vertex1; const v2 = edgeA.m_vertex2; const v3 = edgeA.m_vertex3; const hasVertex0 = edgeA.m_hasVertex0; const hasVertex3 = edgeA.m_hasVertex3; matrix.subVec2(edge1, v2, v1); matrix.normalizeVec2(edge1); matrix.setVec2(normal1, edge1.y, -edge1.x); const offset1 = matrix.dotVec2(normal1, centroidB) - matrix.dotVec2(normal1, v1); let offset0 = 0.0; let offset2 = 0.0; let convex1 = false; let convex2 = false; matrix.zeroVec2(normal0); matrix.zeroVec2(normal2); // Is there a preceding edge? if (hasVertex0) { matrix.subVec2(edge0, v1, v0); matrix.normalizeVec2(edge0); matrix.setVec2(normal0, edge0.y, -edge0.x); convex1 = matrix.crossVec2Vec2(edge0, edge1) >= 0.0; offset0 = Vec2.dot(normal0, centroidB) - Vec2.dot(normal0, v0); } // Is there a following edge? if (hasVertex3) { matrix.subVec2(edge2, v3, v2); matrix.normalizeVec2(edge2); matrix.setVec2(normal2, edge2.y, -edge2.x); convex2 = Vec2.crossVec2Vec2(edge1, edge2) > 0.0; offset2 = Vec2.dot(normal2, centroidB) - Vec2.dot(normal2, v2); } let front: boolean; matrix.zeroVec2(normal); matrix.zeroVec2(lowerLimit); matrix.zeroVec2(upperLimit); // Determine front or back collision. Determine collision normal limits. if (hasVertex0 && hasVertex3) { if (convex1 && convex2) { front = offset0 >= 0.0 || offset1 >= 0.0 || offset2 >= 0.0; if (front) { matrix.copyVec2(normal, normal1); matrix.copyVec2(lowerLimit, normal0); matrix.copyVec2(upperLimit, normal2); } else { matrix.scaleVec2(normal, -1, normal1); matrix.scaleVec2(lowerLimit, -1, normal1); matrix.scaleVec2(upperLimit, -1, normal1); } } else if (convex1) { front = offset0 >= 0.0 || (offset1 >= 0.0 && offset2 >= 0.0); if (front) { matrix.copyVec2(normal, normal1); matrix.copyVec2(lowerLimit, normal0); matrix.copyVec2(upperLimit, normal1); } else { matrix.scaleVec2(normal, -1, normal1); matrix.scaleVec2(lowerLimit, -1, normal2); matrix.scaleVec2(upperLimit, -1, normal1); } } else if (convex2) { front = offset2 >= 0.0 || (offset0 >= 0.0 && offset1 >= 0.0); if (front) { matrix.copyVec2(normal, normal1); matrix.copyVec2(lowerLimit, normal1); matrix.copyVec2(upperLimit, normal2); } else { matrix.scaleVec2(normal, -1, normal1); matrix.scaleVec2(lowerLimit, -1, normal1); matrix.scaleVec2(upperLimit, -1, normal0); } } else { front = offset0 >= 0.0 && offset1 >= 0.0 && offset2 >= 0.0; if (front) { matrix.copyVec2(normal, normal1); matrix.copyVec2(lowerLimit, normal1); matrix.copyVec2(upperLimit, normal1); } else { matrix.scaleVec2(normal, -1, normal1); matrix.scaleVec2(lowerLimit, -1, normal2); matrix.scaleVec2(upperLimit, -1, normal0); } } } else if (hasVertex0) { if (convex1) { front = offset0 >= 0.0 || offset1 >= 0.0; if (front) { matrix.copyVec2(normal, normal1); matrix.copyVec2(lowerLimit, normal0); matrix.scaleVec2(upperLimit, -1, normal1); } else { matrix.scaleVec2(normal, -1, normal1); matrix.copyVec2(lowerLimit, normal1); matrix.scaleVec2(upperLimit, -1, normal1); } } else { front = offset0 >= 0.0 && offset1 >= 0.0; if (front) { matrix.copyVec2(normal, normal1); matrix.copyVec2(lowerLimit, normal1); matrix.scaleVec2(upperLimit, -1, normal1); } else { matrix.scaleVec2(normal, -1, normal1); matrix.copyVec2(lowerLimit, normal1); matrix.scaleVec2(upperLimit, -1, normal0); } } } else if (hasVertex3) { if (convex2) { front = offset1 >= 0.0 || offset2 >= 0.0; if (front) { matrix.copyVec2(normal, normal1); matrix.scaleVec2(lowerLimit, -1, normal1); matrix.copyVec2(upperLimit, normal2); } else { matrix.scaleVec2(normal, -1, normal1); matrix.scaleVec2(lowerLimit, -1, normal1); matrix.copyVec2(upperLimit, normal1); } } else { front = offset1 >= 0.0 && offset2 >= 0.0; if (front) { matrix.copyVec2(normal, normal1); matrix.scaleVec2(lowerLimit, -1, normal1); matrix.copyVec2(upperLimit, normal1); } else { matrix.scaleVec2(normal, -1, normal1); matrix.scaleVec2(lowerLimit, -1, normal2); matrix.copyVec2(upperLimit, normal1); } } } else { front = offset1 >= 0.0; if (front) { matrix.copyVec2(normal, normal1); matrix.scaleVec2(lowerLimit, -1, normal1); matrix.scaleVec2(upperLimit, -1, normal1); } else { matrix.scaleVec2(normal, -1, normal1); matrix.copyVec2(lowerLimit, normal1); matrix.copyVec2(upperLimit, normal1); } } // Get polygonB in frameA polygonBA.count = polygonB.m_count; for (let i = 0; i < polygonB.m_count; ++i) { matrix.transformVec2(polygonBA.vertices[i], xf, polygonB.m_vertices[i]); matrix.rotVec2(polygonBA.normals[i], xf.q, polygonB.m_normals[i]); } const radius = polygonB.m_radius + edgeA.m_radius; manifold.pointCount = 0; { // ComputeEdgeSeparation edgeAxis.type = EPAxisType.e_edgeA; edgeAxis.index = front ? 0 : 1; edgeAxis.separation = Infinity; for (let i = 0; i < polygonBA.count; ++i) { const v = polygonBA.vertices[i]; const s = matrix.dotVec2(normal, v) - matrix.dotVec2(normal, v1); if (s < edgeAxis.separation) { edgeAxis.separation = s; } } } // If no valid normal can be found than this edge should not collide. // @ts-ignore todo: why we need this if here? if (edgeAxis.type == EPAxisType.e_unknown) { return; } if (edgeAxis.separation > radius) { return; } { // ComputePolygonSeparation polygonAxis.type = EPAxisType.e_unknown; polygonAxis.index = -1; polygonAxis.separation = -Infinity; matrix.setVec2(perp, -normal.y, normal.x); for (let i = 0; i < polygonBA.count; ++i) { matrix.scaleVec2(n, -1, polygonBA.normals[i]); const s1 = matrix.dotVec2(n, polygonBA.vertices[i]) - matrix.dotVec2(n, v1); const s2 = matrix.dotVec2(n, polygonBA.vertices[i]) - matrix.dotVec2(n, v2); const s = math_min(s1, s2); if (s > radius) { // No collision polygonAxis.type = EPAxisType.e_edgeB; polygonAxis.index = i; polygonAxis.separation = s; break; } // Adjacency if (matrix.dotVec2(n, perp) >= 0.0) { if (matrix.dotVec2(n, normal) - matrix.dotVec2(upperLimit, normal) < -Settings.angularSlop) { continue; } } else { if (matrix.dotVec2(n, normal) - matrix.dotVec2(lowerLimit, normal) < -Settings.angularSlop) { continue; } } if (s > polygonAxis.separation) { polygonAxis.type = EPAxisType.e_edgeB; polygonAxis.index = i; polygonAxis.separation = s; } } } if (polygonAxis.type != EPAxisType.e_unknown && polygonAxis.separation > radius) { return; } // Use hysteresis for jitter reduction. const k_relativeTol = 0.98; const k_absoluteTol = 0.001; let primaryAxis: EPAxis; if (polygonAxis.type == EPAxisType.e_unknown) { primaryAxis = edgeAxis; } else if (polygonAxis.separation > k_relativeTol * edgeAxis.separation + k_absoluteTol) { primaryAxis = polygonAxis; } else { primaryAxis = edgeAxis; } ie[0].recycle(); ie[1].recycle(); if (primaryAxis.type == EPAxisType.e_edgeA) { manifold.type = ManifoldType.e_faceA; // Search for the polygon normal that is most anti-parallel to the edge // normal. let bestIndex = 0; let bestValue = matrix.dotVec2(normal, polygonBA.normals[0]); for (let i = 1; i < polygonBA.count; ++i) { const value = matrix.dotVec2(normal, polygonBA.normals[i]); if (value < bestValue) { bestValue = value; bestIndex = i; } } const i1 = bestIndex; const i2 = i1 + 1 < polygonBA.count ? i1 + 1 : 0; matrix.copyVec2(ie[0].v, polygonBA.vertices[i1]); ie[0].id.setFeatures(0, ContactFeatureType.e_face, i1, ContactFeatureType.e_vertex); matrix.copyVec2(ie[1].v, polygonBA.vertices[i2]); ie[1].id.setFeatures(0, ContactFeatureType.e_face, i2, ContactFeatureType.e_vertex); if (front) { rf.i1 = 0; rf.i2 = 1; matrix.copyVec2(rf.v1, v1); matrix.copyVec2(rf.v2, v2); matrix.copyVec2(rf.normal, normal1); } else { rf.i1 = 1; rf.i2 = 0; matrix.copyVec2(rf.v1, v2); matrix.copyVec2(rf.v2, v1); matrix.scaleVec2(rf.normal, -1, normal1); } } else { manifold.type = ManifoldType.e_faceB; matrix.copyVec2(ie[0].v, v1); ie[0].id.setFeatures(0, ContactFeatureType.e_vertex, primaryAxis.index, ContactFeatureType.e_face); matrix.copyVec2(ie[1].v, v2); ie[1].id.setFeatures(0, ContactFeatureType.e_vertex, primaryAxis.index, ContactFeatureType.e_face); rf.i1 = primaryAxis.index; rf.i2 = rf.i1 + 1 < polygonBA.count ? rf.i1 + 1 : 0; matrix.copyVec2(rf.v1, polygonBA.vertices[rf.i1]); matrix.copyVec2(rf.v2, polygonBA.vertices[rf.i2]); matrix.copyVec2(rf.normal, polygonBA.normals[rf.i1]); } matrix.setVec2(rf.sideNormal1, rf.normal.y, -rf.normal.x); matrix.setVec2(rf.sideNormal2, -rf.sideNormal1.x, -rf.sideNormal1.y); rf.sideOffset1 = matrix.dotVec2(rf.sideNormal1, rf.v1); rf.sideOffset2 = matrix.dotVec2(rf.sideNormal2, rf.v2); // Clip incident edge against extruded edge1 side edges. clipPoints1[0].recycle(); clipPoints1[1].recycle(); clipPoints2[0].recycle(); clipPoints2[1].recycle(); // Clip to box side 1 const np1 = clipSegmentToLine(clipPoints1, ie, rf.sideNormal1, rf.sideOffset1, rf.i1); if (np1 < Settings.maxManifoldPoints) { return; } // Clip to negative box side 1 const np2 = clipSegmentToLine(clipPoints2, clipPoints1, rf.sideNormal2, rf.sideOffset2, rf.i2); if (np2 < Settings.maxManifoldPoints) { return; } // Now clipPoints2 contains the clipped points. if (primaryAxis.type == EPAxisType.e_edgeA) { matrix.copyVec2(manifold.localNormal, rf.normal); matrix.copyVec2(manifold.localPoint, rf.v1); } else { matrix.copyVec2(manifold.localNormal, polygonB.m_normals[rf.i1]); matrix.copyVec2(manifold.localPoint, polygonB.m_vertices[rf.i1]); } let pointCount = 0; for (let i = 0; i < Settings.maxManifoldPoints; ++i) { const separation = matrix.dotVec2(rf.normal, clipPoints2[i].v) - matrix.dotVec2(rf.normal, rf.v1); if (separation <= radius) { const cp = manifold.points[pointCount]; // ManifoldPoint if (primaryAxis.type == EPAxisType.e_edgeA) { matrix.detransformVec2(cp.localPoint, xf, clipPoints2[i].v); cp.id.set(clipPoints2[i].id); } else { matrix.copyVec2(cp.localPoint, clipPoints2[i].v); cp.id.set(clipPoints2[i].id); cp.id.swapFeatures(); } ++pointCount; } } manifold.pointCount = pointCount; };