UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

622 lines (497 loc) • 19.3 kB
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;