Optimize quadtree

This commit is contained in:
Peaceultime 2024-06-13 22:40:23 +02:00
parent 1d907c8ab9
commit 209a070464
8 changed files with 136 additions and 126 deletions

View File

@ -1,6 +1,7 @@
import * as Three from 'three';
import * as CONST from '../consts';
import { AABB } from '../physics/common';
import Quadtree from '../physics/quadtree.class';
const UP = new Three.Vector3(0, 0, 1);
@ -9,26 +10,41 @@ const _rotation = new Three.Quaternion();
const _euler = new Three.Euler();
const _scale = new Three.Vector3();
export default class Asset implements AABB
export default class Asset
{
mat: Three.Matrix4;
layer: number;
#selected = false;
#dirty = false;
//@ts-ignore
//@ts-expect-error
#aabb: AABB;
static instance = new Three.InstancedMesh(CONST.QUAD, new Three.MeshBasicMaterial({ color: new Three.Color( 0xffffff ) }), 1000000);
#index: number;
#quad?: number;
static instance: Three.InstancedMesh = new Three.InstancedMesh(CONST.QUAD, new Three.MeshBasicMaterial({ color: new Three.Color(0xffffff) }), 1000000);
static assets: Asset[] = [];
constructor(mat?: Three.Matrix4, layer?: number)
{
this.mat = mat ?? new Three.Matrix4();
this.#updateAABB();
this.layer = layer ?? 0;
this.#index = Asset.assets.push(this) - 1;
}
#updateAABB() {
get aabb(): AABB
{
return this.#aabb;
}
insert(quad: Quadtree): void
{
this.#quad = quad.insert(this.#index, this.#aabb);
}
remove(quad: Quadtree): void
{
this.#quad !== undefined && quad.remove(this.#quad);
}
#updateAABB(): void {
const aabb = { x1: -0.5, x2: 0.5, y1: -0.5, y2: 0.5 };
const e = this.mat.elements;
@ -49,18 +65,6 @@ export default class Asset implements AABB
y2: Math.max(y1, y2, y3, y4)
};
}
get x1() {
return this.#aabb.x1;
}
get y1() {
return this.#aabb.y1;
}
get x2() {
return this.#aabb.x2;
}
get y2() {
return this.#aabb.y2;
}
move(x: number, y: number): Asset
{
this.mat.decompose(_position, _rotation, _scale);

View File

@ -52,7 +52,7 @@ export class FreeList<T>
}
export class IntList
{
#data: Uint32Array;
#data: number[];
#fields: number;
#capacity: number = DEFAULT_SIZE;
#length: number = 0;
@ -63,7 +63,7 @@ export class IntList
if(fields <= 0)
throw new Error("Invalid field count");
this.#data = new Uint32Array(this.#capacity * fields);
this.#data = new Array(this.#capacity * fields);
this.#fields = fields;
}
get length(): number
@ -105,8 +105,7 @@ export class IntList
{
this.#capacity *= 2;
//@ts-ignore
this.#data.buffer.transferToFixedLength(this.#capacity);
this.#data.length = this.#capacity * this.#fields;
}
return this.#length++;
@ -140,7 +139,22 @@ export class IntList
toArray(): number[]
{
return Array.from(this.#data);
return this.#data.slice(0, this.#length);
}
//DEBUG
printReadable(names: string[] = []): void
{
const size = this.#length * this.#fields;
for(let i = 0; i < size; i)
{
const obj: Record<string, number> = {};
for(let j = 0; j < this.#fields; ++i, ++j)
{
obj[names[j] ?? j] = this.#data[i];
}
console.log(obj);
}
}
}
export class Stack<T>

View File

@ -10,6 +10,7 @@ export {
QUAD
};
export const FRUSTUMSIZE = 16;
export const RESOLUTION_X = 2048;
export const RESOLUTION_Y = 2048;
export const MAX_DEPTH = 8;

View File

@ -2,45 +2,38 @@ import * as THREE from 'three';
import Renderer from './renderer/renderer.class';
import Asset from './assets/asset.class';
import Quadtree from './physics/quadtree.class';
import { FRUSTUMSIZE } from './consts';
import { RESOLUTION_X, RESOLUTION_Y } from './consts';
import Input from './renderer/input.class';
import { Random, clamp } from './common';
import Selector from './renderer/selector.class';
if(!ArrayBuffer.prototype.hasOwnProperty("transferToFixedLength"))
{
throw new Error("Your web browser doesn't includes the latest features needed to make this website work properly.\nPlease upgrade your browser and try again.");
}
Renderer.init();
Input.init(Renderer.canvas);
Selector.init();
const r = new Random(0);
const quad = new Quadtree({ x1: -RESOLUTION_X / 2, x2: RESOLUTION_X / 2, y1: -RESOLUTION_Y / 2, y2: RESOLUTION_Y / 2 }, 6, 10);
const quad = new Quadtree({x1: -FRUSTUMSIZE * Renderer.aspect / 2, x2: FRUSTUMSIZE * Renderer.aspect / 2, y1: -FRUSTUMSIZE / 2, y2: FRUSTUMSIZE / 2});
const assets: Asset[] = [];
for(let i = 0; i < 10000; i++)
{
assets[i] = new Asset(new THREE.Matrix4(), 1);
const asset = new Asset(new THREE.Matrix4(), 1);
assets[i]
.move(r.nextFloat(-0.5 * FRUSTUMSIZE * Renderer.aspect, 0.5 * FRUSTUMSIZE * Renderer.aspect), r.nextFloat(-0.5 * FRUSTUMSIZE, 0.5 * FRUSTUMSIZE))
asset.move(r.nextInt(-0.5 * RESOLUTION_X, 0.5 * RESOLUTION_X), r.nextInt(-0.5 * RESOLUTION_Y, 0.5 * RESOLUTION_Y))
.rotate(r.nextFloat(Math.PI * 2))
.scale(r.nextFloat(0.01, 0.15), r.nextFloat(0.01, 0.15))
.scale(r.nextFloat(10, 30), r.nextFloat(10, 30));
Asset.instance.setMatrixAt(i, assets[i].mat);
quad.insert(i, assets[i]);
Asset.instance.setMatrixAt(i, asset.mat);
asset.insert(quad);
}
Asset.instance.count = assets.length;
Asset.instance.count = Asset.assets.length;
Asset.instance.computeBoundingBox();
Asset.instance.computeBoundingSphere();
Renderer.scene.add(Asset.instance);
//quad.debug();
Renderer.startRendering();
Input.onDragStart((_, button) => { if(button & 1) Selector.hide(); });
@ -48,8 +41,8 @@ Input.onDragEnd((start, end, button) => {
if(button & 1)
{
const s = performance.now();
const selection = quad.query({x1: Math.min(start.x, end.x), x2: Math.max(start.x, end.x), y1: Math.min(start.y, end.y), y2: Math.max(start.y, end.y)}).map(e => assets[e]);
console.log("Fetching %s out of %s elements in %sms", selection.length, assets.length, performance.now() - s);
const selection = quad.query({x1: Math.min(start.x, end.x), x2: Math.max(start.x, end.x), y1: Math.min(start.y, end.y), y2: Math.max(start.y, end.y)}).map(e => Asset.assets[e]);
console.log("Fetching %s out of %s elements in %sms", selection.length, Asset.assets.length, performance.now() - s);
if(Input.keys['Shift']) Selector.toggle(selection);
else Selector.select(selection);
@ -59,11 +52,11 @@ Input.onDrag((delta, start, end, button) => { if(button & 1) Selector.preview(st
Input.onClick((point, button) => {
if(button & 1)
{
const selection = quad.fetch(point.x, point.y).map(e => assets[e]);
const selection = quad.fetch(point.x, point.y).map(e => Asset.assets[e]);
if(Input.keys['Shift']) Selector.toggle(selection);
else Selector.select(selection);
}
});
Input.onWheel(delta => Renderer.zoom = clamp(Renderer.zoom * 1 + (delta * -0.001), 1, 5));
Input.onMove(p => { if(!Input.dragging) Selector.ghost(assets[quad.fetch(p.x, p.y)[0]]); });
Input.onWheel(delta => Renderer.zoom = clamp(Renderer.zoom * 1 + (delta * -0.001), 0.9, 5));
Input.onMove(p => { if (!Input.dragging) Selector.ghost(Asset.assets[quad.fetch(p.x, p.y)[0]]); });

View File

@ -1,3 +1,5 @@
import Asset from "../assets/asset.class";
export interface Point
{
x: number;
@ -11,22 +13,12 @@ export interface AABB
y2: number;
}
export function intersectsObj(a: AABB, b: AABB | Point): Boolean
{
if(b.hasOwnProperty("x") && b.hasOwnProperty("y"))
{
b = b as Point;
return a.x1 <= b.x && a.x2 >= b.x && a.y1 <= b.y && a.y2 >= b.y;
}
else
{
b = b as AABB;
return a.x1 <= b.x2 && a.x2 >= b.x1 && a.y1 <= b.y2 && a.y2 >= b.y1;
}
}
export function intersects(aLeft: number, aTop: number, aRight: number, aBottom: number, bLeft: number, bTop: number, bRight: number, bBottom: number): Boolean
export function intersects(aLeft: number, aTop: number, aRight: number, aBottom: number, bLeft: number, bTop: number, bRight: number, bBottom: number): boolean
{
return aLeft <= bRight && aRight >= bLeft && aTop <= bBottom && aBottom >= bTop;
}
export function intersectsAsset(left: number, top: number, right: number, bottom: number, id: number): boolean
{
const aabb = Asset.assets[id].aabb;
return intersects(left, top, right, bottom, aabb.x1, aabb.y1, aabb.x2, aabb.y2);
}

View File

@ -1,13 +1,8 @@
import { FreeList, IntList, LinkedList } from "../common";
import { IntList } from "../common";
import Renderer from "../renderer/renderer.class";
import { AABB, intersects } from "./common";
import { AABB, intersectsAsset } from "./common";
import * as THREE from 'three';
/**
* A node that contains elments is called a leaf. Its count is equals to the amount of children it holds.
* A node that contains other nodes is called a branch. Its count is equals to -1.
* The AABB of each node isn't stored and is computed on the go. (Not memory friendly in JS ?)
*/
enum QuadConsts
{
ENodeINext = 0,
@ -23,8 +18,13 @@ enum QuadConsts
}
const _process = new IntList(1);
const _nodeProcess = new IntList(QuadConsts.PropCount);
const _nodes = new IntList(QuadConsts.PropCount);
/**
* A node that contains elments is called a leaf. Its count is equals to the amount of children it holds.
* A node that contains other nodes is called a branch. Its count is equals to -1.
* The AABB of each node isn't stored and is computed on the go.
*/
export default class Quadtree
{
#bounds: AABB;
@ -40,6 +40,11 @@ export default class Quadtree
constructor(bounds: AABB, maxDepth?: number, maxElmts?: number)
{
this.#bounds = bounds;
this.#bounds.x1 = Math.round(this.#bounds.x1);
this.#bounds.x2 = Math.round(this.#bounds.x2);
this.#bounds.y1 = Math.round(this.#bounds.y1);
this.#bounds.y2 = Math.round(this.#bounds.y2);
this.#maxDepth = maxDepth ?? this.#maxDepth;
this.#maxElmts = maxElmts ?? this.#maxElmts;
@ -55,7 +60,7 @@ export default class Quadtree
{
_process.clear();
const leaves = this.#findLeaves(0, 0, this.#bounds.x1, this.#bounds.x2, this.#bounds.y1, this.#bounds.y2, aabb.x1, aabb.x2, aabb.y1, aabb.y2);
const leaves = this.#findLeaves(0, 0, this.#bounds.x1, this.#bounds.x2, this.#bounds.y1, this.#bounds.y2, Math.floor(aabb.x1), Math.floor(aabb.x2), Math.floor(aabb.y1), Math.floor(aabb.y2));
const tmp: Record<number, boolean> = {};
@ -68,14 +73,11 @@ export default class Quadtree
{
const elmt = this.#enodes.get(node, QuadConsts.ENodeIElmt);
const left = this.#content.get(elmt, QuadConsts.ElmtILft),
right = this.#content.get(elmt, QuadConsts.ElmtIRgt),
top = this.#content.get(elmt, QuadConsts.ElmtITop),
bottom = this.#content.get(elmt, QuadConsts.ElmtIBtm);
const id = this.#content.get(elmt, QuadConsts.ElmtIId);
if(!tmp[elmt] && intersects(aabb.x1, aabb.y1, aabb.x2, aabb.y2, left, top, right, bottom))
if (!tmp[elmt] && intersectsAsset(aabb.x1, aabb.y1, aabb.x2, aabb.y2, id))
{
_process.set(_process.push(), 0, elmt);
_process.set(_process.push(), 0, id);
tmp[elmt] = true;
}
node = this.#enodes.get(node, QuadConsts.ENodeINext);
@ -88,10 +90,10 @@ export default class Quadtree
{
const index = this.#content.insert();
this.#content.set(index, QuadConsts.ElmtILft, aabb.x1);
this.#content.set(index, QuadConsts.ElmtIRgt, aabb.x2);
this.#content.set(index, QuadConsts.ElmtITop, aabb.y1);
this.#content.set(index, QuadConsts.ElmtIBtm, aabb.y2);
this.#content.set(index, QuadConsts.ElmtILft, Math.floor(aabb.x1));
this.#content.set(index, QuadConsts.ElmtIRgt, Math.floor(aabb.x2));
this.#content.set(index, QuadConsts.ElmtITop, Math.floor(aabb.y1));
this.#content.set(index, QuadConsts.ElmtIBtm, Math.floor(aabb.y2));
this.#content.set(index, QuadConsts.ElmtIId, id);
this.#insertNode(0, 0, this.#bounds.x1, this.#bounds.x2, this.#bounds.y1, this.#bounds.y2, index);
@ -190,26 +192,26 @@ export default class Quadtree
}
traverse(cb: (index: number, depth: number, left: number, top: number, right: number, bottom: number, leaf: boolean) => void): void
{
_nodeProcess.clear();
_nodes.clear();
insert(_nodeProcess, 0, 0, this.#bounds.x1, this.#bounds.x2, this.#bounds.y1, this.#bounds.y2);
insert(_nodes, 0, 0, this.#bounds.x1, this.#bounds.x2, this.#bounds.y1, this.#bounds.y2);
while(_nodeProcess.length > 0)
while(_nodes.length > 0)
{
const last = _nodeProcess.length - 1;
const last = _nodes.length - 1;
const node = _nodeProcess.get(last, QuadConsts.PropIIdx);
const left = _nodeProcess.get(last, QuadConsts.PropILft);
const right = _nodeProcess.get(last, QuadConsts.PropIRgt);
const top = _nodeProcess.get(last, QuadConsts.PropITop);
const bottom = _nodeProcess.get(last, QuadConsts.PropIBtm);
const depth = _nodeProcess.get(last, QuadConsts.PropIDpt);
const node = _nodes.get(last, QuadConsts.PropIIdx);
const left = _nodes.get(last, QuadConsts.PropILft);
const right = _nodes.get(last, QuadConsts.PropIRgt);
const top = _nodes.get(last, QuadConsts.PropITop);
const bottom = _nodes.get(last, QuadConsts.PropIBtm);
const depth = _nodes.get(last, QuadConsts.PropIDpt);
_nodeProcess.pop();
_nodes.pop();
const count = this.#nodes.get(node, QuadConsts.NodeICount);
cb(left, top, right, bottom, node, depth, count !== -1);
cb(node, depth, left, top, right, bottom, count !== -1);
//If it's a branch
if (count === -1)
@ -217,14 +219,14 @@ export default class Quadtree
const fc = this.#nodes.get(node, QuadConsts.NodeIFirst);
const mx = left + (right - left) / 2, my = top + (bottom - top) / 2;
insert(_nodeProcess, fc + 0, depth + 1, left, mx, top, my);
insert(_nodeProcess, fc + 1, depth + 1, mx, right, top, my);
insert(_nodeProcess, fc + 2, depth + 1, left, mx, my, bottom);
insert(_nodeProcess, fc + 3, depth + 1, mx, right, my, bottom);
insert(_nodes, fc + 0, depth + 1, left, mx, top, my);
insert(_nodes, fc + 1, depth + 1, mx, right, top, my);
insert(_nodes, fc + 2, depth + 1, left, mx, my, bottom);
insert(_nodes, fc + 3, depth + 1, mx, right, my, bottom);
}
}
}
render(): void
debug(): void
{
Renderer.scene.remove(...this.#debugRect);
this.#debugRect = [];
@ -234,26 +236,30 @@ export default class Quadtree
});
Renderer.scene.add(...this.#debugRect);
// this.#nodes.printReadable(["first", "count"]);
// this.#enodes.printReadable(["index", "next"]);
// this.#content.printReadable(["left", "right", "top", "bottom", "index", "depth"]);
}
#findLeaves(index: number, depth: number, lleft: number, lright: number, ltop: number, lbottom: number, eleft: number, eright: number, etop: number, ebottom: number): IntList
{
const leaves = new IntList(QuadConsts.PropCount);
_nodeProcess.clear();
insert(_nodeProcess, index, depth, lleft, lright, ltop, lbottom);
_nodes.clear();
insert(_nodes, index, depth, lleft, lright, ltop, lbottom);
while (_nodeProcess.length > 0)
while (_nodes.length > 0)
{
const last = _nodeProcess.length - 1;
const last = _nodes.length - 1;
const nodeLeft = _nodeProcess.get(last, QuadConsts.PropILft);
const nodeRight = _nodeProcess.get(last, QuadConsts.PropIRgt);
const nodeTop = _nodeProcess.get(last, QuadConsts.PropITop);
const nodeBottom = _nodeProcess.get(last, QuadConsts.PropIBtm);
const nodeIndex = _nodeProcess.get(last, QuadConsts.PropIIdx);
const nodeDepth = _nodeProcess.get(last, QuadConsts.PropIDpt);
const nodeLeft = _nodes.get(last, QuadConsts.PropILft);
const nodeRight = _nodes.get(last, QuadConsts.PropIRgt);
const nodeTop = _nodes.get(last, QuadConsts.PropITop);
const nodeBottom = _nodes.get(last, QuadConsts.PropIBtm);
const nodeIndex = _nodes.get(last, QuadConsts.PropIIdx);
const nodeDepth = _nodes.get(last, QuadConsts.PropIDpt);
_nodeProcess.pop();
_nodes.pop();
if(this.#nodes.get(nodeIndex, QuadConsts.NodeICount) !== -1)
insert(leaves, nodeIndex, nodeDepth, nodeLeft, nodeRight, nodeTop, nodeBottom);
@ -266,16 +272,16 @@ export default class Quadtree
if(etop <= my)
{
if(eleft <= mx) //Add a new
insert(_nodeProcess, fc + 0, nodeDepth + 1, nodeLeft, mx, nodeTop, my);
insert(_nodes, fc + 0, nodeDepth + 1, nodeLeft, mx, nodeTop, my);
if(eright > mx)
insert(_nodeProcess, fc + 1, nodeDepth + 1, mx, nodeRight, nodeTop, my);
insert(_nodes, fc + 1, nodeDepth + 1, mx, nodeRight, nodeTop, my);
}
if(ebottom > my)
{
if(eleft <= mx)
insert(_nodeProcess, fc + 2, nodeDepth + 1, nodeLeft, mx, my, nodeBottom);
insert(_nodes, fc + 2, nodeDepth + 1, nodeLeft, mx, my, nodeBottom);
if(eright > mx)
insert(_nodeProcess, fc + 3, nodeDepth + 1, mx, nodeRight, my, nodeBottom);
insert(_nodes, fc + 3, nodeDepth + 1, mx, nodeRight, my, nodeBottom);
}
}
}
@ -328,14 +334,14 @@ export default class Quadtree
this.#nodes.set(index, QuadConsts.NodeICount, -1);
for(let i = 0; i < _process.length; ++i)
this.#insertNode(index, depth, left, top, right, bottom, _process.get(i, 0));
this.#insertNode(index, depth, left, right, top, bottom, _process.get(i, 0));
}
else
{
this.#nodes.set(index, QuadConsts.NodeICount, this.#nodes.get(index, QuadConsts.NodeICount) + 1);
}
}
#insertNode(index: number, depth: number, left: number, top: number, right: number, bottom: number, elmt: number): void
#insertNode(index: number, depth: number, left: number, right: number, top: number, bottom: number, elmt: number): void
{
const leaves = this.#findLeaves(index, depth, left, right, top, bottom, this.#content.get(elmt, QuadConsts.ElmtILft), this.#content.get(elmt, QuadConsts.ElmtIRgt), this.#content.get(elmt, QuadConsts.ElmtITop), this.#content.get(elmt, QuadConsts.ElmtIBtm));

View File

@ -1,5 +1,5 @@
import * as Three from 'three';
import { FRUSTUMSIZE } from '../consts';
import { RESOLUTION_X, RESOLUTION_Y } from '../consts';
import Stats from 'stats.js';
import { Point } from '../physics/common';
@ -65,17 +65,17 @@ export default class Renderer
this.camera.position.y += y;
}
static screenSpaceToCameraSpace(x: number, y: number, omit: boolean = false): Point {
return { x: ((x / window.innerWidth - 0.5) * FRUSTUMSIZE * this.aspect) / this.#zoom + (omit ? 0 : this.#pos.x), y: (- (y / window.innerHeight - 0.5) * FRUSTUMSIZE) / this.zoom + (omit ? 0 : this.#pos.y) };
return { x: ((x / window.innerWidth - 0.5) * RESOLUTION_X * this.aspect) / this.#zoom + (omit ? 0 : this.#pos.x), y: (- (y / window.innerHeight - 0.5) * RESOLUTION_Y) / this.zoom + (omit ? 0 : this.#pos.y) };
}
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.#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.left = RESOLUTION_X * aspect / - 2 / this.#zoom;
this.camera.right = RESOLUTION_X * aspect / 2 / this.#zoom;
this.camera.top = RESOLUTION_Y / 2 / this.#zoom;
this.camera.bottom = RESOLUTION_Y / - 2 / this.#zoom;
this.camera.updateProjectionMatrix();

View File

@ -49,7 +49,7 @@ export default class Selector
return;
}
Selector.#ghostMesh.box.setFromArray([asset.x1, asset.y1, 0, asset.x2, asset.y2, 0]);
Selector.#ghostMesh.box.setFromArray([asset.aabb.x1, asset.aabb.y1, 0, asset.aabb.x2, asset.aabb.y2, 0]);
Selector.#ghostMesh.updateMatrix();
Selector.#ghostMesh.visible = true;
@ -65,11 +65,11 @@ export default class Selector
Selector.#selected = true;
Selector.#selectionMesh.box.setFromArray([
assets.map(e => e.x1).reduce((p, v) => Math.min(p, v), Infinity),
assets.map(e => e.y1).reduce((p, v) => Math.min(p, v), Infinity),
assets.map(e => e.aabb.x1).reduce((p, v) => Math.min(p, v), Infinity),
assets.map(e => e.aabb.y1).reduce((p, v) => Math.min(p, v), Infinity),
0,
assets.map(e => e.x2).reduce((p, v) => Math.max(p, v), -Infinity),
assets.map(e => e.y2).reduce((p, v) => Math.max(p, v), -Infinity),
assets.map(e => e.aabb.x2).reduce((p, v) => Math.max(p, v), -Infinity),
assets.map(e => e.aabb.y2).reduce((p, v) => Math.max(p, v), -Infinity),
0
]);
Selector.#selectionMesh.updateMatrix();