@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
315 lines (245 loc) • 7.98 kB
JavaScript
import { Matrix4 } from "three";
import { assert } from "../../../../../core/assert.js";
import { sphere_project } from "../../../../../core/geom/3d/sphere/sphere_project.js";
import Vector4 from "../../../../../core/geom/Vector4.js";
import { max3 } from "../../../../../core/math/max3.js";
import { ResourceAccessKind } from "../../../../../core/model/ResourceAccessKind.js";
import { ResourceAccessSpecification } from "../../../../../core/model/ResourceAccessSpecification.js";
import { System } from "../../../../ecs/System.js";
import { CameraSystem } from "../../camera/CameraSystem.js";
import Mesh, { MeshFlags } from "../../mesh/Mesh.js";
import { MeshEvents } from "../../mesh/MeshEvents.js";
import { MeshSystem } from "../../mesh/MeshSystem.js";
import { AnimationGraph } from "./graph/AnimationGraph.js";
import { AnimationGraphFlag } from "./graph/AnimationGraphFlag.js";
/**
* @type {Vector4}
*/
const v4boundingSphere = new Vector4();
export class AnimationGraphSystem extends System {
/**
*
* @param {Vector2} viewportSize
*/
constructor(viewportSize) {
super();
this.dependencies = [AnimationGraph, Mesh];
this.components_used = [
ResourceAccessSpecification.from(Mesh, ResourceAccessKind.Write)
];
/**
*
* @type {number}
* @private
*/
this.__timeDelta = 0;
/**
*
* @type {number}
* @private
*/
this.__animationGraphComponentIndex = 0;
/**
*
* @type {number}
* @private
*/
this.__focalLength = 0;
/**
*
* @type {Vector2}
* @private
*/
this.__viewportSize = viewportSize;
/**
*
* @type {Matrix4}
* @private
*/
this.__projectionMatrix = new Matrix4();
}
/**
* @private
* @param {Mesh} component
* @param {number} entity
*/
handleMeshSetEvent({ component, entity }) {
const ecd = this.entityManager.dataset;
if (!component.getFlag(MeshFlags.Loaded)) {
// mesh is not yet loaded
return;
}
const graph = ecd.getComponent(entity, AnimationGraph);
if (graph.getFlag(AnimationGraphFlag.Linked)) {
//unlink the graph to clear out animation clip bindings
graph.unlink();
}
graph.attach(component.mesh);
//re-link the graph
graph.link(entity, ecd);
}
/**
*
* @param {AnimationGraph} graph
* @param {Mesh} mesh
* @param {number} entity
*/
link(graph, mesh, entity) {
const ecd = this.entityManager.dataset;
if (mesh.getFlag(MeshFlags.Loaded)) {
graph.attach(mesh.mesh);
graph.link(entity, ecd);
}
ecd.addEntityEventListener(entity, MeshEvents.DataSet, this.handleMeshSetEvent, this);
}
/**
*
* @param {AnimationGraph} graph
* @param {Mesh} mesh
* @param {number} entity
*/
unlink(graph, mesh, entity) {
const ecd = this.entityManager.dataset;
graph.unlink();
ecd.removeEntityEventListener(entity, MeshEvents.DataSet, this.handleMeshSetEvent, this);
}
/**
*
* @param {AnimationGraph} graph
* @param {number} entity
*/
advanceGraphTime(graph, entity) {
graph.debtTime += this.__timeDelta;
}
/**
*
* @param {Mesh} mesh
* @param {number} entity
*/
visitVisibleMesh(mesh, entity) {
/**
*
* @type {EntityComponentDataset}
*/
const ecd = this.entityManager.dataset;
/**
*
* @type {AnimationGraph}
*/
const graph = ecd.getComponentByIndex(entity, this.__animationGraphComponentIndex);
if (graph === undefined) {
//mesh has no animation, skip
return true;
}
if (!this.shouldEntityBeAnimated(entity, graph, mesh)) {
return;
}
const dt = graph.debtTime;
if (dt > 0) {
graph.tick(dt);
graph.debtTime = 0;
}
}
/**
*
* @param {number} entity
* @param {AnimationGraph} graph
* @param {Mesh} meshComponent
*/
shouldEntityBeAnimated(entity, graph, meshComponent) {
if (meshComponent === undefined) {
//no mesh component
return false;
}
const mesh = meshComponent.mesh;
if (mesh === null) {
//no renderable object
return false;
}
if (!graph.getFlag(AnimationGraphFlag.Linked)) {
// not linked this is generally a case of failed linkage, either way - don't animate
return false;
}
if (graph.getFlag(AnimationGraphFlag.MeshSizeCulling)) {
//check the size of the mesh in screen space, culling animation of tiny objects
const areaInPixel = this.screenSpaceSize(mesh, this.__projectionMatrix);
if (areaInPixel < 32) {
//too tiny
return false;
}
}
//passed all filters, visible
return true;
}
/**
*
* @param {Mesh} mesh trhee.js Mesh instance
* @param {Matrix4} cameraMatrix
*/
screenSpaceSize(mesh, cameraMatrix) {
/**
* @type {Vector4}
*/
const source = mesh.boundingSphere;
if (source === undefined) {
return 0;
}
assert.notEqual(cameraMatrix, null, 'camera matrix is null');
v4boundingSphere.copy(source);
const position = mesh.position;
const scale = mesh.scale;
const scaleMax = max3(scale.x, scale.y, scale.z);
v4boundingSphere.multiplyScalar(scaleMax);
v4boundingSphere.add3(position);
const area = sphere_project(v4boundingSphere, cameraMatrix.elements, this.__focalLength);
/**
*
* @type {Vector2}
*/
const vs = this.__viewportSize;
const viewportWidth = vs.x;
const viewportHeight = vs.y;
const inPixels = area * viewportWidth * viewportHeight;
return inPixels;
}
update(timeDelta) {
this.__timeDelta = timeDelta;
/**
*
* @type {EntityManager}
*/
const em = this.entityManager;
/**
*
* @type {EntityComponentDataset}
*/
const ecd = em.dataset;
if (ecd === null) {
return;
}
/**
*
* @type {MeshSystem}
*/
const meshSystem = em.getSystem(MeshSystem);
if (meshSystem === null) {
throw new Error('MeshSystem system not found');
}
const firstActiveCamera = CameraSystem.getFirstActiveCamera(ecd).component;
if (firstActiveCamera === undefined) {
//no active camera found
return;
}
/**
* @type {THREE.PerspectiveCamera}
*/
const c = firstActiveCamera.object;
this.__projectionMatrix.copy(c.matrixWorld);
this.__projectionMatrix.invert();
this.__focalLength = c.fov / 180; //convert to Radians
ecd.traverseComponents(AnimationGraph, this.advanceGraphTime, this);
this.__animationGraphComponentIndex = ecd.computeComponentTypeIndex(AnimationGraph);
//update animations for visible meshes
meshSystem.traverseVisible(this.visitVisibleMesh, this);
}
}