@spearwolf/twopoint5d
Version:
a library to create 2.5d realtime graphics and pixelart with three.js
315 lines • 13.9 kB
JavaScript
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