I am handling an animation with N morph target tracks.
In this case, reducing the number of calls to _syncActiveTargets will have a significant impact on performance.
Before #16014, it was possible to reduce the number of calls to _syncActiveTargets to once per animation processing using areUpdatesFrozen.
The code will look something like this:
// This prevents _syncActiveTargets from being called on each iteration of the loop.
morphTargetManager.areUpdatesFrozen = true;
for (const [morphIndex, morphWeight] in evaluatedAnimations) {
applyAnimation(morphIndex, morphWeight);
}
morphTargetManager.areUpdatesFrozen = false;
However, after #16014, optimizing the number of calls to _syncActiveTargets with areUpdatesFrozen is no longer possible, because we set _mustSynchronize to true at the point where areUpdatesFrozen becomes false, which causes us to run the slow task of generating morph target texures.
To recap the problem
A common use case for the Morph Target Manager is to mix multiple Morphs in the same time.
In this case, you would use a loop to update one morph weight on each iteration.
for (const [morphIndex, morphWeight] in evaluatedAnimations) {
applyAnimation(morphIndex, morphWeight);
}
On each iteration, the MorphTarget then fires the onInfluenceChanged observer, which causes the _syncActiveTargets function to run.
If you have 10 morphs on your animation track, _syncActiveTargets will be executed 10 times.
Eventually, _syncActiveTargets will run N times, but the only thing that really matters is the result of the last run; the other N - 1 runs will be for nothing.
To fix this, it looks like we need to apply a dirtyFlag to _syncActiveTargets to treat it with lazy evaluation. Before #16014, I optimized animation evaluation by declaratively using areUpdatesFrozen, but that doesnāt seem like a good idea either.
Thanks, this seems to solve the problem in my case, but for good performance in more cases where we update morph target influences, we need to add more complex conditions to the flag so that _syncActiveTargets can be lazy evaluated at render time instead of just using the current _mustSynchronize dirty flag.
It looks to me that setting areUpdatesFrozen = true / updating target influences / setting areUpdatesFrozen = false is optimal (when the PR is merged)? Because of the call to areUpdatesFrozen = true, updating influences wonāt call _syncActiveTargets. _syncActiveTargets will be called (once) only when setting areUpdatesFrozen = false.
Iāve pictured a scenario where multiple animation systems each try to use that method for optimal performance, and it seems to work fine in this case.
In code, it looks like this
// The code for each system is assumed to be library-embedded and
// not modifiable at the end-user(developer) level.
// animation system1 from some babylon js related npm lib or something...
function animate1(...) {
// some animation evaluations...
morphTargetManager.areUpdatesFrozen = true;
for (const [morphIndex, morphWeight] in evaluatedAnimations) {
applyAnimation(morphIndex, morphWeight);
}
morphTargetManager.areUpdatesFrozen = false;
}
// animation system2 from some babylon js related npm lib or something...
function animate2(...) {
// some animation evaluations...
morphTargetManager.areUpdatesFrozen = true;
for (const [morphIndex, morphWeight] in evaluatedAnimations) {
applyAnimation(morphIndex, morphWeight);
}
morphTargetManager.areUpdatesFrozen = false;
}
// Users can use animation systems from multiple libraries simultaneously.
// However, this will cause `areUpdatesFrozen = false` to be executed once
// in each animate function, resulting in `MorphTargetManager._syncActiveTargets`
// being executed twice.
animate1(mesh);
animate2(mesh);
// If users set areUpdatesFrozen to true before using the animation system
// and set it to false after all animation is evaluated,
// `MorphTargetManager._syncActiveTargets` will only run once.
morphTargetManager.areUpdatesFrozen = true;
animate1(mesh);
animate2(mesh);
morphTargetManager.areUpdatesFrozen = false;
Found an issue: MorphTarget animation does not work properly when numMaxInfluencers is not specified, because the MorphTargetManager.synchronize method is not called when areUpdatesFrozen = false.
Here is a PG that reproduces the issue.
Please check the comments
What are you doing when areUpdatesFrozen == true that would require synchronize() to be called when areUpdatesFrozen is reset to false? It seems we miss to set this._mustSynchronize = true; somewhere, but itās hard to debug with your PG as everything happens in the mmd library.
Donāt apologize, you did a lot to fix morph implementation, and your latest PG is exactly what I needed to fix the bug!
Hereās the PR:
Note that you must use a setTimeout in your repro because the bug wonāt show up if Material.isReadyForSubMesh is called after areUpdatesFrozen is set to true. By introducing the delay, you make sure that this method is called one time before updates are frozen. I think the bug doesnāt show up with a PBR material because it takes more time for this material to compile, but if you set a timeout like 16ms, for eg, you will see the same problem.