top of page
Search
cedarcantab

Position Based Dynamics

Updated: Mar 19



class Vertex {

constructor(world, body, x, y) {

this.position = new Phaser.Math.Vector2(x,y);

this.oldPosition = new Phaser.Math.Vector2(x,y);

this.acceleration = new Phaser.Math.Vector2();

this.parent = body;

}

}

class Edge {

constructor( world, body, pV1, pV2, pBoundary) {

this.v1 = pV1;

this.v2 = pV2;

this.length = Math.hypot(pV2.position.x - pV1.position.x, pV2.position.y - pV1.position.y); //Calculate the original length

this.parent = body;

this.boundary = pBoundary;

}

};





class PhysicsBody {

constructor(world, pMass) {

this.mass = pMass;

this.vertices = [];

this.vertexCount = 0;

this.edges = [];

this.edgeCount = 0;

this.parent = world;

this.center = new Phaser.Math.Vector2();

this.minX; // these are for the bounding box;

this.maxX;

this.minY;

this.maxY;

this.mass;

world.addBody(this)

}

addVertex(V ) {

this.vertices.push(V);

this.vertexCount++;

this.parent.addVertex(V);

}

addEdge(E) {

this.edges.push(E);

this.edgeCount++;

this.parent.addEdge(E);

}

ProjectToAxis(Axis) {

var data = new MinMax();

let dotP = Axis.x this.vertices[ 0 ].position.x + Axis.y this.vertices[ 0 ].position.y;

data.min = dotP;

data.max = dotP; //Set the minimum and maximum values to the projection of the first vertex

for (let i = 0; i < this.vertexCount; i++ ) {

dotP = Axis.x this.vertices[ i ].position.x + Axis.y this.vertices[ i ].position.y; //Project the rest of the vertices onto the axis and extend the interval to the left/right if necessary

data.min = Math.min( dotP, data.min );

data.max = Math.max( dotP, data.max );

}

return data;

}

/**

* Calculates the center of mass

*/

calculateCenter() {

this.center.x = 0;

this.center.y = 0;

this.minX = 10000.0;

this.minY = 10000.0;

this.maxX = -10000.0;

this.maxY = -10000.0;

for (let i = 0; i < this.vertexCount; i++) {

this.center.x += this.vertices[ i ].position.x;

this.center.y += this.vertices[ i ].position.y;

this.minX = Math.min( this.minX, this.vertices[ i ].position.x );

this.minY = Math.min( this.minY, this.vertices[ i ].position.y );

this.maxX = Math.max( this.maxX, this.vertices[ i ].position.x );

this.maxY = Math.max( this.maxY, this.vertices[ i ].position.y );

}

this.center.x /= this.vertexCount;

this.center.y /= this.vertexCount;

}

/**

* Helper function to create a box primitive.

* @param x

* @param y

* @param width

* @param height

*/

createBox( world, x, y, width, height ) {

let V1 = new Vertex( world, this, x , y );

this.addVertex(V1)

let V2 = new Vertex( world, this, x + width, y );

this.addVertex(V2)

let V3 = new Vertex( world, this, x + width, y + height );

this.addVertex(V3)

let V4 = new Vertex( world, this, x , y + height );

this.addVertex(V4)

this.addEdge(new Edge( world, this, V1, V2, true )) ;

this.addEdge(new Edge( world, this, V2, V3, true )) ;

this.addEdge(new Edge( world, this, V3, V4, true ));

this.addEdge(new Edge( world, this, V4, V1, true ));

this.addEdge(new Edge( world, this, V1, V3, false ));

this.addEdge(new Edge( world, this, V2, V4, false ));

}

createTriangle( world, X, Y, w, h) {

let V1 = new Vertex( world, this, X , Y + h );

this.addVertex(V1)

let V2 = new Vertex( world, this, X + w/2, Y );

this.addVertex(V2)

let V3 = new Vertex( world, this, X + w, Y + h );

this.addVertex(V3)

this.addEdge(new Edge( world, this, V1, V2 ));

this.addEdge(new Edge( world, this, V2, V3 ));

this.addEdge(new Edge( world, this, V3, V1 ));

}

};





class Physics {

constructor(scene, width, height, GravitationX, GravitationY, pIterations) {

this.scene = scene;

this.screenWidth = width;

this.screenHeight = height;

this.bodies = [];

this.bodyCount = 0;

this.vertices = [];

this.vertexCount = 0;

this.edgeCount = 0;

this.edges = [];

this.gravity = new Phaser.Math.Vector2(GravitationX, GravitationY);

this.iterations = pIterations;

this.MAX_BODIES = 256; //Maximum body/vertex/edgecount the physics simulation can handle

this.MAX_VERTICES = 1024;

this.MAX_EDGES = 1024;

this.MAX_BODY_VERTICES = 10; //Maximum body/edge count a body can contain

this.MAX_BODY_EDGES = 10;

this.axis = new Phaser.Math.Vector2()

this.collisionInfo = new CollisionInfo();

}

update(dt) {

this.updateForces(dt);

this.updateVerlet(dt);

this.iterateCollisions();

}

render() { // pass the graphics object

this.graphics.clear();

this.graphics.lineStyle(1,'0xff0000')

for (let i = 0; i < this.edgeCount; i++ ) {

this.graphics.lineBetween(this.edges[ i ].v1.position.x, this.edges[ i ].v1.position.y, this.edges[ i ].v2.position.x, this.edges[ i ].v2.position.y );

}

this.graphics.fillStyle('0xffffff');

for (let i = 0; i < this.vertexCount; i++ ) {

this.graphics.fillCircle(this.vertices[ i ].position.x, this.vertices[ i ].position.y, 4);

}

}

/**

* Adds new elements to the simulation

* @param Body

*/

addBody(Body) {

this.bodies.push(Body);

this.bodyCount++;

}

addEdge(E) {

this.edges.push(E);

this.edgeCount++;

}

addVertex(V) {

this.vertices.push(V);

this.vertexCount ++;

}

findVertex( X, Y ) {

var nearestVertex = null;

var minDistance = 1000;

var coords = new Phaser.Math.Vector2(X, Y);

for (let i = 0; i < this.vertexCount; i++ ) {

var distance = Math.hypot(this.vertices[ i ].position.x - coords.x, this.vertices[ i ].position.y - coords.y);

if (distance < minDistance ) {

nearestVertex = this.vertices[ i ];

minDistance = distance;

}

}

return nearestVertex;

}

updateForces(dt) {

for (let I=0; I<this.vertexCount; I++ ) {

this.vertices[ I ].acceleration = this.gravity;

}

}

updateVerlet(dt) {

var tempX;

var tempY;

for (let i = 0; i < this.vertexCount; i++) {

let v = this.vertices[i];

tempX = v.position.x;

tempY = v.position.y;

v.position.x += v.position.x - v.oldPosition.x + v.acceleration.x * dt;

v.position.y += v.position.y - v.oldPosition.y + v.acceleration.y * dt;

v.oldPosition.x = tempX;

v.oldPosition.y = tempY;

}

}

updateEdges() {

for (let i = 0; i < this.edgeCount; i++) {

var e = this.edges[ i ];

// Calculate the vector between the two vertices

let v1v2x = e.v2.position.x - e.v1.position.x;

let v1v2y = e.v2.position.y - e.v1.position.y;

let v1v2Length = Math.hypot(v1v2x, v1v2y); //Calculate the current distance

let diff = v1v2Length - e.length; //Calculate the difference from the original length

// Normalise

var len = 1.0/Math.hypot(v1v2x, v1v2y);

v1v2x *= len;

v1v2y *= len;

// Push both vertices apart by half of the difference respectively so the distance between them equals the original length

e.v1.position.x += v1v2x diff 0.5;

e.v1.position.y += v1v2y diff 0.5;

e.v2.position.x -= v1v2x diff 0.5;

e.v2.position.y -= v1v2y diff 0.5;

}

}

iterateCollisions() {

for (let i = 0; i < this.iterations; i++ ) { //Repeat this a few times to give more exact results

//A small 'hack' that keeps the vertices inside the screen. You could of course implement static objects and create

//four to serve as screen boundaries, but the max/min method is faster

for (let t = 0; t < this.vertexCount; t++ ) {

var pos = this.vertices[ t ].position;

pos.x = Math.max( Math.min( pos.x, this.screenWidth ), 0.0 );

pos.y = Math.max( Math.min( pos.y, this.screenHeight ), 0.0 );

}

this.updateEdges(); //Edge correction step

for (let j = 0; j < this.bodyCount; j++) {

this.bodies[j].calculateCenter(); //Recalculate the center

}

for (let b1 = 0; b1 < this.bodyCount; b1++) { //Iterate trough all bodies

for (let b2 = 0; b2 < this.bodyCount; b2++) {

if (b1 != b2) {

if (this.bodiesOverlap(this.bodies[b1], this.bodies[b2])) { //Test the bounding boxes

if (this.detectCollision(this.bodies[b1], this.bodies[b2])) { //If there is a collision, respond to it

this.processCollision();

}

}

}

}

} // for loop iterating through all the bodies for intra-body collision

} // for loop this.iterations

} // end of iterateCollision method

detectCollision( b1, b2 ) {

var minDistance = 10000.0; //Initialize the length of the collision vector to a relatively large value

for (let i = 0; i < b1.edgeCount + b2.edgeCount; i++ ) { //Just a fancy way of iterating through all of the edges of both bodies at once

var e;

if (i < b1.edgeCount) {

e = b1.edges[i];

}

else {

e = b2.edges[i - b1.edgeCount];

}

//This will skip edges that lie totally inside the bodies, as they don't matter.

//The boundary flag has to be set manually and defaults to true

if (e.boundary == false) {

continue;

}

// Calculate the perpendicular to this edge and normalize it

this.axis.x = e.v1.position.y - e.v2.position.y;

this.axis.y = e.v2.position.x - e.v1.position.x;

// Normalise

var len = 1.0/ Math.hypot(this.axis.x, this.axis.y);

this.axis.x *= len;

this.axis.y *= len;

// Project both bodies onto the perpendicular

var dataA = b1.ProjectToAxis( this.axis );

var dataB = b2.ProjectToAxis( this.axis );

var distance = this.IntervalDistance( dataA.min, dataA.max, dataB.min, dataB.max ); //Calculate the distance between the two intervals

// If the intervals don't overlap, return, since there is no collision

if (distance > 0.0) {

return false;

}

else if (Math.abs(distance) < minDistance ) {

minDistance = Math.abs(distance);

// Save collision information for later

this.collisionInfo.normal.x = this.axis.x;

this.collisionInfo.normal.y = this.axis.y;

this.collisionInfo.e = e; //Store the edge, as it is the collision edge

}

}

this.collisionInfo.depth = minDistance;

if (this.collisionInfo.e.parent != b2) { //Ensure that the body containing the collision edge lies in B2 and the one conatining the collision vertex in B1

var temp = b2;

b2 = b1;

b1 = temp;

}

// int Sign = SGN( CollisionInfo.Normal.multiplyVal( B1.Center.minus(B2.Center) ) ); //This is needed to make sure that the collision normal is pointing at B1

var xx = b1.center.x - b2.center.x;

var yy = b1.center.y - b2.center.y;

var mult = this.collisionInfo.normal.x xx + this.collisionInfo.normal.y yy;

// Remember that the line equation is N*( R - R0 ). We choose B2->Center as R0; the normal N is given by the collision normal

if (mult < 0) {

// Revert the collision normal if it points away from B1

this.collisionInfo.normal.x = 0-this.collisionInfo.normal.x;

this.collisionInfo.normal.y = 0-this.collisionInfo.normal.y;

}

var smallestD = 10000.0; //Initialize the smallest distance to a large value

for (let i=0; i<b1.vertexCount; i++) {

// Measure the distance of the vertex from the line using the line equation

// float Distance = CollisionInfo.Normal.multiplyVal( B1.Vertices[I].Position.minus(B2.Center) );

var xx = b1.vertices[i].position.x - b2.center.x;

var yy = b1.vertices[i].position.y - b2.center.y;

var distance = this.collisionInfo.normal.x xx + this.collisionInfo.normal.y yy;

if (distance < smallestD) { //If the measured distance is smaller than the smallest distance reported so far, set the smallest distance and the collision vertex

smallestD = distance;

this.collisionInfo.v = b1.vertices[i];

}

}

return true; //There is no separating axis. Report a collision!

}

processCollision() {

var e1 = this.collisionInfo.e.v1;

var e2 =this.collisionInfo.e.v2;

var collisionVectorX = this.collisionInfo.normal.x * this.collisionInfo.depth;

var collisionVectorY = this.collisionInfo.normal.y * this.collisionInfo.depth;

var t;

if (Math.abs( e1.position.x - e2.position.x ) > Math.abs( e1.position.y - e2.position.y ) ) {

t = ( this.collisionInfo.v.position.x - collisionVectorX - e1.position.x )/( e2.position.x - e1.position.x );

}

else {

t = ( this.collisionInfo.v.position.y - collisionVectorY - e1.position.y )/( e2.position.y - e1.position.y );

}

var lambda = 1.0/( t*t + ( 1 - t )*( 1 - t ) );

var edgeMass = t*e2.parent.mass + ( 1 - t )*e1.parent.mass; //Calculate the mass at the intersection point

var invCollisionMass = 1.0/( edgeMass + this.collisionInfo.v.parent.mass );

var ratio1 = this.collisionInfo.v.parent.mass*invCollisionMass;

var ratio2 = edgeMass*invCollisionMass;

e1.position.x -= collisionVectorX (( 1 - t )ratio1*lambda);

e1.position.y -= collisionVectorY (( 1 - t )ratio1*lambda);

e2.position.x -= collisionVectorX ( t ratio1*lambda);

e2.position.y -= collisionVectorY ( t ratio1*lambda);

this.collisionInfo.v.position.x += collisionVectorX * ratio2;

this.collisionInfo.v.position.y += collisionVectorY * ratio2;

}

IntervalDistance( minA, maxA, minB, maxB) {

if (minA < minB) {

return minB - maxA;

}

else {

return minA - maxB;

}

}

/**

* Used for optimization to test if the bounding boxes of two bodies overlap.

* @param B1

* @param B2

* @return

*/

bodiesOverlap( B1, B2 ) {

return ( B1.minX <= B2.maxX ) && ( B1.minY <= B2.maxY ) && ( B1.maxX >= B2.minX ) && ( B2.maxY >= B1.minY );

}

}




Useful References




12 views0 comments

Recent Posts

See All

p2 naive broadphase

var Broadphase = require('../collision/Broadphase'); module.exports = NaiveBroadphase; /** * Naive broadphase implementation. Does N^2...

sopiro motor constranit

import { Matrix2, Vector2 } from "./math.js"; import { RigidBody } from "./rigidbody.js"; import { Settings } from "./settings.js";...

Comentarios


記事: Blog2_Post
bottom of page