Stress-testing the physics engine

This commit is contained in:
Peaceultime 2024-06-11 01:19:48 +02:00
parent 6d20041842
commit ca7b1d1956
7 changed files with 192 additions and 40 deletions

BIN
bun.lockb

Binary file not shown.

View File

@ -14,6 +14,7 @@
}, },
"dependencies": { "dependencies": {
"@types/three": "^0.165.0", "@types/three": "^0.165.0",
"stats.js": "^0.17.0",
"three": "^0.165.0", "three": "^0.165.0",
"three-mesh-bvh": "^0.7.5" "three-mesh-bvh": "^0.7.5"
} }

View File

@ -14,30 +14,52 @@ export default class Asset implements AABB
mat: Three.Matrix4; mat: Three.Matrix4;
layer: number; layer: number;
selected: boolean = false; #selected = false;
#dirty = false;
static instance = new Three.InstancedMesh(CONST.QUAD, new Three.MeshBasicMaterial({ color: new Three.Color( 0xffffff ) }), 2**14); //@ts-ignore
#aabb: AABB;
static instance = new Three.InstancedMesh(CONST.QUAD, new Three.MeshBasicMaterial({ color: new Three.Color( 0xffffff ) }), 1000000);
constructor(mat?: Three.Matrix4, layer?: number) constructor(mat?: Three.Matrix4, layer?: number)
{ {
this.mat = mat ?? new Three.Matrix4(); this.mat = mat ?? new Three.Matrix4();
this.#updateAABB();
this.layer = layer ?? 0; this.layer = layer ?? 0;
} }
get x1() #updateAABB() {
{ const aabb = { x1: -0.5, x2: 0.5, y1: -0.5, y2: 0.5 };
return this.mat.elements[0] * (-0.5) + this.mat.elements[4] * (-0.5) + this.mat.elements[12] * (1 / ( this.mat.elements[ 3 ] * -0.5 + + this.mat.elements[ 15 ] ));
const e = this.mat.elements;
const x1 = aabb.x1 * e[0] + aabb.y1 * e[4] + e[12];
const x2 = aabb.x2 * e[0] + aabb.y2 * e[4] + e[12];
const y1 = aabb.x1 * e[1] + aabb.y1 * e[5] + e[13];
const y2 = aabb.x2 * e[1] + aabb.y2 * e[5] + e[13];
const x3 = aabb.x2 * e[0] + aabb.y1 * e[4] + e[12];
const x4 = aabb.x1 * e[0] + aabb.y2 * e[4] + e[12];
const y3 = aabb.x2 * e[1] + aabb.y1 * e[5] + e[13];
const y4 = aabb.x1 * e[1] + aabb.y2 * e[5] + e[13];
this.#aabb = {
x1: Math.min(x1, x2, x3, x4),
x2: Math.max(x1, x2, x3, x4),
y1: Math.min(y1, y2, y3, y4),
y2: Math.max(y1, y2, y3, y4)
};
} }
get y1() get x1() {
{ return this.#aabb.x1;
return this.mat.elements[1] * (-0.5) + this.mat.elements[5] * (-0.5) + this.mat.elements[13] * (1 / ( this.mat.elements[ 7 ] * -0.5 + + this.mat.elements[ 15 ] ));
} }
get x2() get y1() {
{ return this.#aabb.y1;
return this.mat.elements[0] * (0.5) + this.mat.elements[4] * (0.5) + this.mat.elements[12] * (1 / ( this.mat.elements[ 3 ] * 0.5 + + this.mat.elements[ 15 ] ));
} }
get y2() get x2() {
{ return this.#aabb.x2;
return this.mat.elements[1] * (0.5) + this.mat.elements[5] * (0.5) + this.mat.elements[13] * (1 / ( this.mat.elements[ 7 ] * 0.5 + + this.mat.elements[ 15 ] )); }
get y2() {
return this.#aabb.y2;
} }
move(x: number, y: number): Asset move(x: number, y: number): Asset
{ {
@ -48,6 +70,8 @@ export default class Asset implements AABB
this.mat.compose(_position, _rotation, _scale); this.mat.compose(_position, _rotation, _scale);
this.#updateAABB();
return this; return this;
} }
moveTo(x: number, y: number): Asset moveTo(x: number, y: number): Asset
@ -59,6 +83,8 @@ export default class Asset implements AABB
this.mat.compose(_position, _rotation, _scale); this.mat.compose(_position, _rotation, _scale);
this.#updateAABB();
return this; return this;
} }
rotate(rad: number): Asset rotate(rad: number): Asset
@ -71,6 +97,8 @@ export default class Asset implements AABB
this.mat.compose(_position, _rotation, _scale); this.mat.compose(_position, _rotation, _scale);
this.#updateAABB();
return this; return this;
} }
rotateTo(rad: number): Asset rotateTo(rad: number): Asset
@ -83,6 +111,8 @@ export default class Asset implements AABB
this.mat.compose(_position, _rotation, _scale); this.mat.compose(_position, _rotation, _scale);
this.#updateAABB();
return this; return this;
} }
scale(x: number, y: number): Asset scale(x: number, y: number): Asset
@ -94,6 +124,8 @@ export default class Asset implements AABB
this.mat.compose(_position, _rotation, _scale); this.mat.compose(_position, _rotation, _scale);
this.#updateAABB();
return this; return this;
} }
scaleTo(x: number, y: number): Asset scaleTo(x: number, y: number): Asset
@ -105,6 +137,8 @@ export default class Asset implements AABB
this.mat.compose(_position, _rotation, _scale); this.mat.compose(_position, _rotation, _scale);
this.#updateAABB();
return this; return this;
} }
shapeTo(x1: number, y1: number, x2: number, y2: number, x3: number, y3: number, x4: number, y4: number): Asset shapeTo(x1: number, y1: number, x2: number, y2: number, x3: number, y3: number, x4: number, y4: number): Asset

View File

@ -207,4 +207,8 @@ interface LinkedElmt<T>
next: LinkedElmt<T> | null; next: LinkedElmt<T> | null;
prev: LinkedElmt<T> | null; prev: LinkedElmt<T> | null;
}
export function clamp(x: number, min: number, max: number): number
{
return x > max ? max : x < min ? min : x;
} }

View File

@ -3,52 +3,106 @@ import Renderer from './renderer/renderer.class';
import Asset from './assets/asset.class'; import Asset from './assets/asset.class';
import Quadtree from './physics/quadtree.class'; import Quadtree from './physics/quadtree.class';
import { FRUSTUMSIZE } from './consts'; import { FRUSTUMSIZE } from './consts';
import { clamp } from './common';
performance.mark("start");
Renderer.init(); Renderer.init();
const quad = new Quadtree({x1: -FRUSTUMSIZE * Renderer.aspect, x2: FRUSTUMSIZE * Renderer.aspect, y1: -FRUSTUMSIZE, y2: FRUSTUMSIZE}); const quad = new Quadtree({x1: -FRUSTUMSIZE * Renderer.aspect, x2: FRUSTUMSIZE * Renderer.aspect, y1: -FRUSTUMSIZE, y2: FRUSTUMSIZE});
window.Asset = Asset; performance.mark("init");
const assets: Asset[] = []; const assets: Asset[] = [];
for(let i = 0; i < 1; i++) for(let i = 0; i < 10000; i++)
{ {
assets[i] = new Asset(new THREE.Matrix4(), 1); assets[i] = new Asset(new THREE.Matrix4(), 1);
assets[i] assets[i]
.move((Math.random() - 0.5) * FRUSTUMSIZE * Renderer.aspect, (Math.random() - 0.5) * FRUSTUMSIZE) .move((Math.random() - 0.5) * FRUSTUMSIZE * Renderer.aspect, (Math.random() - 0.5) * FRUSTUMSIZE)
.rotate(Math.random() * Math.PI * 2) .rotate(Math.random() * Math.PI * 2)
.scale(Math.random() * 1.5 + 0.1, Math.random() * 1.5 + 0.1) .scale(Math.random() * 0.05 + 0.005, Math.random() * 0.05 + 0.005)
Asset.instance.setMatrixAt(i, assets[i].mat); Asset.instance.setMatrixAt(i, assets[i].mat);
quad.insert(assets[i]); quad.insert(assets[i]);
console.log(assets[i]);
console.log("{ %s, %s, %s, %s }", assets[i].x1, assets[i].y1, assets[i].x2, assets[i].y2);
} }
performance.mark("ready");
const highlightBox = new THREE.Box3();
const highlightHelper = new THREE.Box3Helper(highlightBox, 0xffffff);
highlightHelper.visible = false;
let overallTimer = 0, overallSamples = 0;
Asset.instance.count = assets.length; Asset.instance.count = assets.length;
Asset.instance.computeBoundingBox(); Asset.instance.computeBoundingBox();
Asset.instance.computeBoundingSphere(); Asset.instance.computeBoundingSphere();
console.log(Asset.instance.boundingBox); const sphere = new THREE.SphereGeometry(0.05);
const sphereMesh = new THREE.Mesh(sphere, new THREE.MeshBasicMaterial({ wireframe: true, }));
Renderer.scene.add(Asset.instance); Renderer.scene.add(Asset.instance);
Renderer.render(); Renderer.scene.add(highlightHelper);
Renderer.scene.add(sphereMesh);
Renderer.startRendering();
window.addEventListener('mousedown', drag); window.addEventListener('mousedown', drag);
window.addEventListener('mouseup', select); window.addEventListener('mouseup', select);
window.addEventListener('mousemove', hover); window.addEventListener('mousemove', hover);
window.addEventListener('wheel', zoom);
console.log("Start time: %sms", performance.measure("Start time", "start", "init").duration);
console.log("Init time: %sms", performance.measure("Init time", "init", "ready").duration);
let cursor = { x: 0, y: 0 };
let dragCursor = {x: 0, y: 0 }, dragging = false;
function drag(e: MouseEvent): void function drag(e: MouseEvent): void
{ {
dragCursor = Renderer.screenSpaceToCameraSpace(e.clientX, e.clientY, false);
dragging = true;
} }
function select(e: MouseEvent): void function select(e: MouseEvent): void
{ {
console.log(e.clientX, e.clientY, (e.clientX / window.innerWidth - 0.5) * FRUSTUMSIZE * Renderer.aspect, - (e.clientY / window.innerHeight - 0.5) * FRUSTUMSIZE); dragging = false;
} }
function hover(e: MouseEvent): void function hover(e: MouseEvent): void
{ {
if(dragging === true)
} {
const cursor = Renderer.screenSpaceToCameraSpace(e.clientX, e.clientY, false);
Renderer.move(dragCursor.x - cursor.x, dragCursor.y - cursor.y);
dragCursor = cursor;
return;
}
else
{
const cursor = Renderer.screenSpaceToCameraSpace(e.clientX, e.clientY, true);
sphereMesh.position.set(cursor.x, cursor.y, 0);
overallTimer -= performance.now();
const assets = quad.fetch(cursor.x, cursor.y) as Asset[];
overallTimer += performance.now();
overallSamples++;
if (assets.length > 0) {
highlightHelper.box.setFromArray([assets[0].x1, assets[0].y1, 0, assets[0].x2, assets[0].y2, 0]);
highlightHelper.updateMatrixWorld();
highlightHelper.visible = true;
}
else {
highlightHelper.visible = false;
}
}
}
function zoom(e: WheelEvent): void
{
Renderer.zoom = clamp(Renderer.zoom * 1 + (e.deltaY * -0.001), 1, 5);
}
setInterval(() => {
console.log("Average query time: %s µs", overallTimer / overallSamples * 1000);
}, 1000);

View File

@ -9,7 +9,7 @@ export default class Quadtree<T extends AABB>
#nodes: Node[]; #nodes: Node[];
#content: T[]; #content: T[];
#dirty: boolean = false; #dirty: boolean = true;
constructor(bounds: AABB, maxDepth?: number, maxElmts?: number) constructor(bounds: AABB, maxDepth?: number, maxElmts?: number)
{ {
@ -22,9 +22,9 @@ export default class Quadtree<T extends AABB>
this.#nodes.push({ children: new LinkedList<number>(), count: 0 }); this.#nodes.push({ children: new LinkedList<number>(), count: 0 });
} }
fetch(point: Point): T[] fetch(x: number, y: number): T[]
{ {
return this.query({x1: point.x, x2: point.x, y1: point.y, y2: point.y}); return this.query({x1: x, x2: x, y1: y, y2: y});
} }
query(aabb: AABB): T[] query(aabb: AABB): T[]
{ {
@ -97,6 +97,30 @@ export default class Quadtree<T extends AABB>
} }
} }
} }
traverse(cb: (prop: NodeProp) => void): void
{
const stack: NodeProp[] = [];
stack.push({ index: 0, depth: 0, bounds: this.#bounds });
while(stack.length > 0)
{
const nodeProp = stack.pop()!;
cb(nodeProp);
//If the node contains elements
if (this.#nodes[nodeProp.index].count === -1)
{
const children = this.#nodes[nodeProp.index].children.toArray();
const mx = nodeProp.bounds.x1 + (nodeProp.bounds.x2 - nodeProp.bounds.x1) / 2, my = nodeProp.bounds.y1 + (nodeProp.bounds.y2 - nodeProp.bounds.y1) / 2;
stack.push({ index: children[0], depth: nodeProp.depth + 1, bounds: { x1: nodeProp.bounds.x1, x2: mx, y1: nodeProp.bounds.y1, y2: my } });
stack.push({ index: children[1], depth: nodeProp.depth + 1, bounds: { x1: mx, x2: nodeProp.bounds.x2, y1: nodeProp.bounds.y1, y2: my } });
stack.push({ index: children[2], depth: nodeProp.depth + 1, bounds: { x1: nodeProp.bounds.x1, x2: mx, y1: my, y2: nodeProp.bounds.y2 } });
stack.push({ index: children[3], depth: nodeProp.depth + 1, bounds: { x1: mx, x2: nodeProp.bounds.x2, y1: my, y2: nodeProp.bounds.y2 } });
}
}
}
#find_leaves(index: number, depth: number, bounds: AABB, elmtBounds: AABB): NodeProp[] #find_leaves(index: number, depth: number, bounds: AABB, elmtBounds: AABB): NodeProp[]
{ {
@ -110,26 +134,26 @@ export default class Quadtree<T extends AABB>
//If the node contains elements //If the node contains elements
if(this.#nodes[nodeProp.index].count !== -1) if(this.#nodes[nodeProp.index].count !== -1)
result.push({index: index, depth: depth, bounds: bounds}); result.push(nodeProp);
else else
{ {
//Check intersection on each 4 sides of the node //Check intersection on each 4 sides of the node
const children = this.#nodes[nodeProp.index].children.toArray(); const children = this.#nodes[nodeProp.index].children.toArray();
const mx = bounds.x1 + (bounds.x2 - bounds.x1) / 2, my = bounds.y1 + (bounds.y2 - bounds.y1) / 2; const mx = nodeProp.bounds.x1 + (nodeProp.bounds.x2 - nodeProp.bounds.x1) / 2, my = nodeProp.bounds.y1 + (nodeProp.bounds.y2 - nodeProp.bounds.y1) / 2;
if(elmtBounds.y1 <= my) if(elmtBounds.y1 <= my)
{ {
if(elmtBounds.x1 <= mx) if(elmtBounds.x1 <= mx)
stack.push({ index: children[0], depth: depth + 1, bounds: { x1: bounds.x1, x2: mx, y1: bounds.y1, y2: my }}); stack.push({ index: children[0], depth: nodeProp.depth + 1, bounds: { x1: nodeProp.bounds.x1, x2: mx, y1: nodeProp.bounds.y1, y2: my }});
if(elmtBounds.x2 > mx) if(elmtBounds.x2 > mx)
stack.push({ index: children[1], depth: depth + 1, bounds: { x1: mx, x2: bounds.x2, y1: bounds.y1, y2: my }}); stack.push({ index: children[1], depth: nodeProp.depth + 1, bounds: { x1: mx, x2: nodeProp.bounds.x2, y1: nodeProp.bounds.y1, y2: my }});
} }
if(elmtBounds.y2 > my) if(elmtBounds.y2 > my)
{ {
if(elmtBounds.x1 <= mx) if(elmtBounds.x1 <= mx)
stack.push({ index: children[2], depth: depth + 1, bounds: { x1: bounds.x1, x2: mx, y1: my, y2: bounds.y2 }}); stack.push({ index: children[2], depth: nodeProp.depth + 1, bounds: { x1: nodeProp.bounds.x1, x2: mx, y1: my, y2: nodeProp.bounds.y2 }});
if(elmtBounds.x2 > mx) if(elmtBounds.x2 > mx)
stack.push({ index: children[3], depth: depth + 1, bounds: { x1: mx, x2: bounds.x2, y1: my, y2: bounds.y2 }}); stack.push({ index: children[3], depth: nodeProp.depth + 1, bounds: { x1: mx, x2: nodeProp.bounds.x2, y1: my, y2: nodeProp.bounds.y2 }});
} }
} }
} }

View File

@ -1,5 +1,7 @@
import * as Three from 'three'; import * as Three from 'three';
import { FRUSTUMSIZE } from '../consts'; import { FRUSTUMSIZE } from '../consts';
import Stats from 'stats.js';
import { Point } from '../physics/common';
export default class Renderer export default class Renderer
{ {
@ -8,6 +10,11 @@ export default class Renderer
static renderer: Three.WebGLRenderer; static renderer: Three.WebGLRenderer;
static camera: Three.OrthographicCamera; static camera: Three.OrthographicCamera;
static #zoom: number;
static #pos: Point = { x: 0, y: 0 };
static #stats: Stats;
static init(): Boolean static init(): Boolean
{ {
try { try {
@ -19,6 +26,12 @@ export default class Renderer
this.camera = new Three.OrthographicCamera(); this.camera = new Three.OrthographicCamera();
this.camera.position.z = 500; this.camera.position.z = 500;
this.#zoom = 1;
const stats = this.#stats = new Stats();
stats.showPanel(0);
document.body.appendChild(stats.dom);
this.#resize(); this.#resize();
window.addEventListener("resize", this.#resize.bind(this)); window.addEventListener("resize", this.#resize.bind(this));
@ -30,15 +43,35 @@ export default class Renderer
return false; return false;
} }
} }
static get zoom(): number
{
return this.#zoom;
}
static set zoom(v: number)
{
this.#zoom = v;
this.#resize();
}
static move(x: number, y: number): void
{
this.#pos.x += x;
this.#pos.y += y;
this.camera.position.x += x;
this.camera.position.y += y;
}
static screenSpaceToCameraSpace(x: number, y: number, offset: boolean): Point {
return { x: ((x / window.innerWidth - 0.5) * FRUSTUMSIZE * this.aspect - (offset ? this.#pos.x : 0)) / this.#zoom, y: (- (y / window.innerHeight - 0.5) * FRUSTUMSIZE - (offset ? this.#pos.x : 0)) / this.zoom };
}
static #resize(): void static #resize(): void
{ {
const aspect = this.aspect = window.innerWidth / window.innerHeight; const aspect = this.aspect = window.innerWidth / window.innerHeight;
this.renderer.setSize( window.innerWidth, window.innerHeight ); this.renderer.setSize( window.innerWidth, window.innerHeight );
this.camera.left = FRUSTUMSIZE * aspect / - 2; this.camera.left = FRUSTUMSIZE * aspect / - 2 / this.#zoom;
this.camera.right = FRUSTUMSIZE * aspect / 2; this.camera.right = FRUSTUMSIZE * aspect / 2 / this.#zoom;
this.camera.top = FRUSTUMSIZE / 2; this.camera.top = FRUSTUMSIZE / 2 / this.#zoom;
this.camera.bottom = FRUSTUMSIZE / - 2; this.camera.bottom = FRUSTUMSIZE / - 2 / this.#zoom;
this.camera.updateProjectionMatrix(); this.camera.updateProjectionMatrix();
@ -46,7 +79,9 @@ export default class Renderer
} }
static render(delta?: number): void static render(delta?: number): void
{ {
this.#stats.begin();
this.renderer.render(this.scene, this.camera); this.renderer.render(this.scene, this.camera);
this.#stats.end();
} }
static startRendering(): void static startRendering(): void
{ {