@spearwolf/twopoint5d
Version:
Create 2.5D realtime graphics and pixelart with WebGL and three.js
265 lines • 11.6 kB
JavaScript
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