diff --git a/bun.lockb b/bun.lockb index 43bc94e..a63096b 100644 Binary files a/bun.lockb and b/bun.lockb differ 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 {