UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

207 lines (157 loc) • 7.33 kB
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;