playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
737 lines (736 loc) • 23.6 kB
JavaScript
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
import { Debug } from "../../../core/debug.js";
import { Asset } from "../../asset/asset.js";
import { AnimEvaluator } from "../../anim/evaluator/anim-evaluator.js";
import { AnimController } from "../../anim/controller/anim-controller.js";
import { Component } from "../component.js";
import { AnimComponentBinder } from "./component-binder.js";
import { AnimComponentLayer } from "./component-layer.js";
import { AnimStateGraph } from "../../anim/state-graph/anim-state-graph.js";
import { Entity } from "../../entity.js";
import {
ANIM_PARAMETER_BOOLEAN,
ANIM_PARAMETER_FLOAT,
ANIM_PARAMETER_INTEGER,
ANIM_PARAMETER_TRIGGER,
ANIM_CONTROL_STATES
} from "../../anim/controller/constants.js";
import { AnimTrack } from "../../anim/evaluator/anim-track.js";
class AnimComponent extends Component {
constructor() {
super(...arguments);
/** @private */
__publicField(this, "_stateGraphAsset", null);
/** @private */
__publicField(this, "_animationAssets", {});
/** @private */
__publicField(this, "_speed", 1);
/** @private */
__publicField(this, "_activate", true);
/** @private */
__publicField(this, "_playing", false);
/** @private */
__publicField(this, "_rootBone", null);
/** @private */
__publicField(this, "_stateGraph", null);
/** @private */
__publicField(this, "_layers", []);
/** @private */
__publicField(this, "_layerIndices", {});
/** @private */
__publicField(this, "_parameters", {});
/** @private */
__publicField(this, "_targets", {});
/** @private */
__publicField(this, "_consumedTriggers", /* @__PURE__ */ new Set());
/** @private */
__publicField(this, "_normalizeWeights", false);
/**
* Returns the parameter object for the specified parameter name. This function is anonymous so that it can be passed to the AnimController
* while still being called in the scope of the AnimComponent.
*
* @param {string} name - The name of the parameter to return the value of.
* @returns {object} The parameter object.
* @private
*/
__publicField(this, "findParameter", (name) => {
return this._parameters[name];
});
/**
* Sets a trigger parameter as having been used by a transition. This function is anonymous so that it can be passed to the AnimController
* while still being called in the scope of the AnimComponent.
*
* @param {string} name - The name of the trigger to set as consumed.
* @private
*/
__publicField(this, "consumeTrigger", (name) => {
this._consumedTriggers.add(name);
});
}
set stateGraphAsset(value) {
if (value === null) {
this.removeStateGraph();
return;
}
if (this._stateGraphAsset) {
const stateGraphAsset = this.system.app.assets.get(this._stateGraphAsset);
stateGraphAsset.off("change", this._onStateGraphAssetChangeEvent, this);
}
let _id;
let _asset;
if (value instanceof Asset) {
_id = value.id;
_asset = this.system.app.assets.get(_id);
if (!_asset) {
this.system.app.assets.add(value);
_asset = this.system.app.assets.get(_id);
}
} else {
_id = value;
_asset = this.system.app.assets.get(_id);
}
if (!_asset || this._stateGraphAsset === _id) {
return;
}
if (_asset.resource) {
this._stateGraph = _asset.resource;
this.loadStateGraph(this._stateGraph);
_asset.on("change", this._onStateGraphAssetChangeEvent, this);
} else {
_asset.once("load", (asset) => {
this._stateGraph = asset.resource;
this.loadStateGraph(this._stateGraph);
});
_asset.on("change", this._onStateGraphAssetChangeEvent, this);
this.system.app.assets.load(_asset);
}
this._stateGraphAsset = _id;
}
get stateGraphAsset() {
return this._stateGraphAsset;
}
/**
* Sets whether the animation component will normalize the weights of its layers by their sum total.
*
* @type {boolean}
*/
set normalizeWeights(value) {
this._normalizeWeights = value;
this.unbind();
}
/**
* Gets whether the animation component will normalize the weights of its layers by their sum total.
*
* @type {boolean}
*/
get normalizeWeights() {
return this._normalizeWeights;
}
set animationAssets(value) {
this._animationAssets = value;
this.loadAnimationAssets();
}
get animationAssets() {
return this._animationAssets;
}
/**
* Sets the speed multiplier for animation play back speed. 1.0 is playback at normal speed, 0.0 pauses
* the animation.
*
* @type {number}
*/
set speed(value) {
this._speed = value;
}
/**
* Gets the speed multiplier for animation play back speed.
*
* @type {number}
*/
get speed() {
return this._speed;
}
/**
* Sets whether the first animation will begin playing when the scene is loaded.
*
* @type {boolean}
*/
set activate(value) {
this._activate = value;
}
/**
* Gets whether the first animation will begin playing when the scene is loaded.
*
* @type {boolean}
*/
get activate() {
return this._activate;
}
/**
* Sets whether to play or pause all animations in the component.
*
* @type {boolean}
*/
set playing(value) {
this._playing = value;
}
/**
* Gets whether to play or pause all animations in the component.
*
* @type {boolean}
*/
get playing() {
return this._playing;
}
/**
* Sets the entity that this anim component should use as the root of the animation hierarchy.
*
* @type {Entity}
*/
set rootBone(value) {
if (typeof value === "string") {
const entity = this.entity.root.findByGuid(value);
Debug.assert(entity, `rootBone entity for supplied guid:${value} cannot be found in the scene`);
this._rootBone = entity;
} else if (value instanceof Entity) {
this._rootBone = value;
} else {
this._rootBone = null;
}
this.rebind();
}
/**
* Gets the entity that this anim component should use as the root of the animation hierarchy.
*
* @type {Entity}
*/
get rootBone() {
return this._rootBone;
}
set stateGraph(value) {
this._stateGraph = value;
}
get stateGraph() {
return this._stateGraph;
}
/**
* Returns the animation layers available in this anim component.
*
* @type {AnimComponentLayer[]}
*/
get layers() {
return this._layers;
}
set layerIndices(value) {
this._layerIndices = value;
}
get layerIndices() {
return this._layerIndices;
}
set parameters(value) {
this._parameters = value;
}
get parameters() {
return this._parameters;
}
set targets(value) {
this._targets = value;
}
get targets() {
return this._targets;
}
/**
* Returns whether all component layers are currently playable.
*
* @type {boolean}
*/
get playable() {
for (let i = 0; i < this._layers.length; i++) {
if (!this._layers[i].playable) {
return false;
}
}
return true;
}
/**
* Returns the base layer of the state graph.
*
* @type {AnimComponentLayer|null}
*/
get baseLayer() {
if (this._layers.length > 0) {
return this._layers[0];
}
return null;
}
_onStateGraphAssetChangeEvent(asset) {
const prevAnimationAssets = this.animationAssets;
const prevMasks = this.layers.map((layer) => layer.mask);
this.removeStateGraph();
this._stateGraph = new AnimStateGraph(asset._data);
this.loadStateGraph(this._stateGraph);
this.animationAssets = prevAnimationAssets;
this.loadAnimationAssets();
this.layers.forEach((layer, i) => {
layer.mask = prevMasks[i];
});
this.rebind();
}
dirtifyTargets() {
const targets = Object.values(this._targets);
for (let i = 0; i < targets.length; i++) {
targets[i].dirty = true;
}
}
_addLayer({ name, states, transitions, weight, mask, blendType }) {
let graph;
if (this.rootBone) {
graph = this.rootBone;
} else {
graph = this.entity;
}
const layerIndex = this._layers.length;
const animBinder = new AnimComponentBinder(this, graph, name, mask, layerIndex);
const animEvaluator = new AnimEvaluator(animBinder);
const controller = new AnimController(
animEvaluator,
states,
transitions,
this._activate,
this,
this.findParameter,
this.consumeTrigger
);
this._layers.push(new AnimComponentLayer(name, controller, this, weight, blendType));
this._layerIndices[name] = layerIndex;
return this._layers[layerIndex];
}
/**
* Adds a new anim component layer to the anim component.
*
* @param {string} name - The name of the layer to create.
* @param {number} [weight] - The blending weight of the layer. Defaults to 1.
* @param {object[]} [mask] - A list of paths to bones in the model which should be animated in
* this layer. If omitted the full model is used. Defaults to null.
* @param {string} [blendType] - Defines how properties animated by this layer blend with
* animations of those properties in previous layers. Defaults to pc.ANIM_LAYER_OVERWRITE.
* @returns {AnimComponentLayer} The created anim component layer.
*/
addLayer(name, weight, mask, blendType) {
const layer = this.findAnimationLayer(name);
if (layer) return layer;
const states = [
{
"name": "START",
"speed": 1
}
];
const transitions = [];
return this._addLayer({ name, states, transitions, weight, mask, blendType });
}
_assignParameters(stateGraph) {
this._parameters = {};
const paramKeys = Object.keys(stateGraph.parameters);
for (let i = 0; i < paramKeys.length; i++) {
const paramKey = paramKeys[i];
this._parameters[paramKey] = {
type: stateGraph.parameters[paramKey].type,
value: stateGraph.parameters[paramKey].value
};
}
}
/**
* Initializes component animation controllers using the provided state graph.
*
* @param {object} stateGraph - The state graph asset to load into the component. Contains the
* states, transitions and parameters used to define a complete animation controller.
* @example
* entity.anim.loadStateGraph({
* "layers": [
* {
* "name": layerName,
* "states": [
* {
* "name": "START",
* "speed": 1
* },
* {
* "name": "Initial State",
* "speed": speed,
* "loop": loop,
* "defaultState": true
* }
* ],
* "transitions": [
* {
* "from": "START",
* "to": "Initial State"
* }
* ]
* }
* ],
* "parameters": {}
* });
*/
loadStateGraph(stateGraph) {
this._stateGraph = stateGraph;
this._assignParameters(stateGraph);
this._layers = [];
let containsBlendTree = false;
for (let i = 0; i < stateGraph.layers.length; i++) {
const layer = stateGraph.layers[i];
this._addLayer({ ...layer });
if (layer.states.some((state) => state.blendTree)) {
containsBlendTree = true;
}
}
if (!containsBlendTree) {
this.setupAnimationAssets();
}
}
setupAnimationAssets() {
for (let i = 0; i < this._layers.length; i++) {
const layer = this._layers[i];
const layerName = layer.name;
for (let j = 0; j < layer.states.length; j++) {
const stateName = layer.states[j];
if (ANIM_CONTROL_STATES.indexOf(stateName) === -1) {
const stateKey = `${layerName}:${stateName}`;
if (!this._animationAssets[stateKey]) {
this._animationAssets[stateKey] = {
asset: null
};
}
}
}
}
this.loadAnimationAssets();
}
loadAnimationAssets() {
for (let i = 0; i < this._layers.length; i++) {
const layer = this._layers[i];
for (let j = 0; j < layer.states.length; j++) {
const stateName = layer.states[j];
if (ANIM_CONTROL_STATES.indexOf(stateName) !== -1) continue;
const animationAsset = this._animationAssets[`${layer.name}:${stateName}`];
if (!animationAsset || !animationAsset.asset) {
this.findAnimationLayer(layer.name).assignAnimation(stateName, AnimTrack.EMPTY);
continue;
}
const assetId = animationAsset.asset;
const asset = this.system.app.assets.get(assetId);
if (asset) {
if (asset.resource) {
this.onAnimationAssetLoaded(layer.name, stateName, asset);
} else {
asset.once("load", function(layerName, stateName2) {
return function(asset2) {
this.onAnimationAssetLoaded(layerName, stateName2, asset2);
}.bind(this);
}.bind(this)(layer.name, stateName));
this.system.app.assets.load(asset);
}
}
}
}
}
onAnimationAssetLoaded(layerName, stateName, asset) {
this.findAnimationLayer(layerName).assignAnimation(stateName, asset.resource);
}
/**
* Removes all layers from the anim component.
*/
removeStateGraph() {
this._stateGraph = null;
this._stateGraphAsset = null;
this._animationAssets = {};
this._layers = [];
this._layerIndices = {};
this._parameters = {};
this._playing = false;
this.unbind();
this._targets = {};
}
/**
* Reset all of the components layers and parameters to their initial states. If a layer was
* playing before it will continue playing.
*/
reset() {
this._assignParameters(this._stateGraph);
for (let i = 0; i < this._layers.length; i++) {
const layerPlaying = this._layers[i].playing;
this._layers[i].reset();
this._layers[i].playing = layerPlaying;
}
}
unbind() {
if (!this._normalizeWeights) {
Object.keys(this._targets).forEach((targetKey) => {
this._targets[targetKey].unbind();
});
}
}
/**
* Rebind all of the components layers.
*/
rebind() {
this._targets = {};
for (let i = 0; i < this._layers.length; i++) {
this._layers[i].rebind();
}
}
/**
* Finds an {@link AnimComponentLayer} in this component.
*
* @param {string} name - The name of the anim component layer to find.
* @returns {AnimComponentLayer} Layer.
*/
findAnimationLayer(name) {
const layerIndex = this._layerIndices[name];
return this._layers[layerIndex] || null;
}
addAnimationState(nodeName, animTrack, speed = 1, loop = true, layerName = "Base") {
if (!this._stateGraph) {
this.loadStateGraph(new AnimStateGraph({
"layers": [
{
"name": layerName,
"states": [
{
"name": "START",
"speed": 1
},
{
"name": nodeName,
"speed": speed,
"loop": loop,
"defaultState": true
}
],
"transitions": [
{
"from": "START",
"to": nodeName
}
]
}
],
"parameters": {}
}));
}
const layer = this.findAnimationLayer(layerName);
if (layer) {
layer.assignAnimation(nodeName, animTrack, speed, loop);
} else {
this.addLayer(layerName)?.assignAnimation(nodeName, animTrack, speed, loop);
}
}
/**
* Associates an animation with a state or blend tree node in the loaded state graph. If all
* states are linked and the {@link activate} value was set to true then the component will
* begin playing. If no state graph is loaded, a default state graph will be created with a
* single state based on the provided nodePath parameter.
*
* @param {string} nodePath - Either the state name or the path to a blend tree node that this
* animation should be associated with. Each section of a blend tree path is split using a
* period (`.`) therefore state names should not include this character (e.g "MyStateName" or
* "MyStateName.BlendTreeNode").
* @param {AnimTrack} animTrack - The animation track that will be assigned to this state and
* played whenever this state is active.
* @param {string} [layerName] - The name of the anim component layer to update. If omitted the
* default layer is used. If no state graph has been previously loaded this parameter is
* ignored.
* @param {number} [speed] - Update the speed of the state you are assigning an animation to.
* Defaults to 1.
* @param {boolean} [loop] - Update the loop property of the state you are assigning an
* animation to. Defaults to true.
*/
assignAnimation(nodePath, animTrack, layerName, speed = 1, loop = true) {
if (!this._stateGraph && nodePath.indexOf(".") === -1) {
this.loadStateGraph(new AnimStateGraph({
"layers": [
{
"name": "Base",
"states": [
{
"name": "START",
"speed": 1
},
{
"name": nodePath,
"speed": speed,
"loop": loop,
"defaultState": true
}
],
"transitions": [
{
"from": "START",
"to": nodePath
}
]
}
],
"parameters": {}
}));
this.baseLayer.assignAnimation(nodePath, animTrack);
return;
}
const layer = layerName ? this.findAnimationLayer(layerName) : this.baseLayer;
if (!layer) {
Debug.error("assignAnimation: Trying to assign an anim track to a layer that doesn't exist");
return;
}
layer.assignAnimation(nodePath, animTrack, speed, loop);
}
/**
* Removes animations from a node in the loaded state graph.
*
* @param {string} nodeName - The name of the node that should have its animation tracks removed.
* @param {string} [layerName] - The name of the anim component layer to update. If omitted the
* default layer is used.
*/
removeNodeAnimations(nodeName, layerName) {
const layer = layerName ? this.findAnimationLayer(layerName) : this.baseLayer;
if (!layer) {
Debug.error("removeStateAnimations: Trying to remove animation tracks from a state before the state graph has been loaded. Have you called loadStateGraph?");
return;
}
layer.removeNodeAnimations(nodeName);
}
getParameterValue(name, type) {
const param = this._parameters[name];
if (param && param.type === type) {
return param.value;
}
Debug.log(`Cannot get parameter value. No parameter found in anim controller named "${name}" of type "${type}"`);
return void 0;
}
setParameterValue(name, type, value) {
const param = this._parameters[name];
if (param && param.type === type) {
param.value = value;
return;
}
Debug.log(`Cannot set parameter value. No parameter found in anim controller named "${name}" of type "${type}"`);
}
/**
* Returns a float parameter value by name.
*
* @param {string} name - The name of the float to return the value of.
* @returns {number} A float.
*/
getFloat(name) {
return this.getParameterValue(name, ANIM_PARAMETER_FLOAT);
}
/**
* Sets the value of a float parameter that was defined in the animation components state graph.
*
* @param {string} name - The name of the parameter to set.
* @param {number} value - The new float value to set this parameter to.
*/
setFloat(name, value) {
this.setParameterValue(name, ANIM_PARAMETER_FLOAT, value);
}
/**
* Returns an integer parameter value by name.
*
* @param {string} name - The name of the integer to return the value of.
* @returns {number} An integer.
*/
getInteger(name) {
return this.getParameterValue(name, ANIM_PARAMETER_INTEGER);
}
/**
* Sets the value of an integer parameter that was defined in the animation components state
* graph.
*
* @param {string} name - The name of the parameter to set.
* @param {number} value - The new integer value to set this parameter to.
*/
setInteger(name, value) {
if (typeof value === "number" && value % 1 === 0) {
this.setParameterValue(name, ANIM_PARAMETER_INTEGER, value);
} else {
Debug.error("Attempting to assign non integer value to integer parameter", name, value);
}
}
/**
* Returns a boolean parameter value by name.
*
* @param {string} name - The name of the boolean to return the value of.
* @returns {boolean} A boolean.
*/
getBoolean(name) {
return this.getParameterValue(name, ANIM_PARAMETER_BOOLEAN);
}
/**
* Sets the value of a boolean parameter that was defined in the animation components state
* graph.
*
* @param {string} name - The name of the parameter to set.
* @param {boolean} value - The new boolean value to set this parameter to.
*/
setBoolean(name, value) {
this.setParameterValue(name, ANIM_PARAMETER_BOOLEAN, !!value);
}
/**
* Returns a trigger parameter value by name.
*
* @param {string} name - The name of the trigger to return the value of.
* @returns {boolean} A boolean.
*/
getTrigger(name) {
return this.getParameterValue(name, ANIM_PARAMETER_TRIGGER);
}
/**
* Sets the value of a trigger parameter that was defined in the animation components state
* graph to true.
*
* @param {string} name - The name of the parameter to set.
* @param {boolean} [singleFrame] - If true, this trigger will be set back to false at the end
* of the animation update. Defaults to false.
*/
setTrigger(name, singleFrame = false) {
this.setParameterValue(name, ANIM_PARAMETER_TRIGGER, true);
if (singleFrame) {
this._consumedTriggers.add(name);
}
}
/**
* Resets the value of a trigger parameter that was defined in the animation components state
* graph to false.
*
* @param {string} name - The name of the parameter to set.
*/
resetTrigger(name) {
this.setParameterValue(name, ANIM_PARAMETER_TRIGGER, false);
}
onBeforeRemove() {
if (Number.isFinite(this._stateGraphAsset)) {
const stateGraphAsset = this.system.app.assets.get(this._stateGraphAsset);
stateGraphAsset.off("change", this._onStateGraphAssetChangeEvent, this);
}
}
update(dt) {
for (let i = 0; i < this.layers.length; i++) {
this.layers[i].update(dt * this.speed);
}
this._consumedTriggers.forEach((trigger) => {
this.parameters[trigger].value = false;
});
this._consumedTriggers.clear();
}
resolveDuplicatedEntityReferenceProperties(oldAnim, duplicatedIdsMap) {
if (oldAnim.rootBone && duplicatedIdsMap[oldAnim.rootBone.guid]) {
this.rootBone = duplicatedIdsMap[oldAnim.rootBone.guid];
} else {
this.rebind();
}
}
}
export {
AnimComponent
};