diff --git a/bun.lockb b/bun.lockb index 7829147..43bc94e 100644 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 3ba1189..f5d2eaf 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ }, "dependencies": { "@types/three": "^0.165.0", - "three": "^0.165.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 34a02a6..d20bee2 100644 --- a/src/assets/asset.class.ts +++ b/src/assets/asset.class.ts @@ -1,18 +1,115 @@ import * as Three from 'three'; +import * as CONST from '../consts'; +import { AABB } from '../physics/common'; -export default class Asset +const UP = new Three.Vector3(0, 0, 1); + +const _position = new Three.Vector3(); +const _rotation = new Three.Quaternion(); +const _euler = new Three.Euler(); +const _scale = new Three.Vector3(); + +export default class Asset implements AABB { - #mat: Three.Matrix4; - #layer: number; + mat: Three.Matrix4; + layer: number; - ready: boolean = false; + selected: boolean = false; - _obj: Three.Object3D | undefined; + static instance = new Three.InstancedMesh(CONST.QUAD, new Three.MeshBasicMaterial({ color: new Three.Color( 0xffffff ) }), 2**14); constructor(mat?: Three.Matrix4, layer?: number) { - this.#mat = mat ?? new Three.Matrix4(); - this.#layer = layer ?? 0; + this.mat = mat ?? new Three.Matrix4(); + 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 ] )); + } + 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 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 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 ] )); + } + move(x: number, y: number): Asset + { + this.mat.decompose(_position, _rotation, _scale); + + _position.x += x; + _position.y += y; + + this.mat.compose(_position, _rotation, _scale); + + return this; + } + moveTo(x: number, y: number): Asset + { + this.mat.decompose(_position, _rotation, _scale); + + _position.x = x; + _position.y = y; + + this.mat.compose(_position, _rotation, _scale); + + return this; + } + rotate(rad: number): Asset + { + this.mat.decompose(_position, _rotation, _scale); + + _euler.setFromQuaternion(_rotation); + _euler.z += rad; + _rotation.setFromEuler(_euler); + + this.mat.compose(_position, _rotation, _scale); + + return this; + } + rotateTo(rad: number): Asset + { + this.mat.decompose(_position, _rotation, _scale); + + _euler.setFromQuaternion(_rotation); + _euler.z = rad; + _rotation.setFromEuler(_euler); + + this.mat.compose(_position, _rotation, _scale); + + return this; + } + scale(x: number, y: number): Asset + { + this.mat.decompose(_position, _rotation, _scale); + + _scale.x *= x; + _scale.y *= y; + + this.mat.compose(_position, _rotation, _scale); + + return this; + } + scaleTo(x: number, y: number): Asset + { + this.mat.decompose(_position, _rotation, _scale); + + _scale.x = x; + _scale.y = y; + + this.mat.compose(_position, _rotation, _scale); + + return this; + } + shapeTo(x1: number, y1: number, x2: number, y2: number, x3: number, y3: number, x4: number, y4: number): Asset + { + + return this; } - static init(): void {} } \ No newline at end of file diff --git a/src/assets/sprite.class.ts b/src/assets/sprite.class.ts index b1ea974..22b5698 100644 --- a/src/assets/sprite.class.ts +++ b/src/assets/sprite.class.ts @@ -1,13 +1,5 @@ import Asset from './asset.class'; -import * as CONST from '../consts'; -import * as Three from 'three'; export default class Sprite extends Asset { - static #material = new Three.RawShaderMaterial({ - fragmentShader: "", - vertexShader: "", - }); - static #mesh = CONST.QUAD; - static #instance = new Three.InstancedMesh(CONST.QUAD, Sprite.#material, 2**14); } \ No newline at end of file diff --git a/src/common.ts b/src/common.ts new file mode 100644 index 0000000..703743b --- /dev/null +++ b/src/common.ts @@ -0,0 +1,210 @@ +const DEFAULT_BUCKET_SIZE = 64; + + +export class FastStack +{ + #arr: T[]; + + #pos: number; //Index of the last non empty value + + #bucketSize: number; + #bucketCount: number = 1; + + constructor(size?: number) + { + this.#bucketSize = size ?? DEFAULT_BUCKET_SIZE; + + this.#arr = new Array(this.#bucketSize * this.#bucketCount); + this.#pos = 0; + } + push(item: T): void + { + if(this.#pos >= this.#arr.length) + this.#expand(); + + this.#arr[this.#pos] = item; + this.#pos++; + } + pop(): T + { + if(this.length === 0) + throw new Error("Empty queue."); + + const item = this.#arr[this.#pos]; + delete this.#arr[this.#pos]; + this.#pos--; + + return item; + } + peek(): T + { + return this.#arr[this.#pos]; + } + clear(): void + { + this.#pos = 0; + } + get length(): number + { + return this.#pos; + } + #expand(): void + { + if(this.length >= this.#bucketSize * this.#bucketCount) + this.#bucketCount++; + + this.#arr.length = this.#bucketSize * this.#bucketCount; + } +} +export class FastQueue +{ + #arr: T[]; + + #idx: number; //Index of the first non empty value + #pos: number; //Index of the last non empty value + + #bucketSize: number; + #bucketCount: number = 1; + + constructor(size?: number) + { + this.#bucketSize = size ?? DEFAULT_BUCKET_SIZE; + + this.#arr = new Array(this.#bucketSize * this.#bucketCount); + this.#idx = 0; + this.#pos = 0; + } + push(item: T): void + { + if(this.#pos >= this.#arr.length) + this.#expand(); + + this.#arr[this.#pos] = item; + this.#pos++; + } + pull(): T + { + if(this.length === 0) + throw new Error("Empty queue."); + + const item = this.#arr[this.#idx]; + delete this.#arr[this.#idx]; + this.#idx++; + + return item; + } + peek(): T + { + return this.#arr[this.#idx]; + } + clear(): void + { + this.#idx = 0; + this.#pos = 0; + } + get length(): number + { + return this.#pos - this.#idx; + } + #shrink(): void + { + this.#arr.splice(0, this.#idx); + + this.#arr.length = this.#bucketSize * this.#bucketCount; + this.#idx = 0; + } + #expand(): void + { + if(this.#idx !== 0) + this.#shrink(); + + if(this.length >= this.#bucketSize * this.#bucketCount) + this.#bucketCount++; + + this.#arr.length = this.#bucketSize * this.#bucketCount; + } +} +export class LinkedList +{ + #head: LinkedElmt | null; + #tail: LinkedElmt | null; //Extension + constructor() + { + this.#head = null; + this.#tail = null; + } + add(item: T): void + { + const tail = this.#tail; + + this.#tail = { elmt: item, next: null, prev: tail }; + + if(tail) tail.next = this.#tail; + else this.#head = this.#tail; + } + pop(): T | null + { + if(this.#head === null) + return null; + + const head = this.#head; + this.#head = head.next; + + if(head === this.#tail) this.#tail = null; + if(head.next) head.next.prev = null; + if(head.next && head.next === this.#tail) this.#tail = head.next; + + return head.elmt; + } + remove(item: T): boolean + { + let current = this.#head; + while(current) + { + if(current.elmt === item) + { + const prev = current.prev; + const next = current.next; + + if(prev) prev.next = next; + if(next) next.prev = prev; + + return true; + } + current = current.next; + } + + return false; + } + forEach(cb: (e: T, i: number) => void): void + { + let current = this.#head, i = 0; + while(current) + { + cb(current.elmt, i++); + + current = current.next; + } + } + toArray(): T[] + { + const result: T[] = []; + this.forEach(e => result.push(e)); + return result; + } + clear() + { + if(this.#head) this.#head.next = null; + if(this.#tail) this.#tail.prev = null; + + this.#head = null; + this.#tail = null; + } +} +interface LinkedElmt +{ + elmt: T; + + next: LinkedElmt | null; + prev: LinkedElmt | null; +} \ No newline at end of file diff --git a/src/consts.ts b/src/consts.ts index 774d80a..dbf7a7d 100644 --- a/src/consts.ts +++ b/src/consts.ts @@ -1,11 +1,15 @@ import * as Three from 'three'; const QUAD = new Three.BufferGeometry(); -QUAD.setIndex( new Three.Float32BufferAttribute( [ 0, 2, 1, 2, 3, 1 ], 1 ) ) +QUAD.setIndex( new Three.Uint16BufferAttribute( [ 0, 2, 1, 2, 3, 1 ], 1 ) ) QUAD.setAttribute( 'position', new Three.Float32BufferAttribute( [ -0.5, 0.5, 0, 0.5, 0.5, 0, -0.5, -0.5, 0, 0.5, -0.5, 0 ], 3 ) ); QUAD.setAttribute( 'normal', new Three.Float32BufferAttribute( [ 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1 ], 3 ) ); QUAD.setAttribute( 'uv', new Three.Float32BufferAttribute( [ 0, 1, 1, 1, 0, 0, 1, 0 ], 2 ) ); export { QUAD -}; \ No newline at end of file +}; + +export const FRUSTUMSIZE = 16; + +export const MAX_DEPTH = 8; \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 8cc6107..03acbcc 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,3 +1,54 @@ +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'; -Renderer.init(); \ No newline at end of file +Renderer.init(); +const quad = new Quadtree({x1: -FRUSTUMSIZE * Renderer.aspect, x2: FRUSTUMSIZE * Renderer.aspect, y1: -FRUSTUMSIZE, y2: FRUSTUMSIZE}); + +window.Asset = Asset; + +const assets: Asset[] = []; +for(let i = 0; i < 1; 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) + + 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); +} + +Asset.instance.count = assets.length; + +Asset.instance.computeBoundingBox(); +Asset.instance.computeBoundingSphere(); + +console.log(Asset.instance.boundingBox); + +Renderer.scene.add(Asset.instance); +Renderer.render(); + +window.addEventListener('mousedown', drag); +window.addEventListener('mouseup', select); +window.addEventListener('mousemove', hover); + +function drag(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); +} +function hover(e: MouseEvent): void +{ + +} \ No newline at end of file diff --git a/src/physics/common.ts b/src/physics/common.ts new file mode 100644 index 0000000..9e9ff9d --- /dev/null +++ b/src/physics/common.ts @@ -0,0 +1,28 @@ +export interface Point +{ + x: number; + y: number; +} +export interface AABB +{ + x1: number; + y1: number; + x2: number; + y2: number; +} + +export function intersects(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; + } +} \ No newline at end of file diff --git a/src/physics/quadtree.class.ts b/src/physics/quadtree.class.ts new file mode 100644 index 0000000..42d730e --- /dev/null +++ b/src/physics/quadtree.class.ts @@ -0,0 +1,185 @@ +import { LinkedList } from "../common"; +import { AABB, Point, intersects } from "./common"; + +export default class Quadtree +{ + #bounds: AABB; + #maxDepth: number = 8; + #maxElmts: number = 4; + #nodes: Node[]; + #content: T[]; + + #dirty: boolean = false; + + constructor(bounds: AABB, maxDepth?: number, maxElmts?: number) + { + this.#bounds = bounds; + this.#maxDepth = maxDepth ?? this.#maxDepth; + this.#maxElmts = maxElmts ?? this.#maxElmts; + + this.#nodes = []; + this.#content = []; + + this.#nodes.push({ children: new LinkedList(), count: 0 }); + } + fetch(point: Point): T[] + { + return this.query({x1: point.x, x2: point.x, y1: point.y, y2: point.y}); + } + query(aabb: AABB): T[] + { + const result: number[] = []; + + const leaves = this.#find_leaves(0, 0, this.#bounds, aabb); + for(let i = 0; i < leaves.length; i++) + { + const node = this.#nodes[leaves[i].index]; + node.children.forEach(e => { + if(!result.includes(e) && intersects(this.#content[e], aabb)) + result.push(e); + }); + } + + return result.map(e => this.#content[e]); + } + insert(item: T): number + { + const idx = this.#content.push(item) - 1; + this.#node_insert(0, 0, this.#bounds, idx); + return idx; + } + remove(index: number): void + { + if(index >= this.#content.length) + throw new Error("Out of bound exception."); + + const elmt = this.#content[index]; + const leaves = this.#find_leaves(0, 0, this.#bounds, elmt); + + for(let i = 0; i < leaves.length; i++) + { + if(this.#nodes[leaves[i].index].children.remove(index)) + this.#nodes[leaves[i].index].count--; + } + this.#dirty = true; + } + cleanup(): void + { + //Only cleanup if it's dirty. + //Allows the system to call the function at each loop iteration. + if(!this.#dirty) + return; + + const stack: number[] = []; + + if(this.#nodes[0].count) + stack.push(0); + + while(stack.length !== 0) + { + const node = this.#nodes[stack.pop()!]; + + let empty_leaves = 0; + + node.children.forEach(e => { + const child = this.#nodes[e]; + + if(child.count === 0) + empty_leaves++; + else if(child.count === -1) + stack.push(e); + }); + + if(empty_leaves === 4) + { + node.count = 0; + node.children.clear(); + } + } + } + + #find_leaves(index: number, depth: number, bounds: AABB, elmtBounds: AABB): NodeProp[] + { + const stack: NodeProp[] = [], result: NodeProp[] = []; + stack.push({index: index, depth: depth, bounds: bounds}); + + //Fetch every nodes intersecting the element bounds + while(stack.length > 0) + { + const nodeProp = stack.pop()!; + + //If the node contains elements + if(this.#nodes[nodeProp.index].count !== -1) + result.push({index: index, depth: depth, bounds: bounds}); + 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; + + 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 }}); + if(elmtBounds.x2 > mx) + stack.push({ index: children[1], depth: depth + 1, bounds: { x1: mx, x2: bounds.x2, y1: 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 }}); + if(elmtBounds.x2 > mx) + stack.push({ index: children[3], depth: depth + 1, bounds: { x1: mx, x2: bounds.x2, y1: my, y2: bounds.y2 }}); + } + } + } + return result; + } + #leaf_insert(index: number, depth: number, bounds: AABB, elmt: number): void + { + const node = this.#nodes[index]; + node.children.add(elmt); + + //Split if the max amount of element is reached and the max depth isn't + if(node.count == this.#maxElmts && depth < this.#maxDepth) + { + const elmts = node.children.toArray(); + node.children.clear(); + node.count = -1; + + for(let i = 0; i < 4; i++) + { + node.children.add(this.#nodes.push({ children: new LinkedList(), count: 0 }) - 1); + } + for(let i = 0; i < elmts.length; i++) + { + this.#node_insert(index, depth, bounds, elmts[i]); + } + } + else //Otherwise, just increase the count + { + node.count++; + } + } + #node_insert(index: number, depth: number, bounds: AABB, elmt: number): void + { + const aabb = this.#content[elmt]; + const leaves = this.#find_leaves(index, depth, bounds, aabb); + + for(let i = 0; i < leaves.length; i++) + { + this.#leaf_insert(leaves[i].index, leaves[i].depth, leaves[i].bounds, elmt); + } + } +} +interface NodeProp +{ + index: number; + depth: number; + bounds: AABB; +} +interface Node +{ + children: LinkedList; + count: number; //The count is used to get the amount of T elements in the current node. If the Node only contains other nodes, the count is = -1. +} \ No newline at end of file diff --git a/src/renderer/renderer.class.ts b/src/renderer/renderer.class.ts index 57fc33f..4f4d039 100644 --- a/src/renderer/renderer.class.ts +++ b/src/renderer/renderer.class.ts @@ -1,22 +1,23 @@ import * as Three from 'three'; -import Asset from '../assets/asset.class'; +import { FRUSTUMSIZE } from '../consts'; export default class Renderer { - static #scene: Three.Scene; - static #camera: Three.OrthographicCamera; + static scene: Three.Scene; + static aspect: number; + + static renderer: Three.WebGLRenderer; + static camera: Three.OrthographicCamera; static init(): Boolean { try { - const canvas = document.createElement("canvas"); - canvas.addEventListener("webglcontextcreationerror", console.error); - const context = canvas.getContext("webgl2"); - - this.#renderer.setPixelRatio( window.devicePixelRatio ); - document.body.appendChild(this.#renderer.domElement); + this.renderer = new Three.WebGLRenderer({ antialias: true }); + this.renderer.setPixelRatio( window.devicePixelRatio ); + document.body.appendChild(this.renderer.domElement); - this.#scene = new Three.Scene(); - this.#camera = new Three.OrthographicCamera( window.innerWidth / - 2, window.innerWidth / 2, window.innerHeight / 2, window.innerHeight / - 2, 1, 1000 ); + this.scene = new Three.Scene(); + this.camera = new Three.OrthographicCamera(); + this.camera.position.z = 500; this.#resize(); window.addEventListener("resize", this.#resize.bind(this)); @@ -31,17 +32,28 @@ export default class Renderer } static #resize(): void { - this.#renderer.setSize( window.innerWidth, window.innerHeight ); - this.#camera.left = window.innerWidth / - 2; - this.#camera.right = window.innerWidth / 2; - this.#camera.top = window.innerHeight / 2; - this.#camera.bottom = window.innerHeight / - 2; + 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.updateProjectionMatrix(); this.render(); } - static render(): void + static render(delta?: number): void { - console.log(new Three.PlaneGeometry()); - this.#renderer.render(this.#scene, this.#camera); + this.renderer.render(this.scene, this.camera); + } + static startRendering(): void + { + this.renderer.setAnimationLoop(Renderer.render.bind(Renderer)); + } + static stopRendering(): void + { + this.renderer.setAnimationLoop(null); } } \ No newline at end of file