UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

736 lines (573 loc) • 18.3 kB
import { AnimationMixer } from "three"; import { assert } from "../../../../../../core/assert.js"; import { combine_hash } from "../../../../../../core/collection/array/combine_hash.js"; import { computeHashFloat } from "../../../../../../core/primitives/numbers/computeHashFloat.js"; import { threeUpdateTransform } from "../../../../util/threeUpdateTransform.js"; import { AnimationGraphFlag } from "./AnimationGraphFlag.js"; import { AnimationState } from "./AnimationState.js"; import { AnimationStateType } from "./AnimationStateType.js"; import { AnimationTransition } from "./AnimationTransition.js"; import { readAnimationGraphDefinitionFromJSON } from "./definition/serialization/readAnimationGraphDefinitionFromJSON.js"; import { writeAnimationGraphDefinitionToJSON } from "./definition/serialization/writeAnimationGraphDefinitionToJSON.js"; export class AnimationGraph { constructor() { /** * * @type {AnimationGraphDefinition} */ this.def = null; /** * * @type {AnimationState} */ this.state = null; /** * * @type {AnimationTransition[]} */ this.transitions = []; /** * * @type {AnimationState[]} */ this.states = []; /** * * @type {AnimationTransition[]} */ this.activeTransitions = []; /** * * @type {AnimationState[]} */ this.simulatedStates = []; /** * * @type {BlendStateMatrix} */ this.blendState = null; /** * * @type {number} */ this.debtTime = 0; /** * * @type {EntityComponentDataset} * @private */ this.__dataset = null; /** * * @type {number} * @private */ this.__entity = -1; /** * * @type {AnimationMixer} * @private */ this.__mixer = null; /** * * @type {AnimationAction[]} * @private */ this.__actions = []; /** * * @type {Object3D} * @private */ this.__mesh = null; /** * * @type {number} */ this.flags = AnimationGraphFlag.MeshSizeCulling; } /** * * @param {AnimationGraph} other * @returns {boolean} */ equals(other) { if (this === other) { return true; } return this.flags === other.flags && this.debtTime === other.debtTime && this.state.equals(other.state) && this.def.equals(other.def) ; } /** * @returns {number} */ hash() { return combine_hash( this.flags, computeHashFloat(this.debtTime), this.state.hash(), this.def.hash() ); } toJSON() { return { def: writeAnimationGraphDefinitionToJSON(this.def), state: this.def.states.indexOf(this.state.def), debtTime: this.debtTime, flags: this.flags }; } /** * * @param {*} def Graph definition JSON * @param {number} state * @param {number} debtTime * @param {number} flags */ fromJSON({ def, state, debtTime = 0, flags = 0 }) { const graph_definition = readAnimationGraphDefinitionFromJSON(def); this.debtTime = debtTime; this.flags = flags; this.initialize(graph_definition); if (state === undefined) { state = graph_definition.states.indexOf(graph_definition.startingSate); } this.state = this.states[state]; } /** * * @param j * @returns {AnimationGraph} */ static fromJSON(j) { const r = new AnimationGraph(); r.fromJSON(j); return r; } /** * * @param {number|AnimationGraphFlag} flag * @returns {void} */ setFlag(flag) { this.flags |= flag; } /** * * @param {number|AnimationGraphFlag} flag * @returns {void} */ clearFlag(flag) { this.flags &= ~flag; } /** * * @param {number|AnimationGraphFlag} flag * @param {boolean} value */ writeFlag(flag, value) { if (value) { this.setFlag(flag); } else { this.clearFlag(flag); } } /** * * @param {number|AnimationGraphFlag} flag * @returns {boolean} */ getFlag(flag) { return (this.flags & flag) === flag; } /** * * @param {AnimationStateDefinition} stateDefinition * @returns {AnimationState} */ getStateByDefinition(stateDefinition) { assert.defined(stateDefinition, 'stateDefinition'); const states = this.states; const n = states.length; for (let i = 0; i < n; i++) { const state = states[i]; if (state.def === stateDefinition) { return state; } } return undefined; } /** * * @param {string} name * @return {AnimationState} */ getStateByClipName(name) { const animationStates = this.states; const n = animationStates.length; for (let i = 0; i < n; i++) { const state = animationStates[i]; /** * @type {AnimationStateDefinition} */ const stateDefinition = state.def; if (stateDefinition.type !== AnimationStateType.Clip) { continue; } if (stateDefinition.motion.def.name === name) { return state; } } } /** * * @param {string} name * @return {AnimationState|undefined} */ getStateByName(name) { const animationStates = this.states; const n = animationStates.length; for (let i = 0; i < n; i++) { const state = animationStates[i]; /** * @type {AnimationStateDefinition} */ const stateDefinition = state.def; if (stateDefinition.name === name) { return state; } } } /** * * @param {AnimationTransitionDefinition} transitionDefinition * @returns {AnimationTransition|undefined} */ getTransitionByDefinition(transitionDefinition) { const transitions = this.transitions; const n = transitions.length; for (let i = 0; i < n; i++) { const t = transitions[i]; if (t.def === transitionDefinition) { return t; } } } /** * * @param {AnimationGraphDefinition} def */ initialize(def) { this.def = def; this.blendState = def.createBlendState(); //build states this.states = []; const animationStateDefinitions = def.states; const stateCount = animationStateDefinitions.length; for (let i = 0; i < stateCount; i++) { const stateDefinition = animationStateDefinitions[i]; const state = new AnimationState(); state.def = stateDefinition; state.graph = this; this.states[i] = state; } this.transitions = []; const animationTransitionDefinitions = def.transitions; const transitionCount = animationTransitionDefinitions.length; for (let i = 0; i < transitionCount; i++) { const animationTransitionDefinition = animationTransitionDefinitions[i]; const transition = new AnimationTransition(); transition.def = animationTransitionDefinition; transition.graph = this; this.transitions[i] = transition; } this.state = this.getStateByDefinition(def.startingSate); //link transitions for (let i = 0; i < transitionCount; i++) { const transition = this.transitions[i]; const d = transition.def; transition.source = this.getStateByDefinition(d.source); transition.target = this.getStateByDefinition(d.target); transition.initialize(); } //link states for (let i = 0; i < stateCount; i++) { const state = this.states[i]; const d = state.def; state.inEdges = d.inEdges.map(this.getTransitionByDefinition, this); state.outEdges = d.outEdges.map(this.getTransitionByDefinition, this); //initialize states state.initialize(); } } /** * * @param {Object3D} mesh */ attach(mesh) { assert.defined(mesh, 'mesh'); assert.notNull(mesh, 'mesh'); assert.equal(mesh.isObject3D, true, 'Mesh.isMesh !== true'); if (this.__mesh === mesh) { return; } this.__mesh = mesh; this.__mixer = new AnimationMixer(mesh); /** * * @type {AnimationGraphDefinition} */ const graphDefinition = this.def; /** * * @type {AnimationClipDefinition[]} */ const clipIndex = graphDefinition.clipIndex; const nClips = clipIndex.length; /** * @type {AnimationClip[]} */ const animations = mesh.animations; if (animations === undefined) { throw new Error('Mesh.animations is undefined, no animations'); } const animationCount = animations.length; main: for (let i = 0; i < nClips; i++) { /** * * @type {AnimationClipDefinition} */ const animationClipDefinition = clipIndex[i]; for (let j = 0; j < animationCount; j++) { const animation = animations[j]; if (animation.name === animationClipDefinition.name) { const animationAction = this.__mixer.clipAction(animation, null); this.__actions[i] = animationAction; animationClipDefinition.duration = animation.duration; continue main; } } throw new Error(`Animation '${animationClipDefinition.name}' not found`); } } link(entity, ecd) { if (this.getFlag(AnimationGraphFlag.Linked)) { //already linked if (this.__entity === entity && this.__dataset === ecd) { //everything is the same, nothing to do return; } else { throw new Error('Graph is already linked to another source, must call .unlink first'); } } this.__entity = entity; this.__dataset = ecd; this.enterState(this.state); this.setFlag(AnimationGraphFlag.Linked); } unlink() { if (!this.getFlag(AnimationGraphFlag.Linked)) { //not linked, do nothing return; } this.exitState(this.state); this.clearFlag(AnimationGraphFlag.Linked); } /** * * @param {AnimationState} state * @returns {boolean} */ stopStateSimulation(state) { const j = this.simulatedStates.indexOf(state); if (j !== -1) { this.simulatedStates.splice(j, 1); return true; } else { return false; } } updateBlendState() { const nS = this.simulatedStates.length; for (let i = 0; i < nS; i++) { const s = this.simulatedStates[i]; s.updateBlendState(); } const animationTransitions = this.activeTransitions; const nAT = animationTransitions.length; const currentBlendState = this.blendState; if (nAT > 0) { if (nAT === 1) { //special case, only one active transition const first = animationTransitions[0]; first.updateBlendsState(); currentBlendState.copy(first.blendState); } else { currentBlendState.zero(); // accumulate for (let i = 0; i < nAT; i++) { const activeTransition = animationTransitions[i]; activeTransition.updateBlendsState(); currentBlendState.add(activeTransition.blendState); } // normalize currentBlendState.weightsMultiplyScalar(1 / nAT); } } else { //no active transitions, copy current state's blend matrix currentBlendState.copy(this.state.blendState); } //write blend state this.writeBlendState(); } writeBlendState() { if (this.__mixer == null) { return; } const blendState = this.blendState; const n = blendState.weights.length; for (let i = 0; i < n; i++) { const weight = blendState.weights[i]; const timeScale = blendState.timeScales[i]; /** * * @type {AnimationAction} */ const action = this.__actions[i]; if (action === undefined) { console.warn(`Action[${i}] is undefined`); continue; } action.weight = weight; action.timeScale = timeScale; } } /** * * @param {number} timeDelta in seconds */ tick(timeDelta) { const activeTransitions = this.activeTransitions; let nT = activeTransitions.length; transitions: for (let i = 0; i < nT; i++) { const activeTransition = activeTransitions[i]; activeTransition.tick(timeDelta); if (activeTransition.isFinished()) { //terminate transition activeTransitions.splice(i, 1); nT--; i--; //try to stop simulation of linked source state for (let j = 0; j < nT; j++) { const t = activeTransitions[j]; if (t.source === activeTransition.source || t.target === activeTransition.source) { //state is in use continue transitions; } } //source is not in use, we can terminate it this.stopStateSimulation(activeTransition.source); } } const simulatedStates = this.simulatedStates; const nS = simulatedStates.length; for (let i = 0; i < nS; i++) { const simulatedState = simulatedStates[i]; simulatedState.tick(timeDelta); } this.updateBlendState(); this.writeBlendState(); const mixer = this.__mixer; if (mixer !== null) { mixer.update(timeDelta); /** * get root * @type {Object3D} */ const root = mixer.getRoot(); //update bone matrix hierarchy // root.updateWorldMatrix(false, true); threeUpdateTransform(root); } } /** * * @param {AnimationState} state */ enterState(state) { this.state = state; if (this.simulatedStates.indexOf(state) === -1) { this.simulatedStates.push(state); } //activate new state const ecd = this.__dataset; const entity = this.__entity; state.activate(entity, ecd); const stateDefinition = state.def; if (stateDefinition.type === AnimationStateType.Clip) { /** * * @type {AnimationClip} */ const clip = stateDefinition.motion; /** * * @type {AnimationClipDefinition} */ const clipDefinition = clip.def; const clipIndex = this.def.getClipIndex(clipDefinition); /** * * @type {AnimationAction} */ const action = this.__actions[clipIndex]; if (action === undefined) { console.warn(`AnimationState does not have action bound. action is undefined.`); return; } clip.initializeThreeAnimationAction(action); } } /** * * @param {AnimationState} state */ exitState(state) { const entity = this.__entity; const ecd = this.__dataset; //de-activate old state's transitions state.deactivate(entity, ecd); } /** * * @param {AnimationState} state */ activateState(state) { this.exitState(this.state); this.enterState(state); } /** * * @param {AnimationTransition} transition */ transition(transition) { this.activeTransitions.push(transition); /** * * @type {AnimationState} */ const target = transition.target; this.activateState(target); } } /** * @readonly * @type {string} */ AnimationGraph.typeName = "AnimationGraph";