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