quadtree terrain
This commit is contained in:
parent
42a4358df1
commit
8232dcebd2
@ -96,7 +96,7 @@ export class Game {
|
||||
this.thirdPersonCamera?.update(dt);
|
||||
this.joystick?.update(dt);
|
||||
|
||||
this.world?.update(dt);
|
||||
this.player && this.world?.update(this.player.container.position);
|
||||
}
|
||||
|
||||
bindSocket() {
|
||||
|
@ -1,19 +1,17 @@
|
||||
import { Mesh, Object3D, Vector3 } from 'three';
|
||||
import { Object3D, Vector2, Vector3 } from 'three';
|
||||
import { WorldChunk } from '../../../common/world/WorldChunk';
|
||||
import { WorldManager } from '../../../common/world/WorldManager';
|
||||
import { ClientWorldChunkShader } from './ClientWorldChunkShader';
|
||||
import { ClientWorldMesher } from './ClientWorldMesher';
|
||||
import { ClientWorldTexture } from './ClientWorldTexture';
|
||||
import { QuadtreeMesher } from './quadtree/quadtree-mesher';
|
||||
|
||||
// TODO: distance loading
|
||||
// TODO: LOD
|
||||
|
||||
const BASE = '/assets/terrain/';
|
||||
|
||||
export class ClientWorld extends WorldManager {
|
||||
public world = new Object3D();
|
||||
private _mesher = new ClientWorldMesher();
|
||||
private _chunkMeshes: Mesh[] = [];
|
||||
private _chunkMeshers: QuadtreeMesher[] = [];
|
||||
private _chunkMeshQueue: WorldChunk[] = [];
|
||||
private _worldTextures: Map<string, ClientWorldTexture> = new Map();
|
||||
private _shader = new ClientWorldChunkShader(this._worldTextures);
|
||||
@ -62,7 +60,7 @@ export class ClientWorld extends WorldManager {
|
||||
}
|
||||
}
|
||||
|
||||
update(dt: number) {
|
||||
update(camera: Vector3) {
|
||||
if (this._chunkMeshQueue.length) {
|
||||
const chunk = this._chunkMeshQueue.shift();
|
||||
const material = this._shader.getShader(
|
||||
@ -71,16 +69,22 @@ export class ClientWorld extends WorldManager {
|
||||
chunk.region.splat.map((file) => `${BASE}/texture/${file}`),
|
||||
);
|
||||
|
||||
const mesh = this._mesher.createTerrainMesh(
|
||||
chunk,
|
||||
const root = new QuadtreeMesher(
|
||||
chunk.size,
|
||||
new Vector2(chunk.size * chunk.x, chunk.size * chunk.y),
|
||||
material,
|
||||
this.getHeight.bind(this),
|
||||
this.getNormalVector.bind(this),
|
||||
);
|
||||
|
||||
this._chunkMeshes.push(mesh);
|
||||
this.world.add(mesh);
|
||||
root.getHeight = this.getInterpolatedHeight.bind(this);
|
||||
root.getNormal = this.getNormalVector.bind(this);
|
||||
root.initialize();
|
||||
|
||||
this._chunkMeshers.push(root);
|
||||
this.world.add(root.container);
|
||||
return;
|
||||
}
|
||||
|
||||
this._chunkMeshers.forEach((item) => item.update(camera));
|
||||
}
|
||||
|
||||
private createMeshes() {
|
||||
|
@ -1,75 +0,0 @@
|
||||
import {
|
||||
BufferGeometry,
|
||||
Float32BufferAttribute,
|
||||
Material,
|
||||
Mesh,
|
||||
Vector3,
|
||||
} from 'three';
|
||||
import { WorldChunk } from '../../../common/world/WorldChunk';
|
||||
|
||||
export class ClientWorldMesher {
|
||||
public createGeometry(
|
||||
chunk: WorldChunk,
|
||||
getHeight: (x: number, y: number) => number,
|
||||
getNormal: (x: number, y: number) => Vector3,
|
||||
): BufferGeometry {
|
||||
const geometry = new BufferGeometry();
|
||||
const vertices = [];
|
||||
const normals = [];
|
||||
const indices = [];
|
||||
const uvs = [];
|
||||
for (let x = 0; x < chunk.size; x++) {
|
||||
for (let y = 0; y < chunk.size; y++) {
|
||||
const normal = getNormal(y, x);
|
||||
vertices.push(
|
||||
(y / chunk.size - 1) * chunk.size,
|
||||
getHeight(y, x),
|
||||
(x / chunk.size - 1) * chunk.size,
|
||||
);
|
||||
normals.push(normal.x, normal.y, normal.z);
|
||||
uvs.push(y / (chunk.size - 1), x / (chunk.size - 1));
|
||||
}
|
||||
}
|
||||
|
||||
for (let x = 0; x < chunk.size - 1; x++) {
|
||||
for (let y = 0; y < chunk.size - 1; y++) {
|
||||
const topLeft = x * chunk.size + y;
|
||||
const topRight = topLeft + 1;
|
||||
const bottomLeft = (x + 1) * chunk.size + y;
|
||||
const bottomRight = bottomLeft + 1;
|
||||
indices.push(
|
||||
topLeft,
|
||||
bottomLeft,
|
||||
topRight,
|
||||
topRight,
|
||||
bottomLeft,
|
||||
bottomRight,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public createTerrainMesh(
|
||||
chunk: WorldChunk,
|
||||
material: Material,
|
||||
getHeight: (x: number, y: number) => number,
|
||||
getNormal: (x: number, y: number) => Vector3,
|
||||
): Mesh {
|
||||
const geometry = this.createGeometry(chunk, getHeight, getNormal);
|
||||
const mesh = new Mesh(geometry, material);
|
||||
mesh.position.set(
|
||||
chunk.size * (chunk.x + 1),
|
||||
0,
|
||||
chunk.size * (chunk.y + 1),
|
||||
);
|
||||
|
||||
return mesh;
|
||||
}
|
||||
}
|
26
src/client/object/world/quadtree/quadtree-mesher.ts
Normal file
26
src/client/object/world/quadtree/quadtree-mesher.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { Material, Object3D, Vector2, Vector3 } from 'three';
|
||||
import { QuadtreeNode } from './quadtree-node';
|
||||
|
||||
export class QuadtreeMesher {
|
||||
public getHeight!: (x: number, y: number) => number;
|
||||
public getNormal!: (x: number, y: number) => Vector3;
|
||||
public root!: QuadtreeNode;
|
||||
public container = new Object3D();
|
||||
public actionsLeft = 1;
|
||||
public maxDepth = 3;
|
||||
|
||||
constructor(
|
||||
public size: number,
|
||||
public position: Vector2,
|
||||
public material: Material,
|
||||
) {}
|
||||
|
||||
public update(camera: Vector3) {
|
||||
this.actionsLeft = 1;
|
||||
this.root?.update(camera);
|
||||
}
|
||||
|
||||
public initialize() {
|
||||
this.root = new QuadtreeNode(this, null, 0, 0, this.position.clone());
|
||||
}
|
||||
}
|
286
src/client/object/world/quadtree/quadtree-node.ts
Normal file
286
src/client/object/world/quadtree/quadtree-node.ts
Normal file
@ -0,0 +1,286 @@
|
||||
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(x: number): number {
|
||||
return (x + 2) % 4;
|
||||
}
|
||||
|
||||
export function isAdjacent(s: number, q: number): boolean {
|
||||
return (4 + q - s) % 4 <= 1;
|
||||
}
|
||||
|
||||
export function reflectSide(s: number, q: number): number {
|
||||
return s % 2 ? (q % 2 ? q - 1 : q + 1) : 3 - q;
|
||||
}
|
||||
|
||||
export class QuadtreeNode {
|
||||
public _children: QuadtreeNode[] = [];
|
||||
public _neighbors: QuadtreeNode[] = [];
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
neighbor._neighbors[mirrorSide(side)] = this;
|
||||
}
|
||||
|
||||
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 _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 + (y / (vertCount - 1)) * apparentSize;
|
||||
const absY = this.position.y + (x / (vertCount - 1)) * apparentSize;
|
||||
const normal = this.root.getNormal(absX, absY);
|
||||
|
||||
// Calculate relative resolution
|
||||
const pj = vertDivj * (this.root.size / divisionLevel);
|
||||
const pi = vertDivi * (this.root.size / divisionLevel);
|
||||
|
||||
vertices.push(pj, this.root.getHeight(absX, absY), pi);
|
||||
normals.push(normal.x, normal.y, normal.z);
|
||||
uvs.push(absX / (this.root.size - 1), absY / (this.root.size - 1));
|
||||
}
|
||||
}
|
||||
|
||||
for (let x = 0; x < vertCount - 1; x++) {
|
||||
for (let y = 0; y < vertCount - 1; y++) {
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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.root.container.remove(this._mesh);
|
||||
this._mesh = null;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user