Skip to content

Commit

Permalink
Add gravity example
Browse files Browse the repository at this point in the history
  • Loading branch information
DrA1ex committed Sep 28, 2023
1 parent b1f92a1 commit da55066
Show file tree
Hide file tree
Showing 9 changed files with 502 additions and 225 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ Yet another physics engine implementation.

## Examples

- Particles (WebGL): [link](https://dra1ex.github.io/physics-js/examples/particles)
- Particles (WebGL): [link](https://dra1ex.github.io/physics-js/examples/particles)
- Gravity: [link](https://dra1ex.github.io/physics-js/examples/gravity)
- Ramp: [link](https://dra1ex.github.io/physics-js/examples/ramp)
- Friction: [link](https://dra1ex.github.io/physics-js/examples/friction)
- Tower: [link](https://dra1ex.github.io/physics-js/examples/tower)
Expand Down
17 changes: 11 additions & 6 deletions examples/common/bootstrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,18 +71,19 @@ export class Bootstrap {
#debug = false;
#slowMotion = 1;

get state() {return this.#state;}
get state() {return this.#state; }

get renderer() { return this.#renderer;}
get renderer() { return this.#renderer; }
get solver() { return this.#solver; }

/** @deprecated Use renderer instead */
get canvas() {return this.#renderer.canvas;}
get canvas() { return this.#renderer.canvas; }
/** @deprecated Use renderer instead */
get dpr() {return this.#renderer.dpr;}
get dpr() { return this.#renderer.dpr; }
/** @deprecated Use renderer instead */
get canvasWidth() {return this.#renderer.canvasWidth;}
get canvasWidth() { return this.#renderer.canvasWidth; }
/** @deprecated Use renderer instead */
get canvasHeight() {return this.#renderer.canvasHeight;}
get canvasHeight() { return this.#renderer.canvasHeight; }

get stats() {return {...this.#stats};}
get statsExtra() {return this.#stats.extra;}
Expand Down Expand Up @@ -322,6 +323,10 @@ export class Bootstrap {
}
}

clearShapes() {
this.#drawingShapes.clear();
}

enableHotKeys() {
document.body.onkeydown = (e) => {
if (e.code === "Escape") this.state === State.play ? this.pause() : this.play();
Expand Down
4 changes: 3 additions & 1 deletion examples/common/utils.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import {Vector2} from "../../lib/utils/vector.js";
import {BoundaryBox} from "../../lib/physics/common/boundary.js";
import {PolygonBody} from "../../lib/physics/body/poly.js";
import {RectBody} from "../../lib/physics/body/rect.js";

import * as CommonUtils from "../../lib/utils/common.js";
import {Vector2} from "../../lib/utils/vector.js";

/**
* @param {Vector2} position
Expand Down
12 changes: 12 additions & 0 deletions examples/gravity/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>physics.js: Gravity Demo</title>
<link href="./style.css" rel="stylesheet">
</head>
<body>
<canvas id="canvas"></canvas>
</body>
<script type="module" src="index.js"></script>
</html>
128 changes: 128 additions & 0 deletions examples/gravity/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import {Vector2} from "../../lib/utils/vector.js";
import {BoundaryBox} from "../../lib/physics/common/boundary.js";
import {CircleBody} from "../../lib/physics/body/circle.js";
import {InsetConstraint} from "../../lib/physics/constraint.js";
import {WebglRenderer} from "../../lib/render/renderer/webgl/renderer.js";

import {Bootstrap} from "../common/bootstrap.js";
import * as Params from "../common/params.js";
import * as Utils from "../common/utils.js";

const options = Params.parse()
const BootstrapInstance = new Bootstrap(
new WebglRenderer(document.getElementById("canvas"), options),
Object.assign({solverBias: 0.1}, options)
);

const WorldRect = new BoundaryBox(0, BootstrapInstance.canvasWidth, 0, BootstrapInstance.canvasHeight);
BootstrapInstance.addConstraint(new InsetConstraint(WorldRect, 0.3));
BootstrapInstance.addRect(WorldRect);

const {count, minSize, maxSize, gravity} = Params.parseSettings({
count: {parser: Params.Parser.int, param: "count", default: 100},
minSize: {parser: Params.Parser.int, param: "min_size", default: 5},
maxSize: {parser: Params.Parser.int, param: "max_size", default: 10},
gravity: {parser: Params.Parser.float, param: "gravity", default: 10}
});

for (let i = 0; i < count; i++) {
const size = minSize + Math.random() * Math.max(0, maxSize - minSize);

const position = new Vector2(
BootstrapInstance.canvasWidth * Math.random(),
BootstrapInstance.canvasHeight * Math.random()
);

Utils.clampBodyPosition(position, WorldRect, size);
const body = new CircleBody(position.x, position.y, size / 2, size);

const {renderer} = BootstrapInstance.addRigidBody(
body.setFriction(options.friction)
.setTag("particle")
);

renderer.color = Utils.randomColor(100, 200);
}

BootstrapInstance.enableHotKeys();
BootstrapInstance.run();

function gravityStep() {
BootstrapInstance.clearShapes();
const tree = BootstrapInstance.solver.stepInfo.tree;
if (tree) {
processNode(tree.root);
}
}

function processNode(node) {
if (node.leafs.length > 0) {
const stepDelta = BootstrapInstance.solver.stepInfo.delta;
const count = node.leafs.length;
for (let i = 0; i < count; i++) {
const l1 = node.leafs[i];
const l1Items = l1.items.filter(body => body.tag === "particle");

if (!l1Items.length) continue;

const l1Boundary = BoundaryBox.fromBodies(l1Items);
const mass1 = l1Items.reduce((p, c) => p + c.mass, 0);

for (let j = i + 1; j < count; j++) {
const l2 = node.leafs[j];
const l2Items = l2.items.filter(body => body.tag === "particle");

if (!l2Items.length) continue;

const l2Boundary = BoundaryBox.fromBodies(l2Items);
const mass2 = l2Items.reduce((p, c) => p + c.mass, 0);

const impulse = calculateForce(l1Boundary.center, l2Boundary.center, stepDelta);

const impulse1 = impulse.scaled(mass2);
for (const item of l1Items) applyForce(item, impulse1)

const impulse2 = impulse.scaled(-mass1);
for (const item of l2Items) applyForce(item, impulse2)
}

processNode(l1);
}
} else {
processLeaf(node);
}
}

function processLeaf(leaf) {
const items = leaf.items.filter(body => body.tag === "particle");

const stepDelta = BootstrapInstance.solver.stepInfo.delta;
const count = items.length;
for (let i = 0; i < count; i++) {
for (let j = i + 1; j < count; j++) {
const p1 = items[i];
const p2 = items[j];

const impulse = calculateForce(p1.position, p2.position, stepDelta);

applyForce(p1, impulse.scaled(p2.mass));
applyForce(p2, impulse.scaled(p1.mass).negate());
}
}
}

function calculateForce(p1, p2, stepDelta) {
const delta = p1.delta(p2);
const distSqr = delta.lengthSquared();

const force = -gravity / distSqr;
return delta.scale(force * stepDelta);
}

function applyForce(body, impulse) {
body.velocity.add(impulse.scaled(body.invertedMass));
}

while (true) {
await BootstrapInstance.requestPhysicsFrame(gravityStep);
}
14 changes: 14 additions & 0 deletions examples/gravity/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
html, body {
height: 100%;
}

body {
box-sizing: border-box;
margin: 0;
padding: 1rem;
}

#canvas {
width: 100%;
height: 100%;
}
26 changes: 26 additions & 0 deletions lib/physics/common/boundary.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,30 @@ export class BoundaryBox {
static fromPoints(points) {
return BoundaryBox.empty().updateFromPoints(points);
}

/**
* @param {Body[]} items
* @param {ObjectStack<BoundaryBox>} [pool=null]
* @return {BoundaryBox}
*/
static fromBodies(items, pool = null) {
let left = Number.POSITIVE_INFINITY, right = Number.NEGATIVE_INFINITY;
let top = Number.POSITIVE_INFINITY, bottom = Number.NEGATIVE_INFINITY;

for (const item of items) {
const b = item.boundary;

if (b.left < left) left = b.left;
if (b.right > right) right = b.right;

if (b.top < top) top = b.top;
if (b.bottom > bottom) bottom = b.bottom;
}

if (pool) {
return pool.get().update(left, right, top, bottom);
}

return new BoundaryBox(left, right, top, bottom);
}
}
22 changes: 3 additions & 19 deletions lib/physics/common/tree.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export class SpatialTree {

this.#savedStackPosition = BoundaryBoxPool.save();

this.root = new SpatialLeaf(this.#leafId++, items, SpatialTree.#calcBoundary(items));
this.root = new SpatialLeaf(this.#leafId++, items, BoundaryBox.fromBodies(items, BoundaryBoxPool));
this.#fillLeaf(this.root, items, this.root.boundary);
}

Expand Down Expand Up @@ -85,7 +85,8 @@ export class SpatialTree {
const filteredItems = items.filter(b => !b.static && SpatialTree.#isContainedByBoundary(b, segmentBoundary));

if (filteredItems.length > 0) {
const leaf = new SpatialLeaf(this.#leafId++, filteredItems, SpatialTree.#calcBoundary(filteredItems), segmentBoundary);
const boundaryBox = BoundaryBox.fromBodies(filteredItems, BoundaryBoxPool);
const leaf = new SpatialLeaf(this.#leafId++, filteredItems, boundaryBox, segmentBoundary);
current.addLeaf(leaf);
}
}
Expand All @@ -100,23 +101,6 @@ export class SpatialTree {
BoundaryBoxPool.restore(this.#savedStackPosition);
}

static #calcBoundary(items) {
let left = Number.POSITIVE_INFINITY, right = Number.NEGATIVE_INFINITY;
let top = Number.POSITIVE_INFINITY, bottom = Number.NEGATIVE_INFINITY;

for (const item of items) {
const b = item.boundary;

if (b.left < left) left = b.left;
if (b.right > right) right = b.right;

if (b.top < top) top = b.top;
if (b.bottom > bottom) bottom = b.bottom;
}

return BoundaryBoxPool.get().update(left, right, top, bottom);
}

static #isContainedByBoundary(body, boundary) {
const {x, y} = body.position;

Expand Down
Loading

0 comments on commit da55066

Please sign in to comment.