UNPKG

@spearwolf/twopoint5d

Version:

a library to create 2.5d realtime graphics and pixelart with three.js

315 lines 13.9 kB
var _a; import { Box3, Box3Helper, BoxGeometry, Color, Frustum, Line3, Matrix4, Mesh, MeshBasicMaterial, Object3D, OrthographicCamera, PerspectiveCamera, Plane, PlaneHelper, Vector2, Vector3, } from 'three'; import { Dependencies } from '../utils/Dependencies.js'; import { AABB2 } from './AABB2.js'; import { HelpersManager } from './HelpersManager.js'; import { Map2DTile } from './Map2DTile.js'; import { Map2DTileCoordsUtil } from './Map2DTileCoordsUtil.js'; const _v = new Vector3(); const _m = new Matrix4(); const toBoxId = (x, y) => `${x},${y}`; const toAABB2 = ({ top, left, width, height }, xOffset, yOffset) => new AABB2(left + xOffset, top + yOffset, width, height); const makeCameraFrustum = (camera, target = new Frustum()) => target.setFromProjectionMatrix(_m.copy(camera.projectionMatrix).multiply(camera.matrixWorldInverse)); const findTileIndex = (tiles, id) => tiles.findIndex((tile) => tile.id === id); const insertAndSortByDistance = (arr, tile) => { const index = arr.findIndex((t) => tile.distanceToCamera < t.distanceToCamera); if (index === -1) { arr.push(tile); } else { arr.splice(index, 0, tile); } }; export class CameraBasedVisibility { static { this.Plane = new Plane(new Vector3(0, 1, 0), 0); } #cameraWorldPosition; #planeWorld; #planeOrigin; #pointOnPlane; #planeCoords2D; #centerPoint2D; #matrixWorld; #matrixWorldInverse; #cameraFrustum; #tileBoxMatrix; #map2dTileCoords; #showHelpers; #deps; #visibles; #visibleTiles; #helpers; constructor(camera) { this.frustumBoxScale = 1.1; this.lookAtCenter = false; this.depth = 100; this.#cameraWorldPosition = new Vector3(); this.#planeWorld = _a.Plane.clone(); this.#planeOrigin = new Vector3(); this.#planeCoords2D = new Vector2(); this.#centerPoint2D = new Vector2(); this.#matrixWorld = new Matrix4(); this.#matrixWorldInverse = new Matrix4(); this.#cameraFrustum = new Frustum(); this.#tileBoxMatrix = new Matrix4(); this.#map2dTileCoords = new Map2DTileCoordsUtil(); this.debugNextVisibleTiles = false; this.#showHelpers = false; this.maxDebugHelpers = 9; this.tileBoxHelperExpand = -0.01; this.frustumBoxHelperExpand = 0; this.frustumBoxHelperColor = new Color(0x777777); this.frustumBoxPrimaryHelperColor = new Color(0xffffff); this.tileBoxHelperColor = new Color(0x772222); this.tileBoxPrimaryHelperColor = new Color(0xff0066); this.#deps = new Dependencies([ 'depth', 'lookAtCenter', Dependencies.cloneable('centerPoint2D'), Dependencies.cloneable('map2dTileCoords'), Dependencies.cloneable('nodeMatrixWoderld'), Dependencies.cloneable('cameraMatrixWorld'), Dependencies.cloneable('cameraProjectionMatrix'), ]); this.#visibles = []; this.#helpers = new HelpersManager(); this.camera = camera; } get needsUpdate() { return true; } set needsUpdate(_update) { } get showHelpers() { return this.#showHelpers; } set showHelpers(showHelpers) { if (this.#showHelpers && !showHelpers) { this.#helpers.remove(); } if (!this.#showHelpers && showHelpers) { this.createHelpers(); } this.#showHelpers = showHelpers; } dependenciesChanged(node) { return this.#deps.changed({ depth: this.depth, lookAtCenter: this.lookAtCenter, centerPoint2D: this.#centerPoint2D, map2dTileCoords: this.#map2dTileCoords, nodeMatrixWorld: node.matrixWorld, cameraMatrixWorld: this.camera.matrixWorld, cameraProjectionMatrix: this.camera.projectionMatrix, }); } computeVisibleTiles(previousTiles, [centerX, centerY], map2dTileCoords, node) { if (!this.camera) { return undefined; } this.#map2dTileCoords = map2dTileCoords; this.#centerPoint2D.set(centerX, centerY); node.updateWorldMatrix(true, false); this.camera.updateMatrixWorld(); this.camera.updateProjectionMatrix(); if (!this.dependenciesChanged(node)) { if (this.#visibleTiles) { this.#visibleTiles.createTiles = undefined; this.#visibleTiles.reuseTiles = this.#visibleTiles.tiles; this.#visibleTiles.removeTiles = undefined; } return this.#visibleTiles; } this.#matrixWorld.copy(node.matrixWorld); this.#matrixWorldInverse.copy(node.matrixWorld).invert(); const pointOnPlane3D = this.findPointOnPlaneThatIsInViewFrustum(); if (pointOnPlane3D != null) { if (this.#pointOnPlane == null) { this.#pointOnPlane = pointOnPlane3D; } else { this.#pointOnPlane.copy(pointOnPlane3D); } } else { this.#pointOnPlane = null; } this.#planeWorld.coplanarPoint(this.#planeOrigin); if (this.showHelpers) { this.#helpers.remove(); } if (pointOnPlane3D == null) { this.#visibleTiles = previousTiles.length > 0 ? { tiles: [], removeTiles: previousTiles } : undefined; return this.#visibleTiles; } this.convertToPlaneCoords2D(pointOnPlane3D, this.#planeCoords2D); if (this.lookAtCenter) { this.#centerPoint2D.sub(this.#planeCoords2D); } this.#planeCoords2D.add(this.#centerPoint2D); this.#visibleTiles = this.findVisibleTiles(previousTiles); if (this.showHelpers) { this.createHelpers(); } return this.#visibleTiles; } findPointOnPlaneThatIsInViewFrustum() { const camWorldDir = this.camera.getWorldDirection(_v).setLength(this.camera.far); this.#cameraWorldPosition.setFromMatrixPosition(this.camera.matrixWorld); const lineOfSightEnd = camWorldDir.clone().add(this.#cameraWorldPosition); const lineOfSight = new Line3(this.#cameraWorldPosition, lineOfSightEnd); this.#planeWorld .copy(_a.Plane) .applyMatrix4(_m.makeTranslation(this.#map2dTileCoords.xOffset, 0, this.#map2dTileCoords.yOffset)) .applyMatrix4(this.#matrixWorld); return this.#planeWorld.intersectLine(lineOfSight, new Vector3()); } findVisibleTiles(previousTiles) { previousTiles = previousTiles.slice(0); makeCameraFrustum(this.camera, this.#cameraFrustum); const primaryTiles = this.#map2dTileCoords.computeTilesWithinCoords(this.#planeCoords2D.x - this.#map2dTileCoords.tileWidth / 2, this.#planeCoords2D.y - this.#map2dTileCoords.tileHeight / 2, this.#map2dTileCoords.tileWidth, this.#map2dTileCoords.tileHeight); const translate = new Vector3().setFromMatrixPosition(this.#matrixWorld); this.#tileBoxMatrix.makeTranslation(this.#map2dTileCoords.xOffset - this.#centerPoint2D.x + translate.x, translate.y, this.#map2dTileCoords.yOffset - this.#centerPoint2D.y + translate.z); const next = []; for (let ty = 0; ty < primaryTiles.rows; ty++) { for (let tx = 0; tx < primaryTiles.columns; tx++) { const x = primaryTiles.tileLeft + tx; const y = primaryTiles.tileTop + ty; next.push({ id: toBoxId(x, y), x, y, primary: true, }); } } const reuseTiles = []; const createTiles = []; const visitedIds = new Set(); this.#visibles.length = 0; while (next.length > 0) { const tile = next.pop(); if (!visitedIds.has(tile.id)) { visitedIds.add(tile.id); tile.coords ??= this.#map2dTileCoords.computeTilesWithinCoords(tile.x * primaryTiles.tileWidth, tile.y * primaryTiles.tileHeight, 1, 1); tile.frustumBox ??= this.makeBox(tile.coords, this.frustumBoxScale) .applyMatrix4(this.#tileBoxMatrix) .applyMatrix4(this.#matrixWorld); if (this.#cameraFrustum.intersectsBox(tile.frustumBox)) { tile.centerWorld = new Vector3(tile.coords.left + tile.coords.width / 2, 0, tile.coords.top + tile.coords.height / 2) .applyMatrix4(this.#tileBoxMatrix) .applyMatrix4(this.#matrixWorld); tile.distanceToCamera = tile.centerWorld.distanceTo(this.#cameraWorldPosition); insertAndSortByDistance(this.#visibles, tile); tile.box ??= this.makeBox(tile.coords).applyMatrix4(this.#tileBoxMatrix); tile.map2dTile = new Map2DTile(tile.x, tile.y, toAABB2(tile.coords, 0, 0)); const previousTilesIndex = findTileIndex(previousTiles, Map2DTile.createID(tile.x, tile.y)); if (previousTilesIndex >= 0) { previousTiles.splice(previousTilesIndex, 1); reuseTiles.push(tile.map2dTile); } else { createTiles.push(tile.map2dTile); } [ [0, -1], [1, 0], [0, 1], [-1, 0], [-1, -1], [1, -1], [1, 1], [-1, 1], ].forEach(([dx, dy]) => { const [tx, ty] = [tile.coords.tileLeft + dx, tile.coords.tileTop + dy]; const tileId = toBoxId(tx, ty); if (!visitedIds.has(tileId)) { next.push({ id: tileId, x: tx, y: ty, }); } }); } } } const offset = new Vector2(this.#map2dTileCoords.xOffset - this.#centerPoint2D.x, this.#map2dTileCoords.yOffset - this.#centerPoint2D.y); return { tiles: this.#visibles.map((visible) => visible.map2dTile), createTiles, reuseTiles, removeTiles: previousTiles, offset, translate, }; } makePointOnPlane(point) { return new Vector3(this.#map2dTileCoords.xOffset + (point?.x ?? 0), 0, this.#map2dTileCoords.yOffset + (point?.y ?? 0)).applyMatrix4(this.#matrixWorld); } convertToPlaneCoords2D(pointOnPlane3D, target) { _v.copy(pointOnPlane3D); _v.sub(this.#planeOrigin).applyMatrix4(this.#matrixWorldInverse); target.set(_v.x, _v.z); } createHelpers() { this.createPlaneHelpers(); this.createTileHelpers(this.#visibles); const el = document.querySelector('.map2dCoords'); if (el) { el.textContent = this.#planeCoords2D.toArray().map(Math.round).join(', '); } } createPlaneHelpers() { this.#helpers.add(new PlaneHelper(this.#planeWorld, 100, 0x20f040), true); if (this.#pointOnPlane) { this.addPointHelper(this.#pointOnPlane, true, 10, 0xc0c0c0); } this.addPointHelper(this.#planeOrigin, true, 5, 0x406090); const uOrigin = this.makePointOnPlane(new Vector2()); const u0 = this.#planeOrigin.clone().sub(uOrigin); const ux = this.makePointOnPlane(new Vector2(50, 0)).add(u0); const uy = this.makePointOnPlane(new Vector2(0, 50)).add(u0); this.addPointHelper(ux, true, 5, 0xff0000); this.addPointHelper(uy, true, 5, 0x00ff00); } createTileHelpers(visibles) { const primaries = visibles.filter((v) => v.primary); primaries.forEach((tile) => { this.addBoxHelper(tile.frustumBox, this.frustumBoxHelperExpand, this.frustumBoxPrimaryHelperColor, true); }); for (let i = 0; i < visibles.length; ++i) { const tile = visibles[i]; if (!tile.primary && i < this.maxDebugHelpers) { this.addBoxHelper(tile.frustumBox, this.frustumBoxHelperExpand, this.frustumBoxHelperColor, true); } this.addBoxHelper(tile.box, this.tileBoxHelperExpand, tile.primary ? this.tileBoxPrimaryHelperColor : this.tileBoxHelperColor, false); } } addPointHelper(point, addToRoot = true, size = 10, color = 0x20f040) { const poiBox = new Mesh(new BoxGeometry(size, size, size), new MeshBasicMaterial({ color })); poiBox.position.copy(point); this.#helpers.add(poiBox, addToRoot); } addBoxHelper(box, expand, color, addToRoot) { box = box.clone(); const boxSize = box.getSize(new Vector3()); box.expandByVector(boxSize.multiplyScalar(expand)); const helper = new Box3Helper(box, color); this.#helpers.add(helper, addToRoot); } makeBox({ top, left, width, height }, scale = 1) { const sw = width * scale - width; const sh = height * scale - height; const ground = this.depth * -0.5 * scale; const ceiling = this.depth * 0.5 * scale; return new Box3(new Vector3(left - sw, ground, top - sh), new Vector3(left + width + sw, ceiling, top + height + sh)); } addToScene(scene) { this.#helpers.scene = scene; } removeFromScene(scene) { this.#helpers.removeFromScene(scene); } } _a = CameraBasedVisibility; //# sourceMappingURL=CameraBasedVisibility.js.map