planck-js
Version:
2D JavaScript/TypeScript physics engine for cross-platform HTML5 game development
420 lines (360 loc) • 12.2 kB
text/typescript
/*
* 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 { Vec2Value } from "../common/Vec2";
import { TransformValue } from "../common/Transform";
import { EPSILON } from "../common/Math";
/** @internal */ const math_sqrt = Math.sqrt;
/** @internal */ const pointA = matrix.vec2(0, 0);
/** @internal */ const pointB = matrix.vec2(0, 0);
/** @internal */ const temp = matrix.vec2(0, 0);
/** @internal */ const cA = matrix.vec2(0, 0);
/** @internal */ const cB = matrix.vec2(0, 0);
/** @internal */ const dist = matrix.vec2(0, 0);
/** @internal */ const planePoint = matrix.vec2(0, 0);
/** @internal */ const clipPoint = matrix.vec2(0, 0);
export enum ManifoldType {
e_unset = -1,
e_circles = 0,
e_faceA = 1,
e_faceB = 2
}
export enum ContactFeatureType {
e_unset = -1,
e_vertex = 0,
e_face = 1
}
/**
* This is used for determining the state of contact points.
*/
export enum PointState {
/** Point does not exist */
nullState = 0,
/** Point was added in the update */
addState = 1,
/** Point persisted across the update */
persistState = 2,
/** Point was removed in the update */
removeState = 3
}
/**
* Used for computing contact manifolds.
*/
export class ClipVertex {
v = matrix.vec2(0, 0);
id: ContactID = new ContactID();
set(o: ClipVertex): void {
matrix.copyVec2(this.v, o.v);
this.id.set(o.id);
}
recycle() {
matrix.zeroVec2(this.v);
this.id.recycle();
}
}
/**
* A manifold for two touching convex shapes. Manifolds are created in `evaluate`
* method of Contact subclasses.
*
* Supported manifold types are e_faceA or e_faceB for clip point versus plane
* with radius and e_circles point versus point with radius.
*
* We store contacts in this way so that position correction can account for
* movement, which is critical for continuous physics. All contact scenarios
* must be expressed in one of these types. This structure is stored across time
* steps, so we keep it small.
*/
export class Manifold {
type: ManifoldType;
/**
* Usage depends on manifold type:
* - circles: not used
* - faceA: the normal on polygonA
* - faceB: the normal on polygonB
*/
localNormal = matrix.vec2(0, 0);
/**
* Usage depends on manifold type:
* - circles: the local center of circleA
* - faceA: the center of faceA
* - faceB: the center of faceB
*/
localPoint = matrix.vec2(0, 0);
/** The points of contact */
points: ManifoldPoint[] = [ new ManifoldPoint(), new ManifoldPoint() ];
/** The number of manifold points */
pointCount: number = 0;
set(that: Manifold): void {
this.type = that.type;
matrix.copyVec2(this.localNormal, that.localNormal);
matrix.copyVec2(this.localPoint, that.localPoint);
this.pointCount = that.pointCount;
this.points[0].set(that.points[0]);
this.points[1].set(that.points[1]);
}
recycle(): void {
this.type = ManifoldType.e_unset;
matrix.zeroVec2(this.localNormal);
matrix.zeroVec2(this.localPoint);
this.pointCount = 0;
this.points[0].recycle();
this.points[1].recycle();
}
/**
* Evaluate the manifold with supplied transforms. This assumes modest motion
* from the original state. This does not change the point count, impulses, etc.
* The radii must come from the shapes that generated the manifold.
*/
getWorldManifold(wm: WorldManifold | null, xfA: TransformValue, radiusA: number, xfB: TransformValue, radiusB: number): WorldManifold {
if (this.pointCount == 0) {
return wm;
}
wm = wm || new WorldManifold();
wm.pointCount = this.pointCount;
const normal = wm.normal;
const points = wm.points;
const separations = wm.separations;
switch (this.type) {
case ManifoldType.e_circles: {
matrix.setVec2(normal, 1.0, 0.0);
const manifoldPoint = this.points[0];
matrix.transformVec2(pointA, xfA, this.localPoint);
matrix.transformVec2(pointB, xfB, manifoldPoint.localPoint);
matrix.subVec2(dist, pointB, pointA);
const lengthSqr = matrix.lengthSqrVec2(dist);
if (lengthSqr > EPSILON * EPSILON) {
const length = math_sqrt(lengthSqr);
matrix.scaleVec2(normal, 1 / length, dist);
}
matrix.combine2Vec2(cA, 1, pointA, radiusA, normal);
matrix.combine2Vec2(cB, 1, pointB, -radiusB, normal);
matrix.combine2Vec2(points[0], 0.5, cA, 0.5, cB);
separations[0] = matrix.dotVec2(matrix.subVec2(temp, cB, cA), normal);
break;
}
case ManifoldType.e_faceA: {
matrix.rotVec2(normal, xfA.q, this.localNormal);
matrix.transformVec2(planePoint, xfA, this.localPoint);
for (let i = 0; i < this.pointCount; ++i) {
const manifoldPoint = this.points[i];
matrix.transformVec2(clipPoint, xfB, manifoldPoint.localPoint);
matrix.combine2Vec2(cA, 1, clipPoint, radiusA - matrix.dotVec2(matrix.subVec2(temp, clipPoint, planePoint), normal), normal);
matrix.combine2Vec2(cB, 1, clipPoint, -radiusB, normal);
matrix.combine2Vec2(points[i], 0.5, cA, 0.5, cB);
separations[i] = matrix.dotVec2(matrix.subVec2(temp, cB, cA), normal);
}
break;
}
case ManifoldType.e_faceB: {
matrix.rotVec2(normal, xfB.q, this.localNormal);
matrix.transformVec2(planePoint, xfB, this.localPoint);
for (let i = 0; i < this.pointCount; ++i) {
const manifoldPoint = this.points[i];
matrix.transformVec2(clipPoint, xfA, manifoldPoint.localPoint);
matrix.combine2Vec2(cB, 1, clipPoint, radiusB - matrix.dotVec2(matrix.subVec2(temp, clipPoint, planePoint), normal), normal);
matrix.combine2Vec2(cA, 1, clipPoint, -radiusA, normal);
matrix.combine2Vec2(points[i], 0.5, cA, 0.5, cB);
separations[i] = matrix.dotVec2(matrix.subVec2(temp, cA, cB), normal);
}
// Ensure normal points from A to B.
matrix.negVec2(normal);
break;
}
}
return wm;
}
static clipSegmentToLine = clipSegmentToLine;
static ClipVertex = ClipVertex;
static getPointStates = getPointStates;
static PointState = PointState;
}
/**
* A manifold point is a contact point belonging to a contact manifold. It holds
* details related to the geometry and dynamics of the contact points.
*
* This structure is stored across time steps, so we keep it small.
*
* Note: impulses are used for internal caching and may not provide reliable
* contact forces, especially for high speed collisions.
*/
export class ManifoldPoint {
/**
* Usage depends on manifold type:
* - circles: the local center of circleB
* - faceA: the local center of circleB or the clip point of polygonB
* - faceB: the clip point of polygonA
*/
localPoint = matrix.vec2(0, 0);
/**
* The non-penetration impulse
*/
normalImpulse = 0;
/**
* The friction impulse
*/
tangentImpulse = 0;
/**
* Uniquely identifies a contact point between two shapes to facilitate warm starting
*/
readonly id = new ContactID();
set(that: ManifoldPoint): void {
matrix.copyVec2(this.localPoint, that.localPoint);
this.normalImpulse = that.normalImpulse;
this.tangentImpulse = that.tangentImpulse;
this.id.set(that.id);
}
recycle(): void {
matrix.zeroVec2(this.localPoint);
this.normalImpulse = 0;
this.tangentImpulse = 0;
this.id.recycle();
}
}
/**
* Contact ids to facilitate warm starting.
*
* ContactFeature: The features that intersect to form the contact point.
*/
export class ContactID {
/**
* Used to quickly compare contact ids.
*/
key = -1;
/** ContactFeature index on shapeA */
indexA = -1;
/** ContactFeature index on shapeB */
indexB = -1;
/** ContactFeature type on shapeA */
typeA = ContactFeatureType.e_unset;
/** ContactFeature type on shapeB */
typeB = ContactFeatureType.e_unset;
setFeatures(indexA: number, typeA: ContactFeatureType, indexB: number, typeB: ContactFeatureType): void {
this.indexA = indexA;
this.indexB = indexB;
this.typeA = typeA;
this.typeB = typeB;
this.key = this.indexA + this.indexB * 4 + this.typeA * 16 + this.typeB * 64;
}
set(that: ContactID): void {
this.indexA = that.indexA;
this.indexB = that.indexB;
this.typeA = that.typeA;
this.typeB = that.typeB;
this.key = this.indexA + this.indexB * 4 + this.typeA * 16 + this.typeB * 64;
}
swapFeatures(): void {
const indexA = this.indexA;
const indexB = this.indexB;
const typeA = this.typeA;
const typeB = this.typeB;
this.indexA = indexB;
this.indexB = indexA;
this.typeA = typeB;
this.typeB = typeA;
this.key = this.indexA + this.indexB * 4 + this.typeA * 16 + this.typeB * 64;
}
recycle(): void {
this.indexA = 0;
this.indexB = 0;
this.typeA = ContactFeatureType.e_unset;
this.typeB = ContactFeatureType.e_unset;
this.key = -1;
}
}
/**
* This is used to compute the current state of a contact manifold.
*/
export class WorldManifold {
/** World vector pointing from A to B */
normal = matrix.vec2(0, 0);
/** World contact point (point of intersection) */
points = [matrix.vec2(0, 0), matrix.vec2(0, 0)]; // [maxManifoldPoints]
/** A negative value indicates overlap, in meters */
separations = [0, 0]; // [maxManifoldPoints]
/** The number of manifold points */
pointCount = 0;
recycle() {
matrix.zeroVec2(this.normal);
matrix.zeroVec2(this.points[0]);
matrix.zeroVec2(this.points[1]);
this.separations[0] = 0;
this.separations[1] = 0;
this.pointCount = 0;
}
}
/**
* Compute the point states given two manifolds. The states pertain to the
* transition from manifold1 to manifold2. So state1 is either persist or remove
* while state2 is either add or persist.
*/
export function getPointStates(
state1: PointState[],
state2: PointState[],
manifold1: Manifold,
manifold2: Manifold
): void {
// state1, state2: PointState[Settings.maxManifoldPoints]
// for (var i = 0; i < Settings.maxManifoldPoints; ++i) {
// state1[i] = PointState.nullState;
// state2[i] = PointState.nullState;
// }
// Detect persists and removes.
for (let i = 0; i < manifold1.pointCount; ++i) {
const id = manifold1.points[i].id;
state1[i] = PointState.removeState;
for (let j = 0; j < manifold2.pointCount; ++j) {
if (manifold2.points[j].id.key === id.key) {
state1[i] = PointState.persistState;
break;
}
}
}
// Detect persists and adds.
for (let i = 0; i < manifold2.pointCount; ++i) {
const id = manifold2.points[i].id;
state2[i] = PointState.addState;
for (let j = 0; j < manifold1.pointCount; ++j) {
if (manifold1.points[j].id.key === id.key) {
state2[i] = PointState.persistState;
break;
}
}
}
}
/**
* Clipping for contact manifolds. Sutherland-Hodgman clipping.
*/
export function clipSegmentToLine(
vOut: ClipVertex[],
vIn: ClipVertex[],
normal: Vec2Value,
offset: number,
vertexIndexA: number
): number {
// Start with no output points
let numOut = 0;
// Calculate the distance of end points to the line
const distance0 = matrix.dotVec2(normal, vIn[0].v) - offset;
const distance1 = matrix.dotVec2(normal, vIn[1].v) - offset;
// If the points are behind the plane
if (distance0 <= 0.0)
vOut[numOut++].set(vIn[0]);
if (distance1 <= 0.0)
vOut[numOut++].set(vIn[1]);
// If the points are on different sides of the plane
if (distance0 * distance1 < 0.0) {
// Find intersection point of edge and plane
const interp = distance0 / (distance0 - distance1);
matrix.combine2Vec2(vOut[numOut].v, 1 - interp, vIn[0].v, interp, vIn[1].v);
// VertexA is hitting edgeB.
vOut[numOut].id.setFeatures(vertexIndexA, ContactFeatureType.e_vertex, vIn[0].id.indexB, ContactFeatureType.e_face);
++numOut;
}
return numOut;
}