Repository navigation
Conversation
AnimEvaluator.removeClip re-resolved each curve path and decremented the binder's AnimTarget instead of the evaluator's own target record, so removed targets were never released. Removed clips kept writing their last value and every rebind added to the layer counters of the anim component targets, which broke layer blending. removeClip now releases the evaluator's own records and undoes the layer counter of the anim component target value. The target value tracks which layers animate it and, with normalized weights, masks out every layer below the highest overwrite layer that animates it, so the result no longer depends on the order in which layers start or stop animating the target. Fixed playcanvas#6150
|
Thanks, the diagnosis is right, and walking A lower layer's contribution is lost when it starts animating a target again The frame reset still depends on Here's what happens if you extend root.anim.baseLayer.transition('Move');
app.systems.anim.onAnimationUpdate(0.1);
app.systems.anim.onAnimationUpdate(0.1);
expect(root.getLocalPosition().x).to.be.closeTo(12.5, 1e-5);
When the layer that picks the target up again is above or between the existing layers, the counter recovers on the next frame, so only the lowest-layer case breaks. The re-acquire tests in this PR all have that lower layer masked by an overwrite layer, which hides the problem. One way to fix it is to detect the first write of a frame without counting layers. For example, reset when Minor
|
Description
AnimEvaluator.removeClipresolved each curve path through the binder again and decrementedcurveson the binder'sAnimTarget. That object is not the evaluator's own target record, so the count becameNaNand never reached 0. This had two effects:AnimTargetValue.layerCounterwas never decremented. EveryAnimEvaluator.rebind()raised it, whether it came fromlayer.rebind()or fromassignAnimation()while playing. The per-frame reset inupdate()then ran only every other frame, and layers with weight < 1 oscillated (5 -> 7.5 -> 5 in the tests).User-facing change: removed or replaced clips no longer keep writing their last pose, so later script changes to those properties now stick. This applies to
AnimComponentand to the legacyAnimationComponentwith anim tracks.Changes:
AnimEvaluator.removeClipwalks the clip's own output records. When a record's last curve is removed, it deletes the record and, on theAnimTargetValuethataddClipcounted towards, decrementslayerCounterand marks the layer as no longer animating the target. The record keeps a reference to that value becauseAnimComponent.rebind()replaces its targets map.update()usescounter >= layerCounter. A layer that stops animating a target lowerslayerCounterwhile the counter for that frame can already be ahead.AnimTargetValuenow records which layers animate it (oneInt8Arrayper target value, allocated in the constructor).addClipandremoveClipupdate it through the newsetLayerBound(), which recomputes the masks. WithnormalizeWeights, a layer is masked out while any overwrite layer above it animates the target; otherwise each layer's mask is just whether it animates the target, as before.setMaskis unchanged.Once removal releases targets, layers start and stop animating a target on every state change, so the mask has to come from the current set of layers. Main's
setMaskonly cleared lower layers when an overwrite layer bound, so the result depended on bind order. With this change it does not: a lower layer that starts animating a target again stays masked while an overwrite layer above it still animates it, and it takes over once that layer stops.One behaviour change: with normalized weights, an overwrite layer with weight 0 that animates a target masks the layers below it in every bind order, so nothing writes the target. On main this happened only when the overwrite layer bound the target after the lower layer.
Binder targets stay cached, as before. Releasing them through
DefaultAnimBinder.unresolveis out of scope on purpose: that method has never run and has its own bugs. It can be a follow-up.Not changed: a paused layer that still holds clips does not advance its counter, so other layers blending the same target are still affected.
AnimComponent.loadStateGraphreplaces the layers without removing the old evaluators' clips (pre-existing, possible follow-up).Tests (
AnimEvaluator,AnimComponentandAnimationComponent):layer.play()without a graph transition, and after legacyAnimationComponent.play()both without blending and through the blend path that removes all but the last clipassignAnimation()and rebindsOn main, 12 of the 15 new tests fail. The other 3 guard behaviour that already worked: world transforms keep updating after removals, and the overwrite mask after a lower layer changes state.
npm testshows 4550 passing and 0 failing (4535 on main), andnpm run lint,npm run build:typesandnpm run test:typespass.Fixes #6150
Checklist