@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
622 lines (497 loc) • 19.3 kB
JavaScript
import { mat4 } from "gl-matrix";
import {
Box3 as ThreeBox3,
BufferAttribute as ThreeBufferAttribute,
BufferGeometry as ThreeBufferGeometry,
MeshBasicMaterial,
Sphere as ThreeSphere,
Vector3 as ThreeVector3
} from 'three';
import { BinaryUint32BVH } from "../../../../core/bvh2/binary/2/BinaryUint32BVH.js";
import { BvhClient } from "../../../../core/bvh2/bvh3/BvhClient.js";
import { array_copy } from "../../../../core/collection/array/array_copy.js";
import Signal from "../../../../core/events/signal/Signal.js";
import { AABB3 } from "../../../../core/geom/3d/aabb/AABB3.js";
import { ray3_array_apply_matrix4 } from "../../../../core/geom/3d/ray/ray3_array_apply_matrix4.js";
import { ray3_array_compose } from "../../../../core/geom/3d/ray/ray3_array_compose.js";
import { SurfacePoint3 } from "../../../../core/geom/3d/SurfacePoint3.js";
import Vector2 from '../../../../core/geom/Vector2.js';
import Vector3 from '../../../../core/geom/Vector3.js';
import { NumericInterval } from "../../../../core/math/interval/NumericInterval.js";
import ObservedInteger from "../../../../core/model/ObservedInteger.js";
import { bvh32_geometry_raycast } from "../../../graphics/geometry/buffered/query/bvh32_geometry_raycast.js";
import ThreeFactory from '../../../graphics/three/ThreeFactory.js';
const EMPTY_GEOMETRY = new ThreeBufferGeometry();
const DEFAULT_MATERIAL = new MeshBasicMaterial();
const ray_tmp = [];
const m4_tmp = [];
/**
* terrain tile is a part of a 2d array
*/
class TerrainTile {
gridPosition = new Vector2();
scale = new Vector2(1, 1);
size = new Vector2(1, 1);
position = new Vector2();
resolution = new ObservedInteger(1);
/**
*
* @type {Material}
*/
material = null;
mesh = ThreeFactory.createMesh(EMPTY_GEOMETRY, DEFAULT_MATERIAL);
/**
*
* @type {THREE.BufferGeometry}
*/
geometry = null;
/**
*
* @type {boolean}
*/
enableBVH = true;
/**
*
* @type {BvhClient}
*/
external_bvh = new BvhClient();
/**
*
* @type {BinaryUint32BVH}
*/
bvh = null;
/**
*
* @type {boolean}
*/
isBuilt = false;
/**
*
* @type {boolean}
*/
isBuildInProgress = false;
referenceCount = 0;
/**
*
* @type {Signal<TerrainTile>}
*/
onBuilt = new Signal();
onDestroyed = new Signal();
/**
* Encodes whether stitching has been performed on per-neighbour basis
* @private
* @type {{bottomLeft: boolean, top: boolean, left: boolean, bottom: boolean, bottomRight: boolean, topLeft: boolean, topRight: boolean, right: boolean}}
*/
stitching = {
top: false,
bottom: false,
left: false,
right: false,
topLeft: false,
topRight: false,
bottomLeft: false,
bottomRight: false
};
/**
* Initial estimate of height bounds for this tile
* Untransformed by transform matrix
* @type {NumericInterval}
* @private
*/
__initial_height_range = new NumericInterval(Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY);
/**
* Used to track number of times tile was built
* @type {number}
*/
version = 0;
constructor() {
this.mesh.name = "TerrainTile";
/**
* Terrain mesh is static, it never changes its transform. Updates are wasteful.
* @type {boolean}
*/
this.mesh.matrixWorldNeedsUpdate = false;
}
/**
*
* @return {number[]}
*/
get transform() {
return this.mesh.matrixWorld.elements;
}
/**
*
* @param {number[]|Float32Array|mat4} m4
*/
set transform(m4) {
array_copy(m4, 0, this.mesh.matrixWorld.elements, 0, 16);
this.computeBoundingBox();
}
/**
*
* @param {SurfacePoint3} result
* @param {number} originX
* @param {number} originY
* @param {number} originZ
* @param {number} directionX
* @param {number} directionY
* @param {number} directionZ
* @return {boolean}
*/
raycastFirstSync(
result,
originX, originY, originZ,
directionX, directionY, directionZ
) {
const m4 = this.transform;
mat4.invert(m4_tmp, m4);
ray3_array_compose(
ray_tmp,
originX, originY, originZ,
directionX, directionY, directionZ
);
ray3_array_apply_matrix4(ray_tmp, 0, ray_tmp, 0, m4_tmp);
const _originX = ray_tmp[0];
const _originY = ray_tmp[1];
const _originZ = ray_tmp[2];
const _directionX = ray_tmp[3];
const _directionY = ray_tmp[4];
const _directionZ = ray_tmp[5];
const geometry = this.geometry;
const geometryIndices = geometry.getIndex().array;
const attribute_position = geometry.getAttribute('position');
const position_array = attribute_position.array;
let hit_found = bvh32_geometry_raycast(
result, this.bvh, position_array, 0, 3,
attribute_position.normalized,
geometryIndices,
_originX, _originY, _originZ,
_directionX, _directionY, _directionZ
);
if (hit_found) {
result.applyMatrix4(m4);
// make sure to pull normal to magnitude of 1
result.normal.normalize();
}
return hit_found;
}
getVertexNormal(index, result) {
const normals = this.geometry.attributes.normal.array;
const index3 = index * 3;
result.set(normals[index3], normals[index3 + 1], normals[index3 + 2]);
}
setVertexNormal(index, value) {
const normals = this.geometry.attributes.normal.array;
const index3 = index * 3;
normals[index3] = value.x;
normals[index3 + 1] = value.y;
normals[index3 + 2] = value.z;
}
/**
* Stitch vertex normals along the edges of the tile set
* @param {TerrainTile|undefined} top
* @param {TerrainTile|undefined} bottom
* @param {TerrainTile|undefined} left
* @param {TerrainTile|undefined} right
* @param {TerrainTile|undefined} topLeft
* @param {TerrainTile|undefined} topRight
* @param {TerrainTile|undefined} bottomLeft
* @param {TerrainTile|undefined} bottomRight
*/
stitchNormals(top, bottom, left, right, topLeft, topRight, bottomLeft, bottomRight) {
const v0 = new Vector3(),
v1 = new Vector3(),
v2 = new Vector3(),
v3 = new Vector3();
const tile = this;
function tileIsBuilt(tile) {
return tile !== undefined && tile.isBuilt;
}
function updateNormals(tile) {
tile.geometry.attributes.normal.needsUpdate = true;
}
/**
*
* @param {TerrainTile} top
* @param {TerrainTile} bottom
*/
function stitchVertical(top, bottom) {
//can only stitch if both sides are built
if (tileIsBuilt(top) && tileIsBuilt(bottom)) {
let i;
const thisResolution = bottom.resolution.getValue();
const otherResolution = top.resolution.getValue();
const otherOffset = top.size.x * otherResolution * (top.size.y * otherResolution - 1);
const stitchCount = bottom.size.x * thisResolution - 1;
//see if we can copy one from the other
if (top.stitching.bottom) {
for (i = 1; i < stitchCount; i++) {
top.getVertexNormal(otherOffset + i, v0);
bottom.setVertexNormal(i, v0);
}
updateNormals(bottom);
bottom.stitching.top = true;
} else if (bottom.stitching.top) {
for (i = 1; i < stitchCount; i++) {
bottom.getVertexNormal(i, v0);
top.setVertexNormal(otherOffset + i, v0);
}
updateNormals(top);
top.stitching.bottom = true;
} else {
//neither is stitched
for (i = 1; i < stitchCount; i++) {
bottom.getVertexNormal(i, v0);
top.getVertexNormal(otherOffset + i, v1);
v0.add(v1);
v0.normalize();
bottom.setVertexNormal(i, v0);
top.setVertexNormal(otherOffset + i, v0);
}
updateNormals(top);
updateNormals(bottom);
top.stitching.bottom = true;
bottom.stitching.top = true;
}
}
}
/**
*
* @param {TerrainTile} left
* @param {TerrainTile} right
*/
function stitchHorizontal(left, right) {
if (tileIsBuilt(left) && tileIsBuilt(right)) {
let i;
const thisResolution = right.resolution.getValue();
const otherResolution = left.resolution.getValue();
const stitchCount = right.size.y * thisResolution - 1;
const otherOffset = left.size.x * otherResolution - 1;
const otherMultiplier = left.size.x * otherResolution;
const thisMultiplier = right.size.x * thisResolution;
let index0, index1;
if (left.stitching.right) {
for (i = 0; i < stitchCount; i++) {
index0 = i * thisMultiplier;
index1 = otherOffset + i * otherMultiplier;
left.getVertexNormal(index1, v0);
right.setVertexNormal(index0, v0);
}
updateNormals(right);
right.stitching.left = true;
} else if (right.stitching.left) {
for (i = 0; i < stitchCount; i++) {
index0 = i * thisMultiplier;
index1 = otherOffset + i * otherMultiplier;
right.getVertexNormal(index0, v0);
left.setVertexNormal(index1, v0);
}
updateNormals(left);
left.stitching.right = true;
} else {
//neither is stitched
for (i = 0; i < stitchCount; i++) {
index0 = i * thisMultiplier;
index1 = otherOffset + i * otherMultiplier;
right.getVertexNormal(index0, v0);
left.getVertexNormal(index1, v1);
v0.add(v1);
v0.normalize();
right.setVertexNormal(index0, v0);
left.setVertexNormal(index1, v0);
}
updateNormals(left);
updateNormals(right);
left.stitching.right = true;
right.stitching.left = true;
}
}
}
/**
*
* @param {TerrainTile} topLeft
* @param {TerrainTile} topRight
* @param {TerrainTile} bottomLeft
* @param {TerrainTile} bottomRight
*/
function stitchOneCorner(topLeft, topRight, bottomLeft, bottomRight) {
function topLeftCornerIndex() {
return (topLeft.size.x * topLeft.resolution.getValue()) * (topLeft.size.y * topLeft.resolution.getValue()) - 1;
}
function topRightCornerIndex() {
return topRight.size.x * topRight.resolution.getValue() * (topRight.size.y * topRight.resolution.getValue() - 1);
}
function bottomLeftCornerIndex() {
return bottomLeft.size.x * bottomLeft.resolution.getValue() - 1;
}
if (tileIsBuilt(topLeft) && tileIsBuilt(topRight) && tileIsBuilt(bottomLeft) && tileIsBuilt(bottomRight)) {
const tlCornerIndex = topLeftCornerIndex();
const cornerIndex = 0;
const tCornerIndex = topRightCornerIndex();
const lCornerIndex = bottomLeftCornerIndex();
topLeft.getVertexNormal(tlCornerIndex, v0);
bottomRight.getVertexNormal(cornerIndex, v1);
topRight.getVertexNormal(tCornerIndex, v2);
bottomLeft.getVertexNormal(lCornerIndex, v3);
v0.add(v1);
v0.add(v2);
v0.add(v3);
v0.normalize();
topLeft.setVertexNormal(tlCornerIndex, v0);
bottomRight.setVertexNormal(cornerIndex, v0);
topRight.setVertexNormal(tCornerIndex, v0);
bottomLeft.setVertexNormal(lCornerIndex, v0);
updateNormals(topLeft);
updateNormals(topRight);
updateNormals(bottomLeft);
updateNormals(bottomRight);
topLeft.stitching.bottomRight = true;
topRight.stitching.bottomLeft = true;
bottomLeft.stitching.topRight = true;
bottomRight.stitching.topLeft = true;
}
}
function stitchCorners() {
//top-left
stitchOneCorner(topLeft, top, left, tile);
//top-right
stitchOneCorner(top, topRight, tile, right);
//bottom-left
stitchOneCorner(left, tile, bottomLeft, bottom);
//bottom-right
stitchOneCorner(tile, right, bottom, bottomRight);
}
function stitchSides() {
//top
stitchVertical(top, tile);
//bottom
stitchVertical(tile, bottom);
//left
stitchHorizontal(left, tile);
//right
stitchHorizontal(tile, right);
}
stitchCorners();
stitchSides();
}
computeBoundingBox() {
/**
* @type {ThreeBox3}
*/
let bb;
const geometry = this.geometry;
if (geometry === null) {
// no geometry present yet
const position = this.position;
const scale = this.scale;
const size = this.size;
const initial_height_range = this.__initial_height_range;
const min = new ThreeVector3(position.x * scale.x, initial_height_range.min, position.y * scale.y);
const max = new ThreeVector3(min.x + size.x * scale.x, initial_height_range.max, min.z + size.y * scale.y);
bb = new ThreeBox3(
min,
max
);
} else {
//check for bvh
const bvh = this.bvh;
if (bvh !== null) {
const float32 = bvh.float32;
const x0 = float32[0];
const y0 = float32[1];
const z0 = float32[2];
const x1 = float32[3];
const y1 = float32[4];
const z1 = float32[5];
geometry.boundingBox = new ThreeBox3(new ThreeVector3(x0, y0, z0), new ThreeVector3(x1, y1, z1));
const dX = x1 - x0;
const dY = y1 - y0;
const dZ = z1 - z0;
const radius = Math.sqrt(dX * dX + dY * dY + dZ * dZ) / 2;
const center = new ThreeVector3(x0 + dX / 2, y0 + dY / 2, z0 + dZ / 2);
geometry.boundingSphere = new ThreeSphere(center, radius);
}
//pull bounding box from geometry
bb = geometry.boundingBox;
if (bb === null) {
geometry.computeBoundingBox();
bb = geometry.boundingBox;
}
}
const x0 = bb.min.x;
const y0 = bb.min.y;
const z0 = bb.min.z;
const x1 = bb.max.x;
const y1 = bb.max.y;
const z1 = bb.max.z;
const geometry_bb = new AABB3(
x0, y0, z0,
x1, y1, z1
);
geometry_bb.applyMatrix4(this.transform);
this.external_bvh.resize(
geometry_bb.x0, geometry_bb.y0, geometry_bb.z0,
geometry_bb.x1, geometry_bb.y1, geometry_bb.z1
);
}
/**
*
* @param {number} min_height
* @param {number} max_height
*/
setInitialHeightBounds(min_height, max_height) {
this.__initial_height_range.set(min_height, max_height);
}
dispose() {
if (!this.isBuilt) {
return;
}
if (this.geometry !== null) {
this.geometry.dispose();
this.geometry = null;
}
this.isBuilt = false;
this.onDestroyed.send1(this);
}
/**
*
* @param {{geometry, bvh?:{leaf_count:number, data:ArrayBuffer}}} tileData
* @returns {Mesh}
*/
build(tileData) {
this.isBuilt = true;
// console.groupCollapsed('Building tile');
// console.time('total');
const tileDataGeometry = tileData.geometry;
const g = new ThreeBufferGeometry();
g.setIndex(new ThreeBufferAttribute(tileDataGeometry.indices, 1));
g.setAttribute('position', new ThreeBufferAttribute(tileDataGeometry.vertices, 3));
g.setAttribute('normal', new ThreeBufferAttribute(tileDataGeometry.normals, 3));
g.setAttribute('uv', new ThreeBufferAttribute(tileDataGeometry.uvs, 2));
//second UV set is needed for lightmap, this is already present in TerrainShader
// g.addAttribute('uv2', new THREE.BufferAttribute(tileDataGeometry.uvs, 2));
this.geometry = g;
if (this.enableBVH) {
// console.time('bvh');
// this.generateBufferedGeometryBVH();
const bvh = this.bvh = new BinaryUint32BVH();
const serialized_bvh = tileData.bvh;
bvh.setLeafCount(serialized_bvh.leaf_count);
bvh.data = serialized_bvh.data;
// console.timeEnd('bvh');
}
const mesh = this.mesh;
mesh.geometry = g;
mesh.receiveShadow = true;
mesh.castShadow = true;
//set bounding box
// console.time('bb');
this.computeBoundingBox();
// console.timeEnd('bb');
// console.timeEnd('total');
// console.groupEnd();
this.version++;
return mesh;
}
}
export default TerrainTile;