From ca7b1d19566de547f7240c68153d08f6f08d86c2 Mon Sep 17 00:00:00 2001 From: Peaceultime Date: Tue, 11 Jun 2024 01:19:48 +0200 Subject: [PATCH] Stress-testing the physics engine --- bun.lockb | Bin 22977 -> 23330 bytes package.json | 1 + src/assets/asset.class.ts | 62 +++++++++++++++++++------ src/common.ts | 4 ++ src/main.ts | 80 +++++++++++++++++++++++++++------ src/physics/quadtree.class.ts | 42 +++++++++++++---- src/renderer/renderer.class.ts | 43 ++++++++++++++++-- 7 files changed, 192 insertions(+), 40 deletions(-) diff --git a/bun.lockb b/bun.lockb index 43bc94ee5cd2b11545ed2b3aec8ec62e36ccaaa3..a63096b14a50784fe95e1fb8c33cad0ceebf4c58 100644 GIT binary patch delta 3621 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 diff --git a/package.json b/package.json index f5d2eaf..2977f61 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ }, "dependencies": { "@types/three": "^0.165.0", + "stats.js": "^0.17.0", "three": "^0.165.0", "three-mesh-bvh": "^0.7.5" } diff --git a/src/assets/asset.class.ts b/src/assets/asset.class.ts index d20bee2..b03936f 100644 --- a/src/assets/asset.class.ts +++ b/src/assets/asset.class.ts @@ -14,30 +14,52 @@ export default class Asset implements AABB mat: Three.Matrix4; 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) { this.mat = mat ?? new Three.Matrix4(); + this.#updateAABB(); this.layer = layer ?? 0; } - get x1() - { - 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 ] )); + #updateAABB() { + const aabb = { x1: -0.5, x2: 0.5, y1: -0.5, y2: 0.5 }; + + 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() - { - 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 x1() { + return this.#aabb.x1; } - get x2() - { - 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 y1() { + return this.#aabb.y1; } - get y2() - { - 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() { + return this.#aabb.x2; + } + get y2() { + return this.#aabb.y2; } move(x: number, y: number): Asset { @@ -48,6 +70,8 @@ export default class Asset implements AABB this.mat.compose(_position, _rotation, _scale); + this.#updateAABB(); + return this; } moveTo(x: number, y: number): Asset @@ -59,6 +83,8 @@ export default class Asset implements AABB this.mat.compose(_position, _rotation, _scale); + this.#updateAABB(); + return this; } rotate(rad: number): Asset @@ -71,6 +97,8 @@ export default class Asset implements AABB this.mat.compose(_position, _rotation, _scale); + this.#updateAABB(); + return this; } rotateTo(rad: number): Asset @@ -83,6 +111,8 @@ export default class Asset implements AABB this.mat.compose(_position, _rotation, _scale); + this.#updateAABB(); + return this; } scale(x: number, y: number): Asset @@ -94,6 +124,8 @@ export default class Asset implements AABB this.mat.compose(_position, _rotation, _scale); + this.#updateAABB(); + return this; } scaleTo(x: number, y: number): Asset @@ -105,6 +137,8 @@ export default class Asset implements AABB this.mat.compose(_position, _rotation, _scale); + this.#updateAABB(); + return this; } shapeTo(x1: number, y1: number, x2: number, y2: number, x3: number, y3: number, x4: number, y4: number): Asset diff --git a/src/common.ts b/src/common.ts index 703743b..02cc0f4 100644 --- a/src/common.ts +++ b/src/common.ts @@ -207,4 +207,8 @@ interface LinkedElmt next: LinkedElmt | null; prev: LinkedElmt | null; +} +export function clamp(x: number, min: number, max: number): number +{ + return x > max ? max : x < min ? min : x; } \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 03acbcc..b4edf5e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,52 +3,106 @@ import Renderer from './renderer/renderer.class'; import Asset from './assets/asset.class'; import Quadtree from './physics/quadtree.class'; import { FRUSTUMSIZE } from './consts'; +import { clamp } from './common'; +performance.mark("start"); Renderer.init(); 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[] = []; -for(let i = 0; i < 1; i++) +for(let i = 0; i < 10000; i++) { assets[i] = new Asset(new THREE.Matrix4(), 1); assets[i] .move((Math.random() - 0.5) * FRUSTUMSIZE * Renderer.aspect, (Math.random() - 0.5) * FRUSTUMSIZE) .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); 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.computeBoundingBox(); 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.render(); +Renderer.scene.add(highlightHelper); +Renderer.scene.add(sphereMesh); + +Renderer.startRendering(); window.addEventListener('mousedown', drag); window.addEventListener('mouseup', select); 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 { - + dragCursor = Renderer.screenSpaceToCameraSpace(e.clientX, e.clientY, false); + dragging = true; } 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 { - -} \ No newline at end of file + 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); \ No newline at end of file diff --git a/src/physics/quadtree.class.ts b/src/physics/quadtree.class.ts index 42d730e..31d1013 100644 --- a/src/physics/quadtree.class.ts +++ b/src/physics/quadtree.class.ts @@ -9,7 +9,7 @@ export default class Quadtree #nodes: Node[]; #content: T[]; - #dirty: boolean = false; + #dirty: boolean = true; constructor(bounds: AABB, maxDepth?: number, maxElmts?: number) { @@ -22,9 +22,9 @@ export default class Quadtree this.#nodes.push({ children: new LinkedList(), 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[] { @@ -97,6 +97,30 @@ export default class Quadtree } } } + 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[] { @@ -110,26 +134,26 @@ export default class Quadtree //If the node contains elements if(this.#nodes[nodeProp.index].count !== -1) - result.push({index: index, depth: depth, bounds: bounds}); + result.push(nodeProp); else { //Check intersection on each 4 sides of the node 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.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) - 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.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) - 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 }}); } } } diff --git a/src/renderer/renderer.class.ts b/src/renderer/renderer.class.ts index 4f4d039..d470876 100644 --- a/src/renderer/renderer.class.ts +++ b/src/renderer/renderer.class.ts @@ -1,5 +1,7 @@ import * as Three from 'three'; import { FRUSTUMSIZE } from '../consts'; +import Stats from 'stats.js'; +import { Point } from '../physics/common'; export default class Renderer { @@ -8,6 +10,11 @@ export default class Renderer static renderer: Three.WebGLRenderer; static camera: Three.OrthographicCamera; + + static #zoom: number; + static #pos: Point = { x: 0, y: 0 }; + + static #stats: Stats; static init(): Boolean { try { @@ -19,6 +26,12 @@ export default class Renderer this.camera = new Three.OrthographicCamera(); this.camera.position.z = 500; + this.#zoom = 1; + + const stats = this.#stats = new Stats(); + stats.showPanel(0); + document.body.appendChild(stats.dom); + this.#resize(); window.addEventListener("resize", this.#resize.bind(this)); @@ -30,15 +43,35 @@ export default class Renderer 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 { const aspect = this.aspect = window.innerWidth / window.innerHeight; this.renderer.setSize( window.innerWidth, window.innerHeight ); - this.camera.left = FRUSTUMSIZE * aspect / - 2; - this.camera.right = FRUSTUMSIZE * aspect / 2; - this.camera.top = FRUSTUMSIZE / 2; - this.camera.bottom = FRUSTUMSIZE / - 2; + this.camera.left = FRUSTUMSIZE * aspect / - 2 / this.#zoom; + this.camera.right = FRUSTUMSIZE * aspect / 2 / this.#zoom; + this.camera.top = FRUSTUMSIZE / 2 / this.#zoom; + this.camera.bottom = FRUSTUMSIZE / - 2 / this.#zoom; this.camera.updateProjectionMatrix(); @@ -46,7 +79,9 @@ export default class Renderer } static render(delta?: number): void { + this.#stats.begin(); this.renderer.render(this.scene, this.camera); + this.#stats.end(); } static startRendering(): void {