@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
333 lines (251 loc) • 9.87 kB
JavaScript
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);
}
}