UNPKG

planck-js

Version:

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

583 lines (487 loc) 17.1 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 type { MassData } from "../../dynamics/Body"; import { RayCastOutput, RayCastInput, AABBValue } from "../AABB"; import { DistanceProxy } from "../Distance"; import { EPSILON } from "../../common/Math"; import { Transform, TransformValue } from "../../common/Transform"; import { Rot } from "../../common/Rot"; import { Vec2, Vec2Value } from "../../common/Vec2"; import { SettingsInternal as Settings } from "../../Settings"; import { Shape } from "../Shape"; /** @internal */ const _ASSERT = typeof ASSERT === "undefined" ? false : ASSERT; /** @internal */ const _CONSTRUCTOR_FACTORY = typeof CONSTRUCTOR_FACTORY === "undefined" ? false : CONSTRUCTOR_FACTORY; /** @internal */ const math_max = Math.max; /** @internal */ const math_min = Math.min; /** @internal */ const temp = matrix.vec2(0, 0); /** @internal */ const e = matrix.vec2(0, 0); /** @internal */ const e1 = matrix.vec2(0, 0); /** @internal */ const e2 = matrix.vec2(0, 0); /** @internal */ const center = matrix.vec2(0, 0); /** @internal */ const s = matrix.vec2(0, 0); declare module "./PolygonShape" { /** @hidden @deprecated Use new keyword. */ // @ts-expect-error function PolygonShape(vertices?: Vec2Value[]): PolygonShape; } /** * A convex polygon. It is assumed that the interior of the polygon is to the * left of each edge. Polygons have a maximum number of vertices equal to * Settings.maxPolygonVertices. In most cases you should not need many vertices * for a convex polygon. extends Shape */ // @ts-expect-error export class PolygonShape extends Shape { static TYPE = "polygon" as const; /** @hidden */ m_type: "polygon"; /** @hidden */ m_centroid: Vec2; /** @hidden */ m_vertices: Vec2[]; // [Settings.maxPolygonVertices] /** @hidden */ m_normals: Vec2[]; // [Settings.maxPolygonVertices] /** @hidden */ m_count: number; /** @hidden */ m_radius: number; constructor(vertices?: Vec2Value[]) { // @ts-ignore if (_CONSTRUCTOR_FACTORY && !(this instanceof PolygonShape)) { return new PolygonShape(vertices); } super(); this.m_type = PolygonShape.TYPE; this.m_radius = Settings.polygonRadius; this.m_centroid = Vec2.zero(); this.m_vertices = []; this.m_normals = []; this.m_count = 0; if (vertices && vertices.length) { this._set(vertices); } } /** @internal */ _serialize(): object { return { type: this.m_type, vertices: this.m_vertices, }; } /** @internal */ static _deserialize(data: any, fixture: any, restore: any): PolygonShape { const vertices: Vec2[] = []; if (data.vertices) { for (let i = 0; i < data.vertices.length; i++) { vertices.push(restore(Vec2, data.vertices[i])); } } const shape = new PolygonShape(vertices); return shape; } getType(): "polygon" { return this.m_type; } getRadius(): number { return this.m_radius; } /** * @internal @deprecated Shapes should be treated as immutable. * * clone the concrete shape. */ _clone(): PolygonShape { const clone = new PolygonShape(); clone.m_type = this.m_type; clone.m_radius = this.m_radius; clone.m_count = this.m_count; clone.m_centroid.setVec2(this.m_centroid); for (let i = 0; i < this.m_count; i++) { clone.m_vertices.push(this.m_vertices[i].clone()); } for (let i = 0; i < this.m_normals.length; i++) { clone.m_normals.push(this.m_normals[i].clone()); } return clone; } /** * Get the number of child primitives. */ getChildCount(): 1 { return 1; } /** @hidden */ _reset(): void { this._set(this.m_vertices); } /** * @internal * * Create a convex hull from the given array of local points. The count must be * in the range [3, Settings.maxPolygonVertices]. * * Warning: the points may be re-ordered, even if they form a convex polygon * Warning: collinear points are handled but not removed. Collinear points may * lead to poor stacking behavior. */ _set(vertices: Vec2Value[]): void { if (_ASSERT) console.assert(3 <= vertices.length && vertices.length <= Settings.maxPolygonVertices); if (vertices.length < 3) { this._setAsBox(1.0, 1.0); return; } let n = math_min(vertices.length, Settings.maxPolygonVertices); // Perform welding and copy vertices into local buffer. const ps: Vec2[] = []; // [Settings.maxPolygonVertices]; for (let i = 0; i < n; ++i) { const v = vertices[i]; let unique = true; for (let j = 0; j < ps.length; ++j) { if (Vec2.distanceSquared(v, ps[j]) < 0.25 * Settings.linearSlopSquared) { unique = false; break; } } if (unique) { ps.push(Vec2.clone(v)); } } n = ps.length; if (n < 3) { // Polygon is degenerate. if (_ASSERT) console.assert(false); this._setAsBox(1.0, 1.0); return; } // Create the convex hull using the Gift wrapping algorithm // http://en.wikipedia.org/wiki/Gift_wrapping_algorithm // Find the right most point on the hull (in case of multiple points bottom most is used) let i0 = 0; let x0 = ps[0].x; for (let i = 1; i < n; ++i) { const x = ps[i].x; if (x > x0 || (x === x0 && ps[i].y < ps[i0].y)) { i0 = i; x0 = x; } } const hull = [] as number[]; // [Settings.maxPolygonVertices]; let m = 0; let ih = i0; while (true) { if (_ASSERT) console.assert(m < Settings.maxPolygonVertices); hull[m] = ih; let ie = 0; for (let j = 1; j < n; ++j) { if (ie === ih) { ie = j; continue; } const r = Vec2.sub(ps[ie], ps[hull[m]]); const v = Vec2.sub(ps[j], ps[hull[m]]); const c = Vec2.crossVec2Vec2(r, v); // c < 0 means counter-clockwise wrapping, c > 0 means clockwise wrapping if (c < 0.0) { ie = j; } // Collinearity check if (c === 0.0 && v.lengthSquared() > r.lengthSquared()) { ie = j; } } ++m; ih = ie; if (ie === i0) { break; } } if (m < 3) { // Polygon is degenerate. if (_ASSERT) console.assert(false); this._setAsBox(1.0, 1.0); return; } this.m_count = m; // Copy vertices. this.m_vertices = []; for (let i = 0; i < m; ++i) { this.m_vertices[i] = ps[hull[i]]; } // Compute normals. Ensure the edges have non-zero length. for (let i = 0; i < m; ++i) { const i1 = i; const i2 = i + 1 < m ? i + 1 : 0; const edge = Vec2.sub(this.m_vertices[i2], this.m_vertices[i1]); if (_ASSERT) console.assert(edge.lengthSquared() > EPSILON * EPSILON); this.m_normals[i] = Vec2.crossVec2Num(edge, 1.0); this.m_normals[i].normalize(); } // Compute the polygon centroid. this.m_centroid = computeCentroid(this.m_vertices, m); } /** @internal */ _setAsBox(hx: number, hy: number, center?: Vec2Value, angle?: number): void { // start with right-bottom, counter-clockwise, as in Gift wrapping algorithm in PolygonShape._set() this.m_vertices[0] = Vec2.neo(hx, -hy); this.m_vertices[1] = Vec2.neo(hx, hy); this.m_vertices[2] = Vec2.neo(-hx, hy); this.m_vertices[3] = Vec2.neo(-hx, -hy); this.m_normals[0] = Vec2.neo(1.0, 0.0); this.m_normals[1] = Vec2.neo(0.0, 1.0); this.m_normals[2] = Vec2.neo(-1.0, 0.0); this.m_normals[3] = Vec2.neo(0.0, -1.0); this.m_count = 4; if (center && Vec2.isValid(center)) { angle = angle || 0; matrix.copyVec2(this.m_centroid, center); const xf = Transform.identity(); xf.p.setVec2(center); xf.q.setAngle(angle); // Transform vertices and normals. for (let i = 0; i < this.m_count; ++i) { this.m_vertices[i] = Transform.mulVec2(xf, this.m_vertices[i]); this.m_normals[i] = Rot.mulVec2(xf.q, this.m_normals[i]); } } } /** * Test a point for containment in this shape. This only works for convex * shapes. * * @param xf The shape world transform. * @param p A point in world coordinates. */ testPoint(xf: TransformValue, p: Vec2Value): boolean { const pLocal = matrix.detransformVec2(temp, xf, p); for (let i = 0; i < this.m_count; ++i) { const dot = matrix.dotVec2(this.m_normals[i], pLocal) - matrix.dotVec2(this.m_normals[i], this.m_vertices[i]); if (dot > 0.0) { return false; } } return true; } /** * Cast a ray against a child shape. * * @param output The ray-cast results. * @param input The ray-cast input parameters. * @param xf The transform to be applied to the shape. * @param childIndex The child shape index */ rayCast(output: RayCastOutput, input: RayCastInput, xf: Transform, childIndex: number): boolean { // Put the ray into the polygon's frame of reference. const p1 = Rot.mulTVec2(xf.q, Vec2.sub(input.p1, xf.p)); const p2 = Rot.mulTVec2(xf.q, Vec2.sub(input.p2, xf.p)); const d = Vec2.sub(p2, p1); let lower = 0.0; let upper = input.maxFraction; let index = -1; for (let i = 0; i < this.m_count; ++i) { // p = p1 + a * d // dot(normal, p - v) = 0 // dot(normal, p1 - v) + a * dot(normal, d) = 0 const numerator = Vec2.dot(this.m_normals[i], Vec2.sub(this.m_vertices[i], p1)); const denominator = Vec2.dot(this.m_normals[i], d); if (denominator == 0.0) { if (numerator < 0.0) { return false; } } else { // Note: we want this predicate without division: // lower < numerator / denominator, where denominator < 0 // Since denominator < 0, we have to flip the inequality: // lower < numerator / denominator <==> denominator * lower > numerator. if (denominator < 0.0 && numerator < lower * denominator) { // Increase lower. // The segment enters this half-space. lower = numerator / denominator; index = i; } else if (denominator > 0.0 && numerator < upper * denominator) { // Decrease upper. // The segment exits this half-space. upper = numerator / denominator; } } // The use of epsilon here causes the assert on lower to trip // in some cases. Apparently the use of epsilon was to make edge // shapes work, but now those are handled separately. // if (upper < lower - matrix.EPSILON) if (upper < lower) { return false; } } if (_ASSERT) console.assert(0.0 <= lower && lower <= input.maxFraction); if (index >= 0) { output.fraction = lower; output.normal = Rot.mulVec2(xf.q, this.m_normals[index]); return true; } return false; } /** * Given a transform, compute the associated axis aligned bounding box for a * child shape. * * @param aabb Returns the axis aligned box. * @param xf The world transform of the shape. * @param childIndex The child shape */ computeAABB(aabb: AABBValue, xf: TransformValue, childIndex: number): void { let minX = Infinity; let minY = Infinity; let maxX = -Infinity; let maxY = -Infinity; for (let i = 0; i < this.m_count; ++i) { const v = matrix.transformVec2(temp, xf, this.m_vertices[i]); minX = math_min(minX, v.x); maxX = math_max(maxX, v.x); minY = math_min(minY, v.y); maxY = math_max(maxY, v.y); } matrix.setVec2(aabb.lowerBound, minX - this.m_radius, minY - this.m_radius); matrix.setVec2(aabb.upperBound, maxX + this.m_radius, maxY + this.m_radius); } /** * Compute the mass properties of this shape using its dimensions and density. * The inertia tensor is computed about the local origin. * * @param massData Returns the mass data for this shape. * @param density The density in kilograms per meter squared. */ computeMass(massData: MassData, density: number): void { // Polygon mass, centroid, and inertia. // Let rho be the polygon density in mass per unit area. // Then: // mass = rho * int(dA) // centroid.x = (1/mass) * rho * int(x * dA) // centroid.y = (1/mass) * rho * int(y * dA) // I = rho * int((x*x + y*y) * dA) // // We can compute these integrals by summing all the integrals // for each triangle of the polygon. To evaluate the integral // for a single triangle, we make a change of variables to // the (u,v) coordinates of the triangle: // x = x0 + e1x * u + e2x * v // y = y0 + e1y * u + e2y * v // where 0 <= u && 0 <= v && u + v <= 1. // // We integrate u from [0,1-v] and then v from [0,1]. // We also need to use the Jacobian of the transformation: // D = cross(e1, e2) // // Simplification: triangle centroid = (1/3) * (p1 + p2 + p3) // // The rest of the derivation is handled by computer algebra. if (_ASSERT) console.assert(this.m_count >= 3); matrix.zeroVec2(center); let area = 0.0; let I = 0.0; // s is the reference point for forming triangles. // It's location doesn't change the result (except for rounding error). matrix.zeroVec2(s); // This code would put the reference point inside the polygon. for (let i = 0; i < this.m_count; ++i) { matrix.plusVec2(s, this.m_vertices[i]); } matrix.scaleVec2(s, 1.0 / this.m_count, s); const k_inv3 = 1.0 / 3.0; for (let i = 0; i < this.m_count; ++i) { // Triangle vertices. matrix.subVec2(e1, this.m_vertices[i], s); if ( i + 1 < this.m_count) { matrix.subVec2(e2, this.m_vertices[i + 1], s); } else { matrix.subVec2(e2, this.m_vertices[0], s); } const D = matrix.crossVec2Vec2(e1, e2); const triangleArea = 0.5 * D; area += triangleArea; // Area weighted centroid matrix.combine2Vec2(temp, triangleArea * k_inv3, e1, triangleArea * k_inv3, e2); matrix.plusVec2(center, temp); const ex1 = e1.x; const ey1 = e1.y; const ex2 = e2.x; const ey2 = e2.y; const intx2 = ex1 * ex1 + ex2 * ex1 + ex2 * ex2; const inty2 = ey1 * ey1 + ey2 * ey1 + ey2 * ey2; I += (0.25 * k_inv3 * D) * (intx2 + inty2); } // Total mass massData.mass = density * area; // Center of mass if (_ASSERT) console.assert(area > EPSILON); matrix.scaleVec2(center, 1.0 / area, center); matrix.addVec2(massData.center, center, s); // Inertia tensor relative to the local origin (point s). massData.I = density * I; // Shift to center of mass then to original body origin. massData.I += massData.mass * (matrix.dotVec2(massData.center, massData.center) - matrix.dotVec2(center, center)); } /** * Validate convexity. This is a very time consuming operation. * @returns true if valid */ validate(): boolean { for (let i = 0; i < this.m_count; ++i) { const i1 = i; const i2 = i < this.m_count - 1 ? i1 + 1 : 0; const p = this.m_vertices[i1]; matrix.subVec2(e, this.m_vertices[i2], p); for (let j = 0; j < this.m_count; ++j) { if (j == i1 || j == i2) { continue; } const c = matrix.crossVec2Vec2(e, matrix.subVec2(temp, this.m_vertices[j], p)); if (c < 0.0) { return false; } } } return true; } computeDistanceProxy(proxy: DistanceProxy): void { for (let i = 0; i < this.m_count; ++i) { proxy.m_vertices[i] = this.m_vertices[i]; } proxy.m_vertices.length = this.m_count; proxy.m_count = this.m_count; proxy.m_radius = this.m_radius; } } /** @internal */ function computeCentroid(vs: Vec2[], count: number): Vec2 { if (_ASSERT) console.assert(count >= 3); const c = Vec2.zero(); let area = 0.0; // pRef is the reference point for forming triangles. // It's location doesn't change the result (except for rounding error). const pRef = Vec2.zero(); if (false) { // This code would put the reference point inside the polygon. for (let i = 0; i < count; ++i) { pRef.add(vs[i]); } pRef.mul(1.0 / count); } const inv3 = 1.0 / 3.0; for (let i = 0; i < count; ++i) { // Triangle vertices. const p1 = pRef; const p2 = vs[i]; const p3 = i + 1 < count ? vs[i + 1] : vs[0]; const e1 = Vec2.sub(p2, p1); const e2 = Vec2.sub(p3, p1); const D = Vec2.crossVec2Vec2(e1, e2); const triangleArea = 0.5 * D; area += triangleArea; // Area weighted centroid matrix.combine3Vec2(temp, 1, p1, 1, p2, 1, p3); matrix.plusScaleVec2(c, triangleArea * inv3, temp); } // Centroid if (_ASSERT) console.assert(area > EPSILON); c.mul(1.0 / area); return c; } export const Polygon = PolygonShape;