UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

315 lines (245 loc) • 7.98 kB
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); } }