Physics tests

This commit is contained in:
Peaceultime 2024-06-10 17:45:32 +02:00
parent 98e78ca33e
commit 6d20041842
10 changed files with 619 additions and 39 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",
"three": "^0.165.0" "three": "^0.165.0",
"three-mesh-bvh": "^0.7.5"
} }
} }

View File

@ -1,18 +1,115 @@
import * as Three from 'three'; 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; mat: Three.Matrix4;
#layer: number; 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) constructor(mat?: Three.Matrix4, layer?: number)
{ {
this.#mat = mat ?? new Three.Matrix4(); this.mat = mat ?? new Three.Matrix4();
this.#layer = layer ?? 0; 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 {}
} }

View File

@ -1,13 +1,5 @@
import Asset from './asset.class'; import Asset from './asset.class';
import * as CONST from '../consts';
import * as Three from 'three';
export default class Sprite extends Asset 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);
} }

210
src/common.ts Normal file
View File

@ -0,0 +1,210 @@
const DEFAULT_BUCKET_SIZE = 64;
export class FastStack<T>
{
#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<T>
{
#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<T>
{
#head: LinkedElmt<T> | null;
#tail: LinkedElmt<T> | 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<T>
{
elmt: T;
next: LinkedElmt<T> | null;
prev: LinkedElmt<T> | null;
}

View File

@ -1,11 +1,15 @@
import * as Three from 'three'; import * as Three from 'three';
const QUAD = new Three.BufferGeometry(); 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( '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( '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 ) ); QUAD.setAttribute( 'uv', new Three.Float32BufferAttribute( [ 0, 1, 1, 1, 0, 0, 1, 0 ], 2 ) );
export { export {
QUAD QUAD
}; };
export const FRUSTUMSIZE = 16;
export const MAX_DEPTH = 8;

View File

@ -1,3 +1,54 @@
import * as THREE from 'three';
import Renderer from './renderer/renderer.class'; import Renderer from './renderer/renderer.class';
import Asset from './assets/asset.class';
import Quadtree from './physics/quadtree.class';
import { FRUSTUMSIZE } from './consts';
Renderer.init(); 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
{
}

28
src/physics/common.ts Normal file
View File

@ -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;
}
}

View File

@ -0,0 +1,185 @@
import { LinkedList } from "../common";
import { AABB, Point, intersects } from "./common";
export default class Quadtree<T extends AABB>
{
#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<number>(), 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<number>(), 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<number>;
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.
}

View File

@ -1,22 +1,23 @@
import * as Three from 'three'; import * as Three from 'three';
import Asset from '../assets/asset.class'; import { FRUSTUMSIZE } from '../consts';
export default class Renderer export default class Renderer
{ {
static #scene: Three.Scene; static scene: Three.Scene;
static #camera: Three.OrthographicCamera; static aspect: number;
static renderer: Three.WebGLRenderer;
static camera: Three.OrthographicCamera;
static init(): Boolean static init(): Boolean
{ {
try { try {
const canvas = document.createElement("canvas"); this.renderer = new Three.WebGLRenderer({ antialias: true });
canvas.addEventListener("webglcontextcreationerror", console.error); this.renderer.setPixelRatio( window.devicePixelRatio );
const context = canvas.getContext("webgl2"); document.body.appendChild(this.renderer.domElement);
this.#renderer.setPixelRatio( window.devicePixelRatio );
document.body.appendChild(this.#renderer.domElement);
this.#scene = new Three.Scene(); 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.camera = new Three.OrthographicCamera();
this.camera.position.z = 500;
this.#resize(); this.#resize();
window.addEventListener("resize", this.#resize.bind(this)); window.addEventListener("resize", this.#resize.bind(this));
@ -31,17 +32,28 @@ export default class Renderer
} }
static #resize(): void static #resize(): void
{ {
this.#renderer.setSize( window.innerWidth, window.innerHeight ); const aspect = this.aspect = window.innerWidth / window.innerHeight;
this.#camera.left = window.innerWidth / - 2; this.renderer.setSize( window.innerWidth, window.innerHeight );
this.#camera.right = window.innerWidth / 2;
this.#camera.top = window.innerHeight / 2; this.camera.left = FRUSTUMSIZE * aspect / - 2;
this.#camera.bottom = window.innerHeight / - 2; this.camera.right = FRUSTUMSIZE * aspect / 2;
this.camera.top = FRUSTUMSIZE / 2;
this.camera.bottom = FRUSTUMSIZE / - 2;
this.camera.updateProjectionMatrix();
this.render(); 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);
} }
} }