333 lines
8.5 KiB
TypeScript
333 lines
8.5 KiB
TypeScript
import {
|
|
BufferGeometry,
|
|
Float32BufferAttribute,
|
|
Mesh,
|
|
Vector2,
|
|
Vector3,
|
|
} from 'three';
|
|
import { QuadtreeMesher } from './quadtree-mesher';
|
|
|
|
export enum LODQuadrant {
|
|
TOP_LEFT,
|
|
TOP_RIGHT,
|
|
BOTTOM_RIGHT,
|
|
BOTTOM_LEFT,
|
|
}
|
|
|
|
export enum LODSide {
|
|
TOP,
|
|
RIGHT,
|
|
BOTTOM,
|
|
LEFT,
|
|
}
|
|
|
|
export function mirrorSide(side: number): number {
|
|
return (side + 2) % 4;
|
|
}
|
|
|
|
export function isAdjacent(side: number, quadrant: number): boolean {
|
|
return (4 + quadrant - side) % 4 <= 1;
|
|
}
|
|
|
|
export function reflectSide(side: number, quadrant: number): number {
|
|
return side % 2 ? (quadrant % 2 ? quadrant - 1 : quadrant + 1) : 3 - quadrant;
|
|
}
|
|
|
|
export class QuadtreeNode {
|
|
private _children: QuadtreeNode[] = [];
|
|
private _neighbors: QuadtreeNode[] = [];
|
|
private _detailDifferences: number[] = [];
|
|
private _leaf = true;
|
|
private _mesh?: Mesh;
|
|
|
|
constructor(
|
|
public root: QuadtreeMesher,
|
|
public parent: QuadtreeNode,
|
|
public level: number,
|
|
public quadrant: number,
|
|
public position: Vector2,
|
|
) {}
|
|
|
|
public dispose() {
|
|
if (this._mesh) {
|
|
this._destroyMesh();
|
|
}
|
|
this._children.forEach((child) => child.dispose());
|
|
this._children.length = 0;
|
|
}
|
|
|
|
public isMeshed() {
|
|
return !!this._mesh;
|
|
}
|
|
|
|
public update(camera: Vector3) {
|
|
if (this.root.actionsLeft === 0) {
|
|
return;
|
|
}
|
|
|
|
const size = this.root.size / Math.pow(2, this.level);
|
|
const abs = new Vector3(
|
|
this.position.x + size / 2,
|
|
camera.y,
|
|
this.position.y + size / 2,
|
|
);
|
|
|
|
if (this._leaf) {
|
|
if (abs.distanceTo(camera) < size && this._canSubdivide()) {
|
|
this._subdivide();
|
|
this.root.actionsLeft -= 1;
|
|
return;
|
|
}
|
|
|
|
if (!this._mesh) {
|
|
this._createMesh();
|
|
this.root.actionsLeft -= 1;
|
|
return;
|
|
}
|
|
} else if (!this._leaf) {
|
|
if (abs.distanceTo(camera) > size) {
|
|
this._merge();
|
|
this.root.actionsLeft -= 1;
|
|
return;
|
|
}
|
|
|
|
if (
|
|
this.isMeshed() &&
|
|
this._children.every((child) => child.isMeshed())
|
|
) {
|
|
this._destroyMesh();
|
|
}
|
|
|
|
this._children.forEach((child) => child.update(camera));
|
|
}
|
|
}
|
|
|
|
public setNeighbor(side: LODSide, neighbor: QuadtreeNode) {
|
|
this._neighbors[side] = neighbor;
|
|
if (neighbor) {
|
|
const mirrored = mirrorSide(side);
|
|
neighbor._neighbors[mirrored] = this;
|
|
neighbor._updateNeighborDetailDifferences(mirrored);
|
|
}
|
|
this._updateNeighborDetailDifferences(side);
|
|
}
|
|
|
|
public findNeighbor(side: LODSide) {
|
|
if (!this._neighbors[side] && this.parent && this.parent._neighbors[side]) {
|
|
const neighbor =
|
|
this.parent._neighbors[side]?._children[
|
|
reflectSide(side, this.quadrant)
|
|
];
|
|
if (neighbor) {
|
|
this.setNeighbor(side, neighbor);
|
|
} else {
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!this._leaf) {
|
|
for (let i = 0; i < 4; i++) {
|
|
if (isAdjacent(side, i)) {
|
|
this._children[i].findNeighbor(side);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private _equalOrHigherNeighbor(side: LODSide): QuadtreeNode {
|
|
for (let node: QuadtreeNode = this; !!node; node = node.parent) {
|
|
if (node?._neighbors[side]) {
|
|
return node._neighbors[side];
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private _updateNeighborDetailDifferences(side: LODSide) {
|
|
for (let i = 0; i < 4; i++) {
|
|
const neighbor = this._equalOrHigherNeighbor(i);
|
|
if (neighbor) {
|
|
this._detailDifferences[i] = Math.min(this.level - neighbor.level, 4);
|
|
}
|
|
}
|
|
|
|
if (!this._leaf) {
|
|
for (let i = 0; i < 4; i++) {
|
|
if (isAdjacent(side, i))
|
|
this._children[i]._updateNeighborDetailDifferences(side);
|
|
}
|
|
}
|
|
}
|
|
|
|
private _createGeometry(): BufferGeometry {
|
|
const apparentSize = this.root.size / Math.pow(2, this.level);
|
|
const vertCount =
|
|
this.root.size / Math.pow(2, this.root.maxDepth - this.level);
|
|
const divisionLevel = Math.pow(2, this.level);
|
|
const geometry = new BufferGeometry();
|
|
const vertices = [];
|
|
const normals = [];
|
|
const indices = [];
|
|
const uvs = [];
|
|
|
|
for (let x = 0; x < vertCount; x++) {
|
|
for (let y = 0; y < vertCount; y++) {
|
|
const vertDivj = y / (vertCount - 1);
|
|
const vertDivi = x / (vertCount - 1);
|
|
|
|
const absX = this.position.x + vertDivj * apparentSize;
|
|
const absY = this.position.y + vertDivi * apparentSize;
|
|
const normal = this.root.getNormal(absX, absY);
|
|
const height = this.root.getHeight(absX, absY);
|
|
|
|
// Calculate relative resolution
|
|
const pj = vertDivj * (this.root.size / divisionLevel);
|
|
const pi = vertDivi * (this.root.size / divisionLevel);
|
|
|
|
vertices.push(pj, height, pi);
|
|
normals.push(normal.x, normal.y, normal.z);
|
|
uvs.push(absX / (this.root.size - 1), absY / (this.root.size - 1));
|
|
|
|
// Create quad
|
|
if (x !== vertCount - 1 && y !== vertCount - 1) {
|
|
const topLeft = x * vertCount + y;
|
|
const topRight = topLeft + 1;
|
|
const bottomLeft = (x + 1) * vertCount + y;
|
|
const bottomRight = bottomLeft + 1;
|
|
indices.push(
|
|
topLeft,
|
|
bottomLeft,
|
|
topRight,
|
|
topRight,
|
|
bottomLeft,
|
|
bottomRight,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// for (let test = 1; test < vertCount; test += 2) {
|
|
// // bottom
|
|
// vertices[test * 3 + 1] = vertices[test * 3 + 1] + 2;
|
|
// // top
|
|
// const testi = vertCount * vertCount * 3 - vertCount * 3 + test * 3;
|
|
// vertices[testi + 1] = vertices[testi + 1] + 2;
|
|
// // right
|
|
// const testi2 = vertCount * 3 * test;
|
|
// vertices[testi2 + 1] = vertices[testi2 + 1] + 2;
|
|
// // left
|
|
// const testi3 = vertCount * 3 * test + (vertCount - 1) * 3;
|
|
// vertices[testi3 + 1] = vertices[testi3 + 1] + 2;
|
|
// }
|
|
|
|
geometry.setIndex(indices);
|
|
geometry.setAttribute('position', new Float32BufferAttribute(vertices, 3));
|
|
geometry.setAttribute('normal', new Float32BufferAttribute(normals, 3));
|
|
geometry.setAttribute('uv', new Float32BufferAttribute(uvs, 2));
|
|
|
|
return geometry;
|
|
}
|
|
|
|
private _merge(): boolean {
|
|
if (this._leaf) {
|
|
return false;
|
|
}
|
|
|
|
this._children.forEach((child) => child.dispose());
|
|
this._children.length = 0;
|
|
this._leaf = true;
|
|
return true;
|
|
}
|
|
|
|
private _subdivide(): boolean {
|
|
if (!this._canSubdivide()) {
|
|
return false;
|
|
}
|
|
|
|
const level = this.level + 1;
|
|
const stepLeft = this.root.size / Math.pow(2, level);
|
|
const stepForward = this.root.size / Math.pow(2, level);
|
|
|
|
const { x, y } = this.position;
|
|
|
|
// Create children
|
|
this._children[LODQuadrant.TOP_LEFT] = new QuadtreeNode(
|
|
this.root,
|
|
this,
|
|
level,
|
|
LODQuadrant.TOP_LEFT,
|
|
new Vector2(x, y),
|
|
);
|
|
this._children[LODQuadrant.TOP_RIGHT] = new QuadtreeNode(
|
|
this.root,
|
|
this,
|
|
level,
|
|
LODQuadrant.TOP_RIGHT,
|
|
new Vector2(stepLeft + x, y),
|
|
);
|
|
this._children[LODQuadrant.BOTTOM_RIGHT] = new QuadtreeNode(
|
|
this.root,
|
|
this,
|
|
level,
|
|
LODQuadrant.BOTTOM_RIGHT,
|
|
new Vector2(stepLeft + x, stepForward + y),
|
|
);
|
|
this._children[LODQuadrant.BOTTOM_LEFT] = new QuadtreeNode(
|
|
this.root,
|
|
this,
|
|
level,
|
|
LODQuadrant.BOTTOM_LEFT,
|
|
new Vector2(x, stepForward + y),
|
|
);
|
|
|
|
// Set sibling neighbors
|
|
this._children[LODQuadrant.TOP_LEFT].setNeighbor(
|
|
LODSide.RIGHT,
|
|
this._children[LODQuadrant.TOP_RIGHT],
|
|
);
|
|
this._children[LODQuadrant.TOP_RIGHT].setNeighbor(
|
|
LODSide.BOTTOM,
|
|
this._children[LODQuadrant.BOTTOM_RIGHT],
|
|
);
|
|
this._children[LODQuadrant.BOTTOM_RIGHT].setNeighbor(
|
|
LODSide.LEFT,
|
|
this._children[LODQuadrant.BOTTOM_LEFT],
|
|
);
|
|
this._children[LODQuadrant.BOTTOM_LEFT].setNeighbor(
|
|
LODSide.TOP,
|
|
this._children[LODQuadrant.TOP_LEFT],
|
|
);
|
|
|
|
// set adjacent neighbors
|
|
for (let i = 0; i < 4; i++) {
|
|
if (this._neighbors[i] && !this._neighbors[i]._leaf) {
|
|
this._neighbors[i].findNeighbor(mirrorSide(i));
|
|
}
|
|
}
|
|
|
|
this._leaf = false;
|
|
return true;
|
|
}
|
|
|
|
private _createMesh() {
|
|
if (this._mesh) {
|
|
this._destroyMesh();
|
|
}
|
|
|
|
const geometry = this._createGeometry();
|
|
const mesh = new Mesh(geometry, this.root.material);
|
|
mesh.position.set(this.position.x, 0, this.position.y);
|
|
this.root.container.add(mesh);
|
|
this._mesh = mesh;
|
|
}
|
|
|
|
private _destroyMesh() {
|
|
this.root.container.remove(this._mesh);
|
|
this._mesh = null;
|
|
}
|
|
|
|
private _canSubdivide() {
|
|
return this._leaf && this.level < this.root.maxDepth - 1;
|
|
}
|
|
}
|