Visitar URL original
fix: Release anim evaluator targets when clips are removed by ankit1324 · Pull Request #9861 · playcanvas/engine · GitHub
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 19 additions & 25 deletions src/framework/anim/evaluator/anim-evaluator.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ class AnimEvaluator {
target: resolved, // resolved target instance
value: [], // storage for calculated value
curves: 0, // number of curves driving this target
blendCounter: 0 // per-frame number of blends (used to identify first blend)
blendCounter: 0, // per-frame number of blends (used to identify first blend)
layerTarget: null // anim component target value this layer counts towards
};

for (let k = 0; k < target.target.components; ++k) {
Expand All @@ -81,8 +82,10 @@ class AnimEvaluator {
}
binder.animComponent.targets[resolved.targetPath] = new AnimTargetValue(binder.animComponent, type);
}
binder.animComponent.targets[resolved.targetPath].layerCounter++;
binder.animComponent.targets[resolved.targetPath].setMask(binder.layerIndex, 1);
// keep a reference, the component replaces its targets map when it rebinds
target.layerTarget = binder.animComponent.targets[resolved.targetPath];
target.layerTarget.layerCounter++;
target.layerTarget.setLayerBound(binder.layerIndex, 1);
}
}

Expand Down Expand Up @@ -111,28 +114,18 @@ class AnimEvaluator {
removeClip(index) {
const targets = this._targets;
const binder = this._binder;

const clips = this._clips;
const clip = clips[index];
const curves = clip.track.curves;

for (let i = 0; i < curves.length; ++i) {
const curve = curves[i];
const paths = curve.paths;
for (let j = 0; j < paths.length; ++j) {
const path = paths[j];

const target = this._binder.resolve(path);

if (target) {
target.curves--;
if (target.curves === 0) {
binder.unresolve(path);
delete targets[target.targetPath];
if (binder.animComponent) {
binder.animComponent.targets[target.targetPath].layerCounter--;
}
}
const outputs = this._outputs[index];

for (let i = 0; i < outputs.length; ++i) {
const target = outputs[i];
target.curves--;
if (target.curves === 0) {
delete targets[target.target.targetPath];
const layerTarget = target.layerTarget;
if (layerTarget) {
layerTarget.layerCounter--;
layerTarget.setLayerBound(binder.layerIndex, 0);
}
}
}
Expand Down Expand Up @@ -265,7 +258,8 @@ class AnimEvaluator {
// if this evaluator is associated with an anim component then we should blend the result of this evaluator with all other anim layer's evaluators
if (binder.animComponent && target.target.usesLayerBlending) {
const animTarget = binder.animComponent.targets[path];
if (animTarget.counter === animTarget.layerCounter) {
// a layer that stops animating this target leaves the counter ahead of layerCounter
if (animTarget.counter >= animTarget.layerCounter) {
animTarget.counter = 0;
}
if (!animTarget.path) {
Expand Down
28 changes: 28 additions & 0 deletions src/framework/anim/evaluator/anim-target-value.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class AnimTargetValue {
constructor(component, type) {
this._component = component;
this.mask = new Int8Array(component.layers.length);
this._bound = new Int8Array(component.layers.length);
this.weights = new Float32Array(component.layers.length);
this.totalWeight = 0;
this.counter = 0;
Expand Down Expand Up @@ -76,6 +77,33 @@ class AnimTargetValue {
}
}

/**
* Sets whether a layer animates this target and updates the mask of every layer. With
* normalized weights, a layer is masked out while an overwrite layer above it animates this
* target, whatever the order in which the layers started or stopped animating it.
*
* @param {number} index - The index of the layer.
* @param {number} value - 1 if the layer animates this target, 0 otherwise.
*/
setLayerBound(index, value) {
const bound = this._bound;
const mask = this.mask;
bound[index] = value;
if (this._normalizeWeights) {
const layers = this._component.layers;
let overwritten = false;
for (let i = mask.length - 1; i >= 0; i--) {
mask[i] = overwritten ? 0 : bound[i];
if (bound[i] && layers[i].blendType === ANIM_LAYER_OVERWRITE) {
overwritten = true;
}
}
this.dirty = true;
} else {
mask[index] = value;
}
}

updateWeights() {
this.totalWeight = 0;
for (let i = 0; i < this.weights.length; i++) {
Expand Down
124 changes: 124 additions & 0 deletions test/framework/anim/evaluator/anim-evaluator.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -165,4 +165,128 @@ describe('AnimEvaluator', function () {
animEvaluator.update(1.0, false);
expect(clip.time).to.equal(0.5);
});

it('AnimEvaluator: removeClip stops animating the targets of the removed clip', function () {
const parent = new GraphNode('parent');
const child1 = new GraphNode('child1');
app.root.addChild(parent);
parent.addChild(child1);

const keys = new AnimData(1, [0, 1]);
const translations = new AnimData(3, [0, 0, 0, 1, 0, 0]);
const curvePath = {
entityPath: ['child1'],
component: 'graph',
propertyPath: ['localPosition']
};
const curve = new AnimCurve([curvePath], 0, 0, INTERPOLATION_LINEAR);
const track = new AnimTrack('test track', 1, [keys], [translations], [curve]);
const clip = new AnimClip(track, 0.0, 1.0, true, true);

const animEvaluator = new AnimEvaluator(new DefaultAnimBinder(parent));
animEvaluator.addClip(clip);
animEvaluator.update(0.5);
expect(child1.localPosition.x).to.equal(0.5);

animEvaluator.removeClip(0);
child1.setLocalPosition(5, 0, 0);
animEvaluator.update(0.5);

expect(child1.localPosition.x).to.equal(5);
});

it('AnimEvaluator: removeClip keeps updating the world transform of nodes still animated', function () {
const parent = new GraphNode('parent');
const child1 = new GraphNode('child1');
const child2 = new GraphNode('child2');
app.root.addChild(parent);
parent.addChild(child1);
parent.addChild(child2);

const keys = new AnimData(1, [0, 1]);
const translations = new AnimData(3, [0, 0, 0, 1, 0, 0]);
const createClip = (name) => {
const curvePath = {
entityPath: [name],
component: 'graph',
propertyPath: ['localPosition']
};
const curve = new AnimCurve([curvePath], 0, 0, INTERPOLATION_LINEAR);
const track = new AnimTrack(name, 1, [keys], [translations], [curve]);
return new AnimClip(track, 0.0, 1.0, true, true);
};

const animEvaluator = new AnimEvaluator(new DefaultAnimBinder(parent));
animEvaluator.addClip(createClip('child1'));
animEvaluator.addClip(createClip('child2'));
animEvaluator.update(0.5);
expect(child2.getPosition().x).to.equal(0.5);

animEvaluator.removeClip(0);
animEvaluator.update(0.25);
expect(child2.getPosition().x).to.equal(0.75);
});

it('AnimEvaluator: removeClip keeps animating a node driven by another clip until both are removed', function () {
const parent = new GraphNode('parent');
const child1 = new GraphNode('child1');
app.root.addChild(parent);
parent.addChild(child1);

const keys = new AnimData(1, [0, 1]);
const translations = new AnimData(3, [0, 0, 0, 1, 0, 0]);
// both paths resolve to child1
const createClip = (entityPath) => {
const curvePath = {
entityPath: entityPath,
component: 'graph',
propertyPath: ['localPosition']
};
const curve = new AnimCurve([curvePath], 0, 0, INTERPOLATION_LINEAR);
const track = new AnimTrack(entityPath.join('/'), 1, [keys], [translations], [curve]);
return new AnimClip(track, 0.0, 1.0, true, true);
};

const animEvaluator = new AnimEvaluator(new DefaultAnimBinder(parent));
animEvaluator.addClip(createClip(['child1']));
animEvaluator.addClip(createClip(['parent', 'child1']));
animEvaluator.update(0.5);
expect(child1.getPosition().x).to.equal(0.5);

animEvaluator.removeClip(0);
animEvaluator.update(0.25);
expect(child1.getPosition().x).to.equal(0.75);

animEvaluator.removeClip(0);
child1.setLocalPosition(5, 0, 0);
animEvaluator.update(0.25);
expect(child1.localPosition.x).to.equal(5);
});

it('AnimEvaluator: a clip added again after removeClips updates the world transform', function () {
const parent = new GraphNode('parent');
const child1 = new GraphNode('child1');
app.root.addChild(parent);
parent.addChild(child1);

const keys = new AnimData(1, [0, 1]);
const translations = new AnimData(3, [0, 0, 0, 1, 0, 0]);
const curvePath = {
entityPath: ['child1'],
component: 'graph',
propertyPath: ['localPosition']
};
const curve = new AnimCurve([curvePath], 0, 0, INTERPOLATION_LINEAR);
const track = new AnimTrack('test track', 1, [keys], [translations], [curve]);

const animEvaluator = new AnimEvaluator(new DefaultAnimBinder(parent));
animEvaluator.addClip(new AnimClip(track, 0.0, 1.0, true, true));
animEvaluator.update(0.5);
expect(child1.getPosition().x).to.equal(0.5);

animEvaluator.removeClips();
animEvaluator.addClip(new AnimClip(track, 0.0, 1.0, true, true));
animEvaluator.update(0.25);
expect(child1.getPosition().x).to.equal(0.25);
});
});
Loading