UNPKG

@spearwolf/twopoint5d

Version:

Create 2.5D realtime graphics and pixelart with WebGL and three.js

265 lines 11.6 kB
import { Box3, Frustum, Line3, Matrix4, Plane, Vector2, Vector3 } from 'three/webgpu'; import { Dependencies } from '../utils/Dependencies.js'; import { AABB2 } from './AABB2.js'; import { Map2DTileCoords } from './Map2DTileCoords.js'; import { Map2DTileCoordsUtil } from './Map2DTileCoordsUtil.js'; const _v = new Vector3(); const _m = new Matrix4(); const NEIGHBOR_DX_DY = [ [0, -1], [1, 0], [0, 1], [-1, 0], [-1, -1], [1, -1], [1, 1], [-1, 1], ]; const toBoxId = (x, y) => `${x},${y}`; const setAABB2 = (target, { top, left, width, height }) => target.set(left, top, width, height); const makeCameraFrustum = (camera, target = new Frustum()) => target.setFromProjectionMatrix(_m.copy(camera.projectionMatrix).multiply(camera.matrixWorldInverse)); const sortByDistance = (a, b) => a.distanceToCamera - b.distanceToCamera; export class CameraBasedVisibility { static { this.Plane = new Plane(new Vector3(0, 1, 0), 0); } #cameraWorldPosition; #centerPoint2D; #matrixWorldInverse; #cameraFrustum; #tileBoxMatrix; #deps; #visibleTiles; #visitedIds; #nextStack; #previousTilesById; #tileBoxPool; #cachedTileCoords; #scratchTranslate; #scratchOffset; #scratchCamDir; #scratchLineEnd; #scratchLineOfSight; #scratchPlaneIntersection; constructor(camera) { this.frustumBoxScale = 1.1; this.lookAtCenter = false; this.depth = 100; this.#cameraWorldPosition = new Vector3(); this.planeWorld = CameraBasedVisibility.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.#deps = new Dependencies([ 'depth', 'lookAtCenter', Dependencies.cloneable('centerPoint2D'), Dependencies.cloneable('map2dTileCoords'), Dependencies.cloneable('matrixWorld'), Dependencies.cloneable('cameraMatrixWorld'), Dependencies.cloneable('cameraProjectionMatrix'), ]); this.visibles = []; this.#visitedIds = new Set(); this.#nextStack = []; this.#previousTilesById = new Map(); this.#tileBoxPool = new Map(); this.#scratchTranslate = new Vector3(); this.#scratchOffset = new Vector2(); this.#scratchCamDir = new Vector3(); this.#scratchLineEnd = new Vector3(); this.#scratchLineOfSight = new Line3(); this.#scratchPlaneIntersection = new Vector3(); this.camera = camera; } dependenciesChanged(matrixWorld) { return this.#deps.changed({ depth: this.depth, lookAtCenter: this.lookAtCenter, centerPoint2D: this.#centerPoint2D, map2dTileCoords: this.map2dTileCoords, matrixWorld, cameraMatrixWorld: this.camera.matrixWorld, cameraProjectionMatrix: this.camera.projectionMatrix, }); } invalidateTileCoordsCacheIfChanged() { const current = this.map2dTileCoords; if (this.#cachedTileCoords && this.#cachedTileCoords.equals(current)) return; if (this.#cachedTileCoords) { this.#cachedTileCoords.copy(current); } else { this.#cachedTileCoords = current.clone(); } for (const tile of this.#tileBoxPool.values()) { tile.coords = undefined; } } computeVisibleTiles(previousTiles, [centerX, centerY], map2dTileCoords, matrixWorld) { if (!this.camera) { return undefined; } this.map2dTileCoords = map2dTileCoords; this.#centerPoint2D.set(centerX, centerY); this.camera.updateMatrixWorld(); this.camera.updateProjectionMatrix(); if (!this.dependenciesChanged(matrixWorld)) { if (this.#visibleTiles) { this.#visibleTiles.createTiles = undefined; this.#visibleTiles.reuseTiles = this.#visibleTiles.tiles; this.#visibleTiles.removeTiles = undefined; } return this.#visibleTiles; } this.invalidateTileCoordsCacheIfChanged(); this.matrixWorld.copy(matrixWorld); this.#matrixWorldInverse.copy(matrixWorld).invert(); const pointOnPlane3D = this.findPointOnPlaneThatIsInViewFrustum(); if (pointOnPlane3D != null) { if (this.pointOnPlane == null) { this.pointOnPlane = new Vector3(); } this.pointOnPlane.copy(pointOnPlane3D); } else { this.pointOnPlane = null; } this.planeWorld.coplanarPoint(this.planeOrigin); 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); return this.#visibleTiles; } findPointOnPlaneThatIsInViewFrustum() { const camWorldDir = this.camera.getWorldDirection(this.#scratchCamDir).setLength(this.camera.far); this.#cameraWorldPosition.setFromMatrixPosition(this.camera.matrixWorld); this.#scratchLineEnd.copy(camWorldDir).add(this.#cameraWorldPosition); this.#scratchLineOfSight.set(this.#cameraWorldPosition, this.#scratchLineEnd); this.planeWorld .copy(CameraBasedVisibility.Plane) .applyMatrix4(_m.makeTranslation(this.map2dTileCoords.xOffset, 0, this.map2dTileCoords.yOffset)) .applyMatrix4(this.matrixWorld); return this.planeWorld.intersectLine(this.#scratchLineOfSight, this.#scratchPlaneIntersection); } acquireTileBox(x, y, primary) { const id = toBoxId(x, y); let tile = this.#tileBoxPool.get(id); if (tile === undefined) { tile = { id, x, y, primary }; this.#tileBoxPool.set(id, tile); } else { tile.primary = primary; } return tile; } findVisibleTiles(previousTiles) { this.#visitedIds.clear(); this.#nextStack.length = 0; this.visibles.length = 0; this.#previousTilesById.clear(); for (let i = 0; i < previousTiles.length; ++i) { this.#previousTilesById.set(previousTiles[i].id, previousTiles[i]); } 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 = this.#scratchTranslate.setFromMatrixPosition(this.matrixWorld); this.#tileBoxMatrix.makeTranslation(this.map2dTileCoords.xOffset - this.#centerPoint2D.x + translate.x, translate.y, this.map2dTileCoords.yOffset - this.#centerPoint2D.y + translate.z); for (let ty = 0; ty < primaryTiles.rows; ty++) { for (let tx = 0; tx < primaryTiles.columns; tx++) { this.#nextStack.push(this.acquireTileBox(primaryTiles.tileLeft + tx, primaryTiles.tileTop + ty, true)); } } const reuseTiles = []; const createTiles = []; while (this.#nextStack.length > 0) { const tile = this.#nextStack.pop(); if (this.#visitedIds.has(tile.id)) continue; this.#visitedIds.add(tile.id); tile.coords ??= this.map2dTileCoords.computeTilesWithinCoords(tile.x * primaryTiles.tileWidth, tile.y * primaryTiles.tileHeight, 1, 1); if (tile.frustumBox === undefined) tile.frustumBox = new Box3(); this.setBox(tile.frustumBox, tile.coords, this.frustumBoxScale) .applyMatrix4(this.#tileBoxMatrix) .applyMatrix4(this.matrixWorld); if (this.#cameraFrustum.intersectsBox(tile.frustumBox)) { if (tile.centerWorld === undefined) tile.centerWorld = new Vector3(); tile.centerWorld .set(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); this.visibles.push(tile); if (tile.box === undefined) tile.box = new Box3(); this.setBox(tile.box, tile.coords).applyMatrix4(this.#tileBoxMatrix); if (tile.map2dTile === undefined) { tile.map2dTile = new Map2DTileCoords(tile.x, tile.y, new AABB2()); } setAABB2(tile.map2dTile.view, tile.coords); const previous = this.#previousTilesById.get(tile.map2dTile.id); if (previous !== undefined) { this.#previousTilesById.delete(tile.map2dTile.id); reuseTiles.push(tile.map2dTile); } else { createTiles.push(tile.map2dTile); } for (let i = 0; i < NEIGHBOR_DX_DY.length; ++i) { const dx = NEIGHBOR_DX_DY[i][0]; const dy = NEIGHBOR_DX_DY[i][1]; const tx = tile.coords.tileLeft + dx; const ty = tile.coords.tileTop + dy; if (!this.#visitedIds.has(toBoxId(tx, ty))) { this.#nextStack.push(this.acquireTileBox(tx, ty, false)); } } } } this.visibles.sort(sortByDistance); const tiles = new Array(this.visibles.length); for (let i = 0; i < this.visibles.length; ++i) tiles[i] = this.visibles[i].map2dTile; const removeTiles = []; for (const t of this.#previousTilesById.values()) removeTiles.push(t); this.#scratchOffset.set(this.map2dTileCoords.xOffset - this.#centerPoint2D.x, this.map2dTileCoords.yOffset - this.#centerPoint2D.y); return { tiles, createTiles, reuseTiles, removeTiles, offset: this.#scratchOffset, translate, }; } convertToPlaneCoords2D(pointOnPlane3D, target) { _v.copy(pointOnPlane3D); _v.sub(this.planeOrigin).applyMatrix4(this.#matrixWorldInverse); target.set(_v.x, _v.z); } setBox(target, { 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; target.min.set(left - sw, ground, top - sh); target.max.set(left + width + sw, ceiling, top + height + sh); return target; } } //# sourceMappingURL=CameraBasedVisibility.js.map