UNPKG

planck-js

Version:

2D physics engine for JavaScript/HTML5 game development

494 lines (411 loc) 13.7 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 = PolygonShape; var create = require('../util/create'); var options = require('../util/options'); var Settings = require('../Settings'); var Shape = require('../Shape'); var Math = require('../common/Math'); var Transform = require('../common/Transform'); var Rot = require('../common/Rot'); var Vec2 = require('../common/Vec2'); var AABB = require('../collision/AABB'); PolygonShape._super = Shape; PolygonShape.prototype = create(PolygonShape._super.prototype); PolygonShape.TYPE = 'polygon'; /** * 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 */ function PolygonShape(vertices) { if (!(this instanceof PolygonShape)) { return new PolygonShape(vertices); } PolygonShape._super.call(this); this.m_type = PolygonShape.TYPE; this.m_radius = Settings.polygonRadius; this.m_centroid = Vec2(); this.m_vertices = []; // Vec2[Settings.maxPolygonVertices] this.m_normals = []; // Vec2[Settings.maxPolygonVertices] this.m_count = 0; if (vertices && vertices.length) { this.Set(vertices); } } PolygonShape.prototype.GetVertex = function(index) { Assert(0 <= index && index < this.m_count); return this.m_vertices[index]; } PolygonShape.prototype.Clone = function() { var clone = new PolygonShape(); clone.m_type = this.m_type; clone.m_radius = this.m_radius; clone.m_count = this.m_count; clone.m_centroid.Set(this.m_centroid); for (var i = 0; i < this.m_count; i++) { clone.m_vertices.push(this.m_vertices[i].Clone()); } for (var i = 0; i < this.m_normals.length; i++) { clone.m_normals.push(this.m_normals[i].Clone()); } return clone; } /** * Build vertices to represent an axis-aligned box centered on the local origin. * Build vertices to represent an oriented box. * * @param hx the half-width. * @param hy the half-height. * @param center the center of the box in local coordinates. * @param angle the rotation of the box in local coordinates. */ PolygonShape.prototype.SetAsBox = function(hx, hy, center, angle) { this.m_vertices[0] = Vec2(-hx, -hy); this.m_vertices[1] = Vec2(hx, -hy); this.m_vertices[2] = Vec2(hx, hy); this.m_vertices[3] = Vec2(-hx, hy); this.m_normals[0] = Vec2(0.0, -1.0); this.m_normals[1] = Vec2(1.0, 0.0); this.m_normals[2] = Vec2(0.0, 1.0); this.m_normals[3] = Vec2(-1.0, 0.0); this.m_count = 4; if (center && ('x' in center) && ('y' in center)) { angle = angle || 0; this.m_centroid.Set(center); var xf = new Transform(); xf.p.Set(center); xf.q.Set(angle); // Transform vertices and normals. for (var i = 0; i < this.m_count; ++i) { this.m_vertices[i] = Transform.Mul(xf, this.m_vertices[i]); this.m_normals[i] = Rot.Mul(xf.q, this.m_normals[i]); } } return this; } PolygonShape.prototype.GetChildCount = function() { return 1; } function ComputeCentroid(vs, count) { Assert(count >= 3); var c = new Vec2(); var area = 0.0; // pRef is the reference point for forming triangles. // It's location doesn't change the result (except for rounding error). var pRef = Vec2(); if (false) { // This code would put the reference point inside the polygon. for (var i = 0; i < count; ++i) { pRef.Add(vs[i]); } pRef.Mul(1.0 / count); } var inv3 = 1.0 / 3.0; for (var i = 0; i < count; ++i) { // Triangle vertices. var p1 = pRef; var p2 = vs[i]; var p3 = i + 1 < count ? vs[i + 1] : vs[0]; var e1 = Vec2.Sub(p2, p1); var e2 = Vec2.Sub(p3, p1); var D = Vec2.Cross(e1, e2); var triangleArea = 0.5 * D; area += triangleArea; // Area weighted centroid // TODO c.WAdd(triangleArea * inv3, p1); c.WAdd(triangleArea * inv3, p2); c.WAdd(triangleArea * inv3, p3); } // Centroid Assert(area > Math.EPSILON); c.Mul(1.0 / area); return c; } /** * * 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. */ PolygonShape.prototype.Set = function(vertices) { Assert(3 <= vertices.length && vertices.length <= Settings.maxPolygonVertices); if (vertices.length < 3) { SetAsBox(1.0, 1.0); return; } var n = Math.min(vertices.length, Settings.maxPolygonVertices); // Perform welding and copy vertices into local buffer. var ps = [];// [Settings.maxPolygonVertices]; var tempCount = 0; for (var i = 0; i < n; ++i) { var v = vertices[i]; var unique = true; for (var j = 0; j < tempCount; ++j) { if (Vec2.DistanceSquared(v, ps[j]) < 0.25 * Settings.linearSlopSquared) { unique = false; break; } } if (unique) { ps[tempCount++] = v; } } n = tempCount; if (n < 3) { // Polygon is degenerate. Assert(false); 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 var i0 = 0; var x0 = ps[0].x; for (var i = 1; i < n; ++i) { var x = ps[i].x; if (x > x0 || (x == x0 && ps[i].y < ps[i0].y)) { i0 = i; x0 = x; } } var hull = [];// [Settings.maxPolygonVertices]; var m = 0; var ih = i0; for (;;) { hull[m] = ih; var ie = 0; for (var j = 1; j < n; ++j) { if (ie == ih) { ie = j; continue; } var r = Vec2.Sub(ps[ie], ps[hull[m]]); var v = Vec2.Sub(ps[j], ps[hull[m]]); var c = Vec2.Cross(r, v); 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. Assert(false); SetAsBox(1.0, 1.0); return; } this.m_count = m; // Copy vertices. for (var i = 0; i < m; ++i) { this.m_vertices[i] = ps[hull[i]]; } // Compute normals. Ensure the edges have non-zero length. for (var i = 0; i < m; ++i) { var i1 = i; var i2 = i + 1 < m ? i + 1 : 0; var edge = Vec2.Sub(this.m_vertices[i2], this.m_vertices[i1]); Assert(edge.LengthSquared() > Math.EPSILON * Math.EPSILON); this.m_normals[i] = Vec2.Cross(edge, 1.0); this.m_normals[i].Normalize(); } // Compute the polygon centroid. this.m_centroid = ComputeCentroid(this.m_vertices, m); } PolygonShape.prototype.TestPoint = function(xf, p) { var pLocal = Rot.MulT(xf.q, Vec2.Sub(p, xf.p)); for (var i = 0; i < this.m_count; ++i) { var dot = Vec2.Dot(this.m_normals[i], Vec2.Sub(pLocal, this.m_vertices[i])); if (dot > 0.0) { return false; } } return true; } PolygonShape.prototype.RayCast = function(output, input, xf, childIndex) { // Put the ray into the polygon's frame of reference. var p1 = MulT(xf.q, Sub(input.p1, xf.p)); var p2 = MulT(xf.q, Sub(input.p2, xf.p)); var d = p2 - p1; var lower = 0.0; var upper = input.maxFraction; var index = -1; for (var 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 var numerator = Dot(this.m_normals[i], this.m_vertices[i] - p1); var denominator = 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 - Math.EPSILON) if (upper < lower) { return false; } } Assert(0.0 <= lower && lower <= input.maxFraction); if (index >= 0) { output.fraction = lower; output.normal = Mul(xf.q, this.m_normals[index]); return true; } return false; } PolygonShape.prototype.ComputeAABB = function(aabb, xf, childIndex) { var minX = Infinity, minY = Infinity; var maxX = -Infinity, maxY = -Infinity; for (var i = 0; i < this.m_count; ++i) { var v = Transform.Mul(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); } aabb.lowerBound.Set(minX, minY); aabb.upperBound.Set(maxX, maxY); aabb.Extend(this.m_radius); } PolygonShape.prototype.ComputeMass = function(massData, density) { // 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. Assert(this.m_count >= 3); var center = new Vec2(); var area = 0.0; var I = 0.0; // s is the reference point for forming triangles. // It's location doesn't change the result (except for rounding error). var s = new Vec2(); // This code would put the reference point inside the polygon. for (var i = 0; i < this.m_count; ++i) { s.Add(this.m_vertices[i]); } s.Mul(1.0 / this.m_count); var k_inv3 = 1.0 / 3.0; for (var i = 0; i < this.m_count; ++i) { // Triangle vertices. var e1 = Vec2.Sub(this.m_vertices[i], s); var e2 = i + 1 < this.m_count ? Vec2.Sub(this.m_vertices[i + 1], s) : Vec2 .Sub(this.m_vertices[0], s); var D = Vec2.Cross(e1, e2); var triangleArea = 0.5 * D; area += triangleArea; // Area weighted centroid center.WAdd(triangleArea * k_inv3, e1, triangleArea * k_inv3, e2); var ex1 = e1.x; var ey1 = e1.y; var ex2 = e2.x; var ey2 = e2.y; var intx2 = ex1 * ex1 + ex2 * ex1 + ex2 * ex2; var inty2 = ey1 * ey1 + ey2 * ey1 + ey2 * ey2; I += (0.25 * k_inv3 * D) * (intx2 + inty2); } // Total mass massData.mass = density * area; // Center of mass Assert(area > Math.EPSILON); center.Mul(1.0 / area); massData.center.WSet(1, center, 1, 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 * (Vec2.Dot(massData.center, massData.center) - Vec2.Dot(center, center)); } // Validate convexity. This is a very time consuming operation. // @returns true if valid PolygonShape.prototype.Validate = function() { for (var i = 0; i < this.m_count; ++i) { var i1 = i; var i2 = i < this.m_count - 1 ? i1 + 1 : 0; var p = this.m_vertices[i1]; var e = Vec2.Sub(this.m_vertices[i2], p); for (var j = 0; j < this.m_count; ++j) { if (j == i1 || j == i2) { continue; } var v = Vec2.Sub(this.m_vertices[j], p); var c = Vec2.Cross(e, v); if (c < 0.0) { return false; } } } return true; } PolygonShape.prototype.ComputeDistanceProxy = function(proxy) { proxy.m_vertices = this.m_vertices; proxy.m_count = this.m_count; proxy.m_radius = this.m_radius; };