^^ Clicking the button in the screen capture demo just puts a new sphere and impostor with springs.
It wasn’t really planning to build a physics engine - I just wanted a force directed graph (Force-directed graph drawing - Wikipedia), so it’s not a physics engine like how you think of the existing ones. I wanted to be able to add additional forces at run time as plugins, the main one being a body gravity force between the nodes which are also linked together with springs.
I found it quite funny (and smart) that ammo.js is from “Avoided Making My Own”. I just put a “Why” in front: brianzinn/wammo: force directed physics engine (github.com). Basically the forces work all together to alter the velocity of the physics impostor. I didn’t add a collision force, but quadtree/octree is actually simpler than the barnes-hut tree as it needs to track center of gravity in each tree node.
The algorithms are not that complex and the solver uses each frame render to improve and work towards equilibrium in the physical world. The gravity (attraction/repulsion) forces are coming FROM each node to all other nodes, so it’s not a world gravity like the other physics engines. You could simulate a planetary system or layout a graph of objects. Anyway, it’s just a very basic start. If you want to try it out the engine the source is here in a very alpha stage danger danger (it’s just 2D right now, but will be easy to upgrade to include 3D as I’m using vector math everywhere on the position and velocity and physics). Perhaps I am being super naive as well, but I think adding collisions and a “world” gravity is also not out of the question to add dynamically.
I quickly hacked together a babylon plugin (I will also update our docs here!):
const intraNodeGravity = new Vector3(0, -80, 0);
scene.enablePhysics(gravityVector, new WammoPlugin());
What’s next? Firstly when the graph does reach equilibrium it drifts off into outer space from the kinetic energy, so I want to add a centering plugin. That will be super easy to just push everything centered at a point with little impulses. I’ve built quite a few solvers that do Gradient Ascent Gradient descent - Wikipedia that optimize over many solutions and many variables, so it would maybe be cool to run multiple solutions simultaneously and choose the best simulation, but I think it would be too CPU intensive - I already have good Pseudo-RNG code in the project for deterministic testing. This demo is 60FPS, but there needs to be a lot of of optimization as it’s in a more academic state - need to make sure there is a lot less GC pressure and re-use of data structures across iterations. The tree has to be re-built every frame (since objects are moving inbetween frames).
Here is the WammoPlugin
to connect the wammo physics engine to babylon:
import { AbstractMesh, IMotorEnabledJoint, IPhysicsEnginePlugin, Nullable, PhysicsImpostor, PhysicsImpostorJoint, PhysicsJoint, Quaternion, Vector3 } from "@babylonjs/core";
import { PhysicsRaycastResult } from "@babylonjs/core/Physics/physicsRaycastResult";
import { Graph, GraphCreateOptions, IVector, PhysicsBody, solver, SolverApi, springForce, Vector2, Vector2Impl } from 'wammo';
export class WammoPlugin implements IPhysicsEnginePlugin {
private _physicsBodyIndex = 0;
private _graph: Graph<IVector<Vector2>, number>;
public world: SolverApi<IVector<Vector2>, number>;
public name: string = "WammoPlugin";
// 60 FPS
private _fixedTimeStep: number = 1 / 60;
private _raycastResult: PhysicsRaycastResult;
private _gravity: number = -2;
private _physicsBodysToRemoveAfterStep: any[] = []; // TODO: remove `any`
constructor(public useDeltaForSimulatorStep: boolean = true, options?: GraphCreateOptions<IVector<Vector2>, number>) {
this._raycastResult = new PhysicsRaycastResult();
this._graph = new Graph<IVector<Vector2>, number>(options);
const springFn = springForce(
options?.defaultSpringCoefficient ?? 0.9,
options?.defaultSpringLength ?? 10
);
this.world = solver();
this.world.loadGraph(this._graph, Vector2Impl);
this.world.addSpringForce('spring-fn', springFn);
}
public initialize(): void {
console.log('Initialize called in PLUGIN??');
}
public executeStep(delta: number, impostors: Array<PhysicsImpostor>): void {
// NOTE:
for (let impostor of impostors) {
// Update physics world objects to match babylon world
impostor.beforeStep();
}
// const stepDelta =
this.world.step(this.useDeltaForSimulatorStep ? delta : this._fixedTimeStep);
for (var mainImpostor of impostors) {
// After physics update make babylon world objects match physics world objects
mainImpostor.afterStep();
}
}
public setGravity(gravity: Vector3): void {
const g = gravity.length();
// console.warn(`Gravity ${g} forced to negative to bring object together`);
this._gravity = -g;
}
public setTimeStep(timeStep: number) {
this._fixedTimeStep = timeStep;
}
public getTimeStep(): number {
return this._fixedTimeStep;
}
public applyImpulse(impostor: PhysicsImpostor, force: Vector3, contactPoint: Vector3) {
console.log('applyImpulse', impostor, force, contactPoint);
// impostor.physicsBody.applyImpulse(impulse, worldPoint);
}
public applyForce(impostor: PhysicsImpostor, force: Vector3, contactPoint: Vector3) {
// var worldPoint = new this.BJSCANNON.Vec3(contactPoint.x, contactPoint.y, contactPoint.z);
// var impulse = new this.BJSCANNON.Vec3(force.x, force.y, force.z);
console.log('apply force');
// impostor.physicsBody.applyForce(impulse, worldPoint);
}
/**
* TODO: this does not look necessary at all...
*/
private _updatePhysicsBodyTransformation(impostor: PhysicsImpostor) {
var object = impostor.object;
if (!object.getBoundingInfo()) {
return;
}
const physicsBody: PhysicsBody<IVector<Vector2>, number> = impostor.physicsBody;
//Now update the physics body from the impostor object
// physicsBody.position.vector.x = impostor.object.position.x;
// physicsBody.position.vector.y = impostor.object.position.y;
console.log(`Set physics body from impostor object: ${physicsBody.id} to ${JSON.stringify(physicsBody.position.vector)}`);
// physicsBody.quaternion.....
}
public generatePhysicsBody(impostor: PhysicsImpostor) {
// When calling forceUpdate generatePhysicsBody is called again, ensure that the updated body does not instantly collide with removed body
// this._removeMarkedPhysicsBodiesFromWorld();
//parent-child relationship. Does this impostor have a parent impostor?
if (impostor.parent) {
console.warn('Parent impostors not supported');
return;
}
//should a new body be created for this impostor?
if (impostor.isBodyInitRequired()) {
// var extendSize = impostor.getObjectExtendSize();
// switch (impostor.type) {
const physicsBody = new PhysicsBody(this._physicsBodyIndex++, new Vector2Impl(impostor.object.position.x, impostor.object.position.y), new Vector2Impl(), new Vector2Impl());
//unregister events, if body is being changed
var oldBody = impostor.physicsBody;
if (oldBody) {
console.warn('body already existed???');
// this.removePhysicsBody(impostor);
}
const mass: number = impostor.getParam("mass");
if (mass) {
console.log(`object '${(impostor.object as any).name ?? 'no name'}' impostor ${physicsBody.id} mass ->`, mass);
physicsBody.mass = mass;
}
this.world.addBody(physicsBody);
impostor.physicsBody = physicsBody;
console.log('physicsBody added to simulation:', physicsBody, impostor);
if (oldBody) {
// TODO: see if we should copy ie: velocity/force from original?
}
this._updatePhysicsBodyTransformation(impostor);
}
}
public removePhysicsBody(impostor: PhysicsImpostor) {
console.log('removePhysicsBody', impostor);
// Only remove the physics body after the physics step to avoid disrupting internal state
if (this._physicsBodysToRemoveAfterStep.indexOf(impostor.physicsBody) === -1) {
this._physicsBodysToRemoveAfterStep.push(impostor.physicsBody);
}
}
private _removeMarkedPhysicsBodiesFromWorld(): void {
if (this._physicsBodysToRemoveAfterStep.length > 0) {
this._physicsBodysToRemoveAfterStep.forEach((physicsBody) => {
console.log('want to remove:', physicsBody)
// this.world.remove(physicsBody);
});
this._physicsBodysToRemoveAfterStep = [];
}
}
public generateJoint(impostorJoint: PhysicsImpostorJoint) {
var mainBody = impostorJoint.mainImpostor.physicsBody;
var connectedBody = impostorJoint.connectedImpostor.physicsBody;
if (!mainBody || !connectedBody) {
console.warn('Joint not connected to 2 bodies?');
return;
}
const fromId = (mainBody as PhysicsBody<IVector<Vector2>, number>).id;
const toId = (connectedBody as PhysicsBody<IVector<Vector2>, number>).id;
const nativeParams = impostorJoint.joint.jointData.nativeParams;
console.error(`world.addSpring(..) ${fromId} -> ${toId}.`, nativeParams);
// TODO: have a method "createLinkAfterStep?"
// this._graph.addLink(
// fromId,
// toId,
// nativeParams?.length,
// nativeParams?.coefficient,
// );
// xx this._graphChangeAfterInit = true;
this.world.addSpring(fromId, toId, nativeParams?.length, nativeParams?.coefficient);
}
public removeJoint(impostorJoint: PhysicsImpostorJoint) {
// if (impostorJoint.joint.type !== PhysicsJoint.SpringJoint) {
// this.world.removeConstraint(impostorJoint.joint.physicsJoint);
// } else {
// impostorJoint.mainImpostor.unregisterAfterPhysicsStep((<SpringJointData>impostorJoint.joint.jointData).forceApplicationCallback);
// }
var mainBody = impostorJoint.mainImpostor.physicsBody;
var connectedBody = impostorJoint.connectedImpostor.physicsBody;
if (!mainBody || !connectedBody) {
console.warn('WammoPlugin: removeJoint not connected to 2 bodies?');
return;
}
const fromId = (mainBody as PhysicsBody<IVector<Vector2>, number>).id;
const toId = (connectedBody as PhysicsBody<IVector<Vector2>, number>).id;
console.log('WammoPlugin: : removeJoint', fromId, toId, impostorJoint);
// impostorJoint.joint is babylon object, so we don't want our physics engine to know about it...
this.world.removeSpring(fromId, toId, undefined, impostorJoint.joint);
}
/**
* Sets the physics body position/rotation from the babylon mesh's position/rotation.
* NOTE: called in AFTER step, so once our simulation/solver has completed an iteranion.
*
* @param impostor imposter containing the physics body and babylon object
*/
public setTransformationFromPhysicsBody(impostor: PhysicsImpostor) {
const physicsBody: PhysicsBody<IVector<Vector2>, number> = impostor.physicsBody;
// y-up
impostor.object.position.set(physicsBody.position.vector.x, 2, physicsBody.position.vector.y);
// if (impostor.object.rotationQuaternion) {
// const q = impostor.physicsBody.quaternion;
// impostor.object.rotationQuaternion.set(q.x, q.y, q.z, q.w);
// }
}
/**
* Sets the babylon object's position/rotation from the physics body's position/rotation.
* Called in physicsImpostor BEFORE step. By leaving this empty we are disallowing position to be set ie: manually?
*
* @param impostor imposter containing the physics body and babylon object
* @param newPosition new position
* @param newRotation new rotation
*/
public setPhysicsBodyTransformation(impostor: PhysicsImpostor, newPosition: Vector3, newRotation: Quaternion) {
// console.log('physics body:', impostor.physicsBody, newPosition);
const physicsBody: PhysicsBody<IVector<Vector2>, number> = impostor.physicsBody;
physicsBody.position.vector.x = newPosition.x;
physicsBody.position.vector.y = newPosition.z;
// impostor.physicsBody.quaternion.set(newRotation.x, newRotation.y, newRotation.z, newRotation.w);
}
public isSupported(): boolean {
if (navigator.userAgent.toLowerCase().indexOf('chrome') > -1) {
// colors from logo
const topBlue = '#3B789A';
const sideBlue = '#405A68';
const bottomBlue = '#61B0E1';
const descriptions: string[] = ['Wammo', 'ManyBody', 'Spring'];
const url = 'http://github.com/brianzinn/wammo/';
const length = url.length + 9;
const args = ['']
args[0] += `%c %c %c ♥ %c ${' '.padEnd(url.length)} %c ♥ %c %c \n`
args.push(...[
`background: ${topBlue}; padding:5px 0;`,
`background: ${sideBlue}; padding:5px 0;`,
`color: red; background: ${bottomBlue}; padding:5px 0;`,
`background: ${topBlue}; padding:5px 0;`,
`color: red; background: ${bottomBlue}; padding:5px 0;`,
`background: ${sideBlue}; padding:5px 0;`,
`background: ${topBlue}; padding:5px 0;`,
]);
descriptions.forEach((d, i) => {
args[0] += `%c %c %c ${(i > 0 ? `- ${d}` : d).padEnd(length, ' ')}%c %c \n`;
args.push(...[
`background: ${topBlue}; padding:5px 0;`,
`background: ${sideBlue}; padding:5px 0;`,
`color: ${bottomBlue}; background: #030307; padding:5px 0;`,
`background: ${sideBlue}; padding:5px 0;`,
`background: ${topBlue}; padding:5px 0;`,
])
});
args[0] += ` %c %c ♥ %c ${url} %c ♥ %c %c \n\n`
args.push(...[
`background: ${sideBlue}; padding:5px 0;`,
`color: red; background: ${bottomBlue}; padding:5px 0;`,
`background: ${topBlue}; padding:5px 0;`,
`color: red; background: ${bottomBlue}; padding:5px 0;`,
`background: ${sideBlue}; padding:5px 0;`,
`background: ${topBlue}; padding:5px 0;`,
]);
window.console.log(...args);
}
else if (window.console) {
window.console.log(`Wammo Physics engine`);
}
return true;
}
/**
* Sets the linear velocity of the physics body
* @param impostor imposter to set the velocity on
* @param velocity velocity to set
*/
public setLinearVelocity(impostor: PhysicsImpostor, velocity: Vector3) {
// impostor.physicsBody.velocity.set(velocity.x, velocity.y, velocity.z);
console.log('setLinearVelocity');
}
/**
* Sets the angular velocity of the physics body
* @param impostor imposter to set the velocity on
* @param velocity velocity to set
*/
public setAngularVelocity(impostor: PhysicsImpostor, velocity: Vector3) {
console.log('setAngularVelocity');
// impostor.physicsBody.angularVelocity.set(velocity.x, velocity.y, velocity.z);
}
/**
* gets the linear velocity
* @param impostor imposter to get linear velocity from
* @returns linear velocity
*/
public getLinearVelocity(impostor: PhysicsImpostor): Nullable<Vector3> {
console.log('getLinearVelocity');
var v = impostor.physicsBody.velocity;
if (!v) {
return null;
}
return new Vector3(v.x, v.y, v.z);
}
/**
* gets the angular velocity
* @param impostor imposter to get angular velocity from
* @returns angular velocity
*/
public getAngularVelocity(impostor: PhysicsImpostor): Nullable<Vector3> {
console.log('getAngularVelocity');
var v = impostor.physicsBody.angularVelocity;
if (!v) {
return null;
}
return new Vector3(v.x, v.y, v.z);
}
/**
* Sets the mass of physics body
* @param impostor imposter to set the mass on
* @param mass mass to set
*/
public setBodyMass(impostor: PhysicsImpostor, mass: number) {
console.log('setting body mass', mass);
(impostor.physicsBody as PhysicsBody<IVector<Vector2>, number>).mass = mass;
// impostor.physicsBody.updateMassProperties();
}
/**
* Gets the mass of the physics body
* @param impostor imposter to get the mass from
* @returns mass
*/
public getBodyMass(impostor: PhysicsImpostor): number {
const mass = (impostor.physicsBody as PhysicsBody<IVector<Vector2>, number>).mass;
// console.log(`getBodyMass -> ${mass}`);
return mass;
}
/**
* Gets friction of the impostor
* @param impostor impostor to get friction from
* @returns friction value
*/
public getBodyFriction(impostor: PhysicsImpostor): number {
// console.log('getBodyFriction');
// return 0;
// return impostor.physicsBody.material.friction;
return 0.5;
}
/**
* Sets friction of the impostor
* @param impostor impostor to set friction on
* @param friction friction value
*/
public setBodyFriction(impostor: PhysicsImpostor, friction: number) {
// impostor.physicsBody.material.friction = friction;
console.warn('set body friction not supported')
}
/**
* Gets restitution of the impostor
* @param impostor impostor to get restitution from
* @returns restitution value
*/
public getBodyRestitution(impostor: PhysicsImpostor): number {
// console.log('getBodyRestitution');
return 0.5; // not supported
// return impostor.physicsBody.material.restitution;
}
/**
* Sets resitution of the impostor
* @param impostor impostor to set resitution on
* @param restitution resitution value
*/
public setBodyRestitution(impostor: PhysicsImpostor, restitution: number) {
console.warn('set body restitution not supported');
// impostor.physicsBody.material.restitution = restitution;
}
/**
* Sleeps the physics body and stops it from being active
* @param impostor impostor to sleep
*/
public sleepBody(impostor: PhysicsImpostor) {
impostor.physicsBody.sleep();
}
/**
* Activates the physics body
* @param impostor impostor to activate
*/
public wakeUpBody(impostor: PhysicsImpostor) {
impostor.physicsBody.wakeUp();
}
/**
* Updates the distance parameters of the joint
* @param joint joint to update
* @param maxDistance maximum distance of the joint
* @param minDistance minimum distance of the joint
*/
public updateDistanceJoint(joint: PhysicsJoint, maxDistance: number) {
console.log('updateDistanceJoint');
joint.physicsJoint.distance = maxDistance;
}
/**
* Sets a motor on the joint
* @param joint joint to set motor on
* @param speed speed of the motor
* @param maxForce maximum force of the motor
* @param motorIndex index of the motor
*/
public setMotor(joint: IMotorEnabledJoint, speed?: number, maxForce?: number, motorIndex?: number) {
console.log('setMotor');
if (!motorIndex) {
joint.physicsJoint.enableMotor();
joint.physicsJoint.setMotorSpeed(speed);
if (maxForce) {
this.setLimit(joint, maxForce);
}
}
}
/**
* Sets the motors limit
* @param joint joint to set limit on
* @param upperLimit upper limit
* @param lowerLimit lower limit
*/
public setLimit(joint: IMotorEnabledJoint, upperLimit: number, lowerLimit?: number) {
console.log('setLimit');
joint.physicsJoint.motorEquation.maxForce = upperLimit;
joint.physicsJoint.motorEquation.minForce = lowerLimit === void 0 ? -upperLimit : lowerLimit;
}
/**
* Syncs the position and rotation of a mesh with the impostor (used by Inspector debug Helper)
* @param mesh mesh to sync
* @param impostor impostor to update the mesh with
*/
public syncMeshWithImpostor(mesh: AbstractMesh, impostor: PhysicsImpostor) {
var body: PhysicsBody<IVector<Vector2>, number> = impostor.physicsBody;
// console.log(`syncMesh to ${JSON.stringify(body.position.vector)}`);
mesh.position.x = body.position.vector.x;
// y-up
// mesh.position.y = body.position.vector.y;
mesh.position.z = body.position.vector.y;
mesh.position.y = 2; // TODO: fix
// if (mesh.rotationQuaternion) {
// mesh.rotationQuaternion.x = body.quaternion.x;
// mesh.rotationQuaternion.y = body.quaternion.y;
// mesh.rotationQuaternion.z = body.quaternion.z;
// mesh.rotationQuaternion.w = body.quaternion.w;
// }
}
/**
* Gets the radius of the impostor
* @param impostor impostor to get radius from
* @returns the radius
*/
public getRadius(impostor: PhysicsImpostor): number {
// this is called by the physics helper (inspector debug)
console.warn('assuming sphere for object size');
const boundingInfo = impostor.object.getBoundingInfo();
return boundingInfo.boundingSphere.radius;
}
/**
* Gets the box size of the impostor
* @param impostor impostor to get box size from
* @param result the resulting box size
*/
public getBoxSizeToRef(impostor: PhysicsImpostor, result: Vector3): void {
console.log('getBoxSizeToRef');
var shape = impostor.physicsBody.shapes[0];
result.x = shape.halfExtents.x * 2;
result.y = shape.halfExtents.y * 2;
result.z = shape.halfExtents.z * 2;
}
public dispose() {
console.log('disposing Wammo Physics plugin.');
}
/**
* Does a raycast in the physics world
* @param from when should the ray start?
* @param to when should the ray end?
* @returns PhysicsRaycastResult
*/
public raycast(from: Vector3, to: Vector3): PhysicsRaycastResult {
console.warn('raycast not supported', from, to);
return this._raycastResult;
}
}
Would be really interested if anybody actually tried it out (or read this far!). I think the node package is broken - I am working off a linked package locally.