playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
485 lines (484 loc) • 12.7 kB
JavaScript
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 {
_stateGraphAsset = null;
_animationAssets = {};
_speed = 1;
_activate = true;
_playing = false;
_rootBone = null;
_stateGraph = null;
_layers = [];
_layerIndices = {};
_parameters = {};
_targets = {};
_consumedTriggers = /* @__PURE__ */ new Set();
_normalizeWeights = false;
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;
}
set normalizeWeights(value) {
this._normalizeWeights = value;
this.unbind();
}
get normalizeWeights() {
return this._normalizeWeights;
}
set animationAssets(value) {
this._animationAssets = value;
this.loadAnimationAssets();
}
get animationAssets() {
return this._animationAssets;
}
set speed(value) {
this._speed = value;
}
get speed() {
return this._speed;
}
set activate(value) {
this._activate = value;
}
get activate() {
return this._activate;
}
set playing(value) {
this._playing = value;
}
get playing() {
return this._playing;
}
set rootBone(value) {
if (typeof value === "string") {
const entity = this.entity.root.findByGuid(value);
this._rootBone = entity;
} else if (value instanceof Entity) {
this._rootBone = value;
} else {
this._rootBone = null;
}
this.rebind();
}
get rootBone() {
return this._rootBone;
}
set stateGraph(value) {
this._stateGraph = value;
}
get stateGraph() {
return this._stateGraph;
}
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;
}
get playable() {
for (let i = 0; i < this._layers.length; i++) {
if (!this._layers[i].playable) {
return false;
}
}
return true;
}
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];
}
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
};
}
}
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);
}
removeStateGraph() {
this._stateGraph = null;
this._stateGraphAsset = null;
this._animationAssets = {};
this._layers = [];
this._layerIndices = {};
this._parameters = {};
this._playing = false;
this.unbind();
this._targets = {};
}
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() {
this._targets = {};
for (let i = 0; i < this._layers.length; i++) {
this._layers[i].rebind();
}
}
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);
}
}
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) {
return;
}
layer.assignAnimation(nodePath, animTrack, speed, loop);
}
removeNodeAnimations(nodeName, layerName) {
const layer = layerName ? this.findAnimationLayer(layerName) : this.baseLayer;
if (!layer) {
return;
}
layer.removeNodeAnimations(nodeName);
}
getParameterValue(name, type) {
const param = this._parameters[name];
if (param && param.type === type) {
return param.value;
}
return void 0;
}
setParameterValue(name, type, value) {
const param = this._parameters[name];
if (param && param.type === type) {
param.value = value;
return;
}
}
findParameter = (name) => {
return this._parameters[name];
};
consumeTrigger = (name) => {
this._consumedTriggers.add(name);
};
getFloat(name) {
return this.getParameterValue(name, ANIM_PARAMETER_FLOAT);
}
setFloat(name, value) {
this.setParameterValue(name, ANIM_PARAMETER_FLOAT, value);
}
getInteger(name) {
return this.getParameterValue(name, ANIM_PARAMETER_INTEGER);
}
setInteger(name, value) {
if (typeof value === "number" && value % 1 === 0) {
this.setParameterValue(name, ANIM_PARAMETER_INTEGER, value);
} else {
}
}
getBoolean(name) {
return this.getParameterValue(name, ANIM_PARAMETER_BOOLEAN);
}
setBoolean(name, value) {
this.setParameterValue(name, ANIM_PARAMETER_BOOLEAN, !!value);
}
getTrigger(name) {
return this.getParameterValue(name, ANIM_PARAMETER_TRIGGER);
}
setTrigger(name, singleFrame = false) {
this.setParameterValue(name, ANIM_PARAMETER_TRIGGER, true);
if (singleFrame) {
this._consumedTriggers.add(name);
}
}
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
};