@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
207 lines (157 loc) • 7.33 kB
JavaScript
import { mat4 } from "gl-matrix";
import { ResourceAccessKind } from "../../../../core/model/ResourceAccessKind.js";
import { ResourceAccessSpecification } from "../../../../core/model/ResourceAccessSpecification.js";
import { GraphicsEngine } from "../../../graphics/GraphicsEngine.js";
import { FogOfWarVisibilityPredicate } from "../../fow/FogOfWarVisibilityPredicate.js";
import { System } from '../../System.js';
import { Transform } from '../../transform/Transform.js';
import GUIElement from "../GUIElement.js";
import ViewportPosition from "../position/ViewportPosition.js";
import HeadsUpDisplay from './HeadsUpDisplay.js';
import { HeadsUpDisplayFlag } from "./HeadsUpDisplayFlag.js";
const projection = new Float32Array(16);
/**
*
* @param {GraphicsEngine} graphicsEngine
* @param containerView
* @constructor
*/
class HeadsUpDisplaySystem extends System {
dependencies = [HeadsUpDisplay];
components_used = [
ResourceAccessSpecification.from(GUIElement, ResourceAccessKind.Read | ResourceAccessKind.Write),
ResourceAccessSpecification.from(ViewportPosition, ResourceAccessKind.Read | ResourceAccessKind.Write),
];
/**
*
* @param {GraphicsEngine} graphicsEngine
*/
constructor(graphicsEngine) {
super();
if (!(graphicsEngine instanceof GraphicsEngine)) {
throw new TypeError(`graphicsEngine is not an instance of GraphicsEngine`);
}
this.graphics = graphicsEngine;
const visibilityPredicate = new FogOfWarVisibilityPredicate();
// Because of blur being applied to FOW, sometimes you can see a tile quite clearly, though be unable to interact with it, higher clearance helps with that
visibilityPredicate.maxClearance = 1;
/**
*
* @type {FogOfWarVisibilityPredicate}
* @private
*/
this.__visibility_predicate = visibilityPredicate;
}
async shutdown(em) {
this.graphics.on.preRender.remove(this.synchronizePositions, this);
}
async startup(em) {
this.entityManager = em;
this.graphics.on.preRender.add(this.synchronizePositions, this);
}
/**
* NOTE: for the sake of speed all of the matrix and vector multiplications have been inlined, readability of this method is pretty bad
* @param {HeadsUpDisplay} hud
* @param {Transform} transform
* @param {ViewportPosition} vp
* @param {GUIElement} element
* @private
*/
__apply_entity_transformation(hud, transform, vp, element) {
const worldOffset = hud.worldOffset;
let x = worldOffset.x;
let y = worldOffset.y;
let z = worldOffset.z;
if (hud.getFlag(HeadsUpDisplayFlag.TransformWorldOffset)) {
//apply scale and rotation
const transform_scale = transform.scale;
x *= transform_scale.x;
y *= transform_scale.y;
z *= transform_scale.y;
const q = transform.rotation;
const qx = q.x;
const qy = q.y;
const qz = q.z;
const qw = q.w;
// calculate quat * vec
const ix = qw * x + qy * z - qz * y;
const iy = qw * y + qz * x - qx * z;
const iz = qw * z + qx * y - qy * x;
const iw = -qx * x - qy * y - qz * z;
// calculate result * inverse quat
x = ix * qw + iw * -qx + iy * -qz - iz * -qy;
y = iy * qw + iw * -qy + iz * -qx - ix * -qz;
z = iz * qw + iw * -qz + ix * -qy - iy * -qx;
}
// add transform position to get final world position
const position = transform.position;
x += position.x;
y += position.y;
z += position.z;
const worldPositionX = x;
const worldPositionY = y;
const worldPositionZ = z;
// Convert the [-1, 1] screen coordinate into a world coordinate on the near plane
// apply projection matrix
const _x = projection[0] * x + projection[4] * y + projection[8] * z + projection[12];
const _y = projection[1] * x + projection[5] * y + projection[9] * z + projection[13];
const _z = projection[2] * x + projection[6] * y + projection[10] * z + projection[14];
const _w = projection[3] * x + projection[7] * y + projection[11] * z + projection[15];
// apply perspective transform
const d = 1 / Math.abs(_w);
x = _x * d;
y = _y * d;
z = _z * d;
const ndcX = (x + 1) / 2;
const ndcY = (1 - y) / 2;
vp.position.set(ndcX, ndcY);
const trackedPositionOutOfBounds = z > 1 || z < -1;
const visible = (!trackedPositionOutOfBounds || vp.stickToScreenEdge) && this.__visibility_predicate.test(worldPositionX, worldPositionY, worldPositionZ);
if (!visible) {
// hide element as it's not in viewport bounds
vp.enabled.set(false);
element.visible.set(false);
} else {
if (hud.getFlag(HeadsUpDisplayFlag.PerspectiveRotation)) {
//compute rotation in screen-space
const w_y_1 = worldPositionY + 1;
const _up_x = projection[0] * worldPositionX + projection[4] * w_y_1 + projection[8] * worldPositionZ + projection[12];
const _up_y = projection[1] * worldPositionX + projection[5] * w_y_1 + projection[9] * worldPositionZ + projection[13];
const _up_w = projection[3] * worldPositionX + projection[7] * w_y_1 + projection[11] * worldPositionZ + projection[15];
// compute perspective divide
const d = 1 / Math.abs(_up_w);
const up_x = _up_x * d;
const up_y = _up_y * d;
const up_ndcX = (up_x + 1) / 2;
const up_ndcY = (1 - up_y) / 2;
const angle = Math.atan2(up_ndcY - ndcY, up_ndcX - ndcX);
element.view.rotation.set(angle + Math.PI / 2);
} else {
//clear rotation
element.view.rotation.set(0);
}
vp.enabled.set(true);
//TODO set z-index to ensure that things that are closer to the camera appear on top
}
}
synchronizePositions() {
const entityManager = this.entityManager;
/**
*
* @type {Camera} three.js camera object
*/
const camera = this.graphics.camera;
mat4.multiply(projection, camera.projectionMatrix.elements, camera.matrixWorldInverse.elements);
const visibilityPredicate = this.__visibility_predicate;
/**
* @type {EntityComponentDataset}
*/
const dataset = entityManager.dataset;
if (dataset !== null) {
visibilityPredicate.initialize(camera, dataset);
dataset.traverseEntities([HeadsUpDisplay, Transform, ViewportPosition, GUIElement], this.__apply_entity_transformation, this);
visibilityPredicate.finalize();
}
};
}
export default HeadsUpDisplaySystem;