UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

333 lines (251 loc) • 9.87 kB
import { Frustum as ThreeFrustum, } from 'three'; import { assert } from "../../../../core/assert.js"; import { SignalBinding } from "../../../../core/events/signal/SignalBinding.js"; import Quaternion from "../../../../core/geom/Quaternion.js"; import { ResourceAccessKind } from "../../../../core/model/ResourceAccessKind.js"; import { ResourceAccessSpecification } from "../../../../core/model/ResourceAccessSpecification.js"; import { System } from '../../../ecs/System.js'; import { Transform } from '../../../ecs/transform/Transform.js'; import { threeUpdateTransform } from "../../util/threeUpdateTransform.js"; import { auto_set_camera_clipping_planes } from "./auto_set_camera_clipping_planes.js"; import { build_three_camera_object } from "./build_three_camera_object.js"; import { Camera } from './Camera.js'; import { frustum_from_camera } from "./frustum_from_camera.js"; import { quaternion_invert_orientation } from "./quaternion_invert_orientation.js"; import { set_camera_aspect_ratio } from "./set_camera_aspect_ratio.js"; import { update_camera_transform } from "./update_camera_transform.js"; const scratch_quat = new Quaternion(); /** * * @param {THREE.Camera} object * @param {Quaternion} rotation */ export function three_camera_set_transform_rotation(object, rotation) { /* NOTE: I'm not sure why, but three.js camera points in the opposite direction to normal objects See: https://github.com/mrdoob/three.js/blob/412b99a7f26e117ea97f40eb53d010ab81aa3279/src/core/Object3D.js#L282 */ quaternion_invert_orientation(scratch_quat, rotation); object.quaternion.set( scratch_quat.x, scratch_quat.y, scratch_quat.z, scratch_quat.w ); object.rotation.setFromQuaternion(object.quaternion); // rotation.__setThreeEuler(camera.object.rotation); // seems unnecessary, based on Object3D.lookAt implementation // camera.object.quaternion.set(rotation.x, rotation.y, rotation.z, rotation.w); object.updateProjectionMatrix(); threeUpdateTransform(object); } export class CameraSystem extends System { /** * * @param {GraphicsEngine} graphics * @constructor */ constructor(graphics) { super(); assert.defined(graphics, 'graphics'); assert.notNull(graphics, 'graphics'); assert.equal(graphics.isGraphicsEngine, true, 'graphics.isGraphicsEngine !== true'); this.scene = graphics.scene; this.dependencies = [Camera, Transform]; this.components_used = [ ResourceAccessSpecification.from(Camera, ResourceAccessKind.Write) ]; /** * * @type {EntityManager} */ this.entityManager = null; /** * @type {GraphicsEngine} */ this.graphics = graphics; this.entityData = []; const self = this; /** * * @param {number} x * @param {number} y * @private */ this.__handleViewportResize = function (x, y) { const em = self.entityManager; if (em === null) { return; } const dataset = em.dataset; if (dataset === null) { return; } dataset.traverseComponents(Camera, function (camera) { set_camera_aspect_ratio(camera, x, y); }); }; this.signalBindings = []; } /** * * @param {Transform} transform * @param {Camera} camera * @param entityId */ link(camera, transform, entityId) { if (camera.object === null) { camera.object = build_three_camera_object(camera); } const graphics = this.graphics; const viewportSize = graphics.viewport.size; set_camera_aspect_ratio(camera, viewportSize.x, viewportSize.y); this.scene.add(camera.object); function synchronizePosition(x, y, z) { assert.isNumber(x, 'x'); assert.isNumber(y, 'y'); assert.isNumber(z, 'z'); camera.object.position.set(x, y, z); update_camera_transform(camera); } function synchronizeRotation() { const rotation = transform.rotation; three_camera_set_transform_rotation(camera.object, rotation); } function rebuild() { camera.object = build_three_camera_object(camera); set_camera_aspect_ratio(camera, viewportSize.x, viewportSize.y); const position = transform.position; synchronizePosition(position.x, position.y, position.z); synchronizeRotation(); synchronizeActiveState(camera.active.getValue()); } function synchronizeActiveState(v) { if (v) { graphics.camera = camera.object; } else { //active camera disabled } } const position = transform.position; const rotation = transform.rotation; const signalBindings = [ new SignalBinding(position.onChanged, synchronizePosition), new SignalBinding(rotation.onChanged, synchronizeRotation), new SignalBinding(camera.projectionType.onChanged, rebuild), new SignalBinding(camera.active.onChanged, synchronizeActiveState), new SignalBinding(camera.fov.onChanged, rebuild) ]; signalBindings.forEach(b => b.link()); this.entityData[entityId] = signalBindings; synchronizePosition(position.x, position.y, position.z); synchronizeRotation(rotation.x, rotation.y, rotation.z, rotation.w); synchronizeActiveState(camera.active.getValue()); assert.notEqual(camera.object, null, 'Camera object must not be null (invariant)'); } /** * * @param {Transform} transform * @param {Camera} camera * @param entityId */ unlink(camera, transform, entityId) { const data = this.entityData; if (data.hasOwnProperty(entityId)) { const entityData = data[entityId]; if (entityData !== void 0) { entityData.forEach(b => b.unlink()); } delete data[entityId]; } this.scene.remove(camera.object); } async startup(entityManager) { this.entityManager = entityManager; const graphics = this.graphics; graphics.viewport.size.onChanged.add(this.__handleViewportResize); const visibilityConstructionPreHook = new SignalBinding(graphics.on.visibilityConstructionStarted, function () { const em = entityManager; const layers = graphics.layers; const dataset = em.dataset; /** * * @param {Camera} c */ function visitCameraEntity(c) { if (c.object === null) { return; } if (!c.active.getValue()) { return; } if (c.object.updateProjectionMatrix !== undefined) { c.object.updateProjectionMatrix(); c.object.updateMatrix(); c.object.updateMatrixWorld(true); } if (c.autoClip === true) { auto_set_camera_clipping_planes(c, layers); } else { c.object.far = c.clip_far; c.object.near = c.clip_near; } } if (dataset !== null) { dataset.traverseComponents(Camera, visitCameraEntity); } }); visibilityConstructionPreHook.link(); this.signalBindings.push(visibilityConstructionPreHook); } async shutdown(entityManager) { this.graphics.viewport.size.onChanged.remove(this.__handleViewportResize); this.signalBindings.forEach(sb => sb.unlink()); this.signalBindings.splice(0, this.signalBindings.length); } /** * * @param {EntityComponentDataset} ecd * @param {function(Camera, entity:number)} visitor */ static traverseActiveCameras(ecd, visitor) { ecd.traverseComponents(Camera, function (c, entity) { if (c.active.getValue()) { visitor(c, entity); } }); } /** * * @param {EntityComponentDataset} ecd * @returns {{component:(Camera|undefined), entity: number}} */ static getFirstActiveCamera(ecd) { const r = { entity: -1, component: undefined }; this.traverseActiveCameras(ecd, (c, e) => { r.entity = e; r.component = c; }); return r; } /** * * @param {EntityComponentDataset} ecd * @param {function(ThreeFrustum[])} callback */ static getActiveFrustums(ecd, callback) { const frustums = []; if (ecd !== null) { ecd.traverseComponents(Camera, function (c) { if (c.active.getValue()) { const camera = c.object; if (camera !== null) { const frustum = new ThreeFrustum(); frustum_from_camera(camera, frustum); frustums.push(frustum); } } }); } callback(frustums); } }