@dominicstop/utils
Version:
Yet another event emitter written in typescript.
264 lines (263 loc) • 10.1 kB
JavaScript
import { Vector2D } from "../geometry";
import { DampingForce } from "./DampingForce";
import { isSomeParticleForce } from "./SomeParticleForce";
import { isSomeSystemForce } from "./SomeSystemForce";
export class PhysicsEngine {
constructor() {
this.particles = [];
this.particleMetadataMap = {};
this.particleForces = [];
this.systemForces = [];
this.worldBounds = null;
this.gravity = Vector2D.zero;
this.restitutionCoefficient = 0.6;
this.collisionIterations = 1;
this.dampingFactor = 0.98;
if (this.dampingFactor < 1) {
const dampingForce = new DampingForce(this.dampingFactor);
this.addForce(dampingForce);
}
;
}
addParticle(particle) {
this.particles.push(particle);
}
;
removeParticle(particleOrId) {
const id = typeof particleOrId === "string"
? particleOrId
: particleOrId.id;
const index = this.particles.findIndex(p => p.id === id);
if (index <= -1) {
return false;
}
;
this.particles.splice(index, 1);
return true;
}
;
getParticleById(id) {
return this.particles.find(p => p.id === id);
}
;
addForce(force) {
if (isSomeParticleForce(force)) {
this.particleForces.push(force);
}
else if (isSomeSystemForce(force)) {
this.systemForces.push(force);
}
;
}
;
removeForce(force) {
const forceList = (() => {
if (isSomeParticleForce(force)) {
return this.particleForces;
}
;
if (isSomeSystemForce(force)) {
return this.systemForces;
}
;
return undefined;
})();
if (forceList == null) {
return false;
}
;
const index = forceList.indexOf(force);
if (index !== -1) {
forceList.splice(index, 1);
return true;
}
;
return false;
}
;
update(deltaTime) {
for (const particle of this.particles) {
particle.resetAcceleration();
if (!particle.isStatic) {
particle.applyForce(this.gravity.multipliedByScalar(particle.mass));
}
}
for (const force of this.systemForces) {
force.applyToAll(this.particles);
}
for (const force of this.particleForces) {
for (const particle of this.particles) {
force.apply(particle);
}
}
for (const particle of this.particles) {
particle.update(deltaTime);
if (this.worldBounds) {
this.handleBoundaryConditions(particle);
}
;
}
;
for (let i = 0; i < this.collisionIterations; i++) {
this.resolveCollisions();
}
;
}
resolveCollisions() {
// loop through all possible pairs of particles
// e.g. `[01, 02, 03, ..., 41, 42...]`
for (let i = 0; i < this.particles.length; i++) {
const particleA = this.particles[i];
for (let j = i + 1; j < this.particles.length; j++) {
const particleB = this.particles[j];
if (particleA.isStatic && particleB.isStatic)
continue;
// check if the two particles are colliding,
// only continue if they have collision
const hasCollision = particleA.isCollidingWithOther(particleB);
if (!hasCollision)
continue;
// calculate the amount of overlap
const overlapVector = particleA.computeOverlapVectorWith(particleB);
if (overlapVector.isZero)
continue;
// calculate the total inverse mass (used for distributing correction)
// skip if total is 0 (i.e. to avoid division by zero)
const totalInverseMass = particleA.inverseMass + particleB.inverseMass;
if (totalInverseMass === 0)
continue;
// compute adj amount
// i.e. how much each particle should move to resolve the overlap
const correctionVector = overlapVector.dividedByScalar(totalInverseMass);
// add position correction to `particleA` (if needed)
particleA.setPosition((() => {
if (particleA.isStatic) {
return particleA.position;
}
;
// * remember: `correctionVector` is the total overlap that needs to be
// resolved/min.
//
// * each particle should move proportionally to its inverse mass (in other words:
// lighter particles move more).
//
// * so scale the correction vector by `particleA.inverseMass`
// to get particleA's share of the movement.
//
const positionAdjScaled = correctionVector.multipliedByScalar(particleA.inverseMass);
return particleA.position.addedWithOther(positionAdjScaled);
})());
// add position correction for `particleB`
particleB.setPosition((() => {
if (particleB.isStatic) {
return particleB.position;
}
;
const positionAdjustmentB = correctionVector.multipliedByScalar(particleB.inverseMass);
return particleB.position.subtractedWithOther(positionAdjustmentB);
})());
// get delta of particle velocity
// calculate relative velocity between the two particles
const relativeVelocity = particleA.velocity.subtractedWithOther(particleB.velocity);
// normalize to get the direction of the collision
const collisionDirection = overlapVector.normalized;
// scale/re-projecr relative velocity towards collision
// direction (collision normal)
const velocityAlongCollisionDirection = relativeVelocity.dotProductWithOtherVector(collisionDirection);
// skip if particles are moving apart
if (velocityAlongCollisionDirection > 0)
continue;
// compute velocity
const impulse = (() => {
// calculate impulse scalar using restitution (bounciness/elasticity)
const restitution = this.restitutionCoefficient;
const impulseAmount = -(1 + restitution) * velocityAlongCollisionDirection / totalInverseMass;
// calculate the impulse vector (direction + amount)
return collisionDirection.multipliedByScalar(impulseAmount);
})();
// apply impulse to `particleA`'s velocity
particleA.setVelocity((() => {
if (particleA.isStatic) {
return particleA.velocity;
}
;
const velocityScaled = impulse.multipliedByScalar(particleA.inverseMass);
return particleA.velocity.addedWithOther(velocityScaled);
})());
// apply impulse to `particleB`'s velocity
particleB.setVelocity((() => {
if (particleB.isStatic) {
return particleB.velocity;
}
;
const velocityScaled = impulse.multipliedByScalar(particleB.inverseMass);
return particleB.velocity.subtractedWithOther(velocityScaled);
})());
}
}
}
handleBoundaryConditions(particle) {
if (!this.worldBounds || particle.isStatic)
return;
const bounds = this.worldBounds;
const pos = particle.position;
const vel = particle.velocity;
let bounced = false;
if (pos.dx < bounds.minX) {
pos.dx = bounds.minX;
vel.dx *= -this.restitutionCoefficient;
bounced = true;
}
else if (pos.dx > bounds.maxX) {
pos.dx = bounds.maxX;
vel.dx *= -this.restitutionCoefficient;
bounced = true;
}
if (pos.dy < bounds.minY) {
pos.dy = bounds.minY;
vel.dy *= -this.restitutionCoefficient;
bounced = true;
}
else if (pos.dy > bounds.maxY) {
pos.dy = bounds.maxY;
vel.dy *= -this.restitutionCoefficient;
bounced = true;
}
if (bounced) {
particle.setPosition(pos);
particle.setVelocity(vel);
}
}
clear() {
this.particles = [];
this.particleForces = [];
this.systemForces = [];
}
getTotalKineticEnergy() {
return this.particles.reduce((sum, p) => sum + p.getKineticEnergy(), 0);
}
logState() {
for (const p of this.particles) {
console.log(`Particle ${p.id}: pos=(${p.position.dx.toFixed(2)}, ${p.position.dy.toFixed(2)}), vel=(${p.velocity.dx.toFixed(2)}, ${p.velocity.dy.toFixed(2)})`);
}
}
getParticleCount() {
return this.particles.length;
}
setGravity(gravity) {
this.gravity = gravity;
}
setWorldBounds(bounds) {
this.worldBounds = bounds;
}
setRestitutionCoefficient(restitution) {
this.restitutionCoefficient = restitution;
}
setCollisionIterations(iterations) {
this.collisionIterations = iterations;
}
checkIfAllParticlesAtRest() {
return this.particles.every(p => p.checkIsAtRest());
}
;
}