UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

384 lines (307 loc) • 10.8 kB
import { AnimationMixer as ThreeAnimationMixer, LoopOnce as ThreeLoopOnce, LoopRepeat as ThreeLoopRepeat, Matrix4 as ThreeMatrix4 } 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 Future, { FutureStates } from '../../../core/process/Future.js'; import { CameraSystem } from '../../graphics/ecs/camera/CameraSystem.js'; import Mesh from '../../graphics/ecs/mesh/Mesh.js'; import { MeshEvents } from "../../graphics/ecs/mesh/MeshEvents.js"; import { MeshSystem } from "../../graphics/ecs/mesh/MeshSystem.js"; import { threeUpdateTransform } from "../../graphics/util/threeUpdateTransform.js"; import { Animation } from '../animation/Animation.js'; import { AnimationClipFlag } from "../animation/AnimationClipFlag.js"; import { AnimationFlags } from "../animation/AnimationFlags.js"; import { System } from '../System.js'; /** * * @param {AnimationMixer} mixer * @param mesh * @param {object<string,Future<AnimationAction>>} actions */ function constructActionClips(mixer, mesh, actions) { function registerAnimationAction(animation) { const clipName = animation.name; if (actions.hasOwnProperty(clipName)) { //already have such an animation //TODO consider different animation sets with same animation names return; } const root = null; const action = new Future(function (resolve, reject) { const clipAction = mixer.clipAction(animation, root); resolve(clipAction); }); actions[clipName] = action; } const animations = mesh.animations; if (animations === undefined) { console.warn('Mesh has no animations'); } if (animations !== undefined && animations.length > 0) { for (let i = 0; i < animations.length; i++) { const animation = animations[i]; registerAnimationAction(animation); } } } function useClip(component, clip, callback) { //find clip const actionClips = component.actionClips; const clipName = clip.name.getValue(); const actionClip = actionClips[clipName]; if (actionClip === undefined) { //no clip found return; } actionClip.resolve(); actionClip.then(callback); } /** * * @param component * @param {AnimationClip} clip */ function startClip(component, clip) { /** * * @param {AnimationAction} clipAction */ function visitClipAction(clipAction) { clipAction.repetitions = clip.repeatCount.getValue(); if (clipAction.repetitions === Number.POSITIVE_INFINITY) { clipAction.loop = ThreeLoopRepeat; } else { clipAction.loop = ThreeLoopOnce; } clipAction.timeScale = clip.timeScale.getValue(); clipAction.setEffectiveWeight(clip.weight.getValue()); clipAction.clampWhenFinished = clip.getFlag(AnimationClipFlag.ClampWhenFinished); if (clipAction.repetitions > 0 && !clipAction.isRunning()) { clipAction.play(); } } useClip(component, clip, visitClipAction); } /** * * @param {Animation} component * @ignore */ function updateAnimationState(component) { const clips = component.clips; function initAnimationClip(ac) { startClip(component, ac); } clips.forEach(initAnimationClip); clips.on.added.add(initAnimationClip); clips.on.removed.add(function (ac) { useClip(component, ac, function (clipAction) { clipAction.stop(); }); }); } /** * * @param {Animation} component * @param {Mesh} model3d */ function registerAnimation(component, model3d) { if (!model3d.hasMesh()) { return; } let mesh = model3d.mesh; //find animation data if (component.mixer === undefined || component.mesh !== mesh) { component.mesh = mesh; component.mixer = new Future(function (resolve, reject) { component.actionClips = {}; const mixer = new ThreeAnimationMixer(mesh); constructActionClips(mixer, mesh, component.actionClips); resolve(mixer); }); } component.mixer.then(function (mixer) { updateAnimationState(component); }); } /** * * @param {ThreeAnimationMixer} mixer * @param {number} timeDelta */ export function advanceAnimation(mixer, timeDelta) { mixer.update(timeDelta); /** * get root * @type {Object3D} */ const root = mixer.getRoot(); //update bone matrix hierarchy // root.updateWorldMatrix(false, true); threeUpdateTransform(root); } /** * @type {Vector4} */ const v4boundingSphere = new Vector4(); class AnimationSystem extends System { /** * * @param {Vector2} viewportSize * @constructor */ constructor(viewportSize) { super(); this.dependencies = [Animation, Mesh]; this.viewportSize = viewportSize; } /** * * @param {Animation} component * @param mesh * @param {int} entity */ link(component, mesh, entity) { const em = this.entityManager; registerAnimation(component, mesh); component.__listenerMeshSet = function (event) { const mesh = event.component; registerAnimation(component, mesh); }; em.dataset.addEntityEventListener(entity, MeshEvents.DataSet, component.__listenerMeshSet); } /** * * @param {Animation} component * @param mesh * @param {int} entity */ unlink(component, mesh, entity) { this.entityManager.dataset.removeEntityEventListener(entity, MeshEvents.DataSet, component.__listenerMeshSet); const animation = component.mixer; if (animation !== void 0 && animation !== null) { animation.then(function (mixer) { mixer.stopAllAction(); }); } } /** * * @param {number} timeDelta */ update(timeDelta) { const em = this.entityManager; const ecd = em.dataset; if (ecd === null) { //no data, nothing to update return; } const firstActiveCamera = CameraSystem.getFirstActiveCamera(ecd).component; if (firstActiveCamera === undefined) { //no active camera found return; } /** * @type {THREE.PerspectiveCamera} */ const c = firstActiveCamera.object; const projectionMatrix = new ThreeMatrix4(); projectionMatrix.copy(c.matrixWorld); projectionMatrix.invert(); const focalLength = c.fov / 180; //convert to Radians const viewportSize = this.viewportSize; /** * * @param {Mesh} mesh trhee.js Mesh instance * @param {Matrix4} cameraMatrix */ function screenSpaceSize(mesh, cameraMatrix) { 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, focalLength); const inPixels = area * viewportSize.x * viewportSize.y; return inPixels; } /** * * @param entity * @param {Animation} animation * @param {Mesh} meshComponent * @returns {boolean} */ function shouldEntityBeAnimated(entity, animation, meshComponent) { if (meshComponent === undefined) { //no mesh component return false; } const mesh = meshComponent.mesh; if (mesh === null) { //no renderable object return false; } if (animation.getFlag(AnimationFlags.MeshSizeCulling)) { //check the size of the mesh in screen space, culling animation of tiny objects const areaInPixel = screenSpaceSize(mesh, projectionMatrix); if (areaInPixel < 32) { //too tiny return false; } } //passed all filters, visible return true; } //advance time for all playing animations ecd.traverseComponents(Animation, function (animation, entity) { if (animation.isPlaying) { animation.debtTime += timeDelta; } }); /** * * @type {MeshSystem} */ const meshSystem = em.getSystem(MeshSystem); const animationComponentIndex = ecd.computeComponentTypeIndex(Animation); //update animations for visible meshes meshSystem.traverseVisible((mesh, entity) => { /** * * @type {Animation} */ const animation = ecd.getComponentByIndex(entity, animationComponentIndex); if (animation === undefined) { //mesh has no animation, skip return true; } if (animation.mixer !== undefined && animation.isPlaying && shouldEntityBeAnimated(entity, animation, mesh)) { const dt = animation.debtTime; if (dt > 0) { const mixerFuture = animation.mixer; if (mixerFuture.state === FutureStates.RESOLVED) { const mixer = mixerFuture.resolvedValue; animation.debtTime = 0; advanceAnimation(mixer, dt); } else if (mixerFuture.state === FutureStates.INITIAL) { mixerFuture.resolve(); } } } }); } } export default AnimationSystem;