Support Animation Layers for Skinned Meshes - #834
Conversation
Columns only drew Separator across the full width, through a hardcoded dynamic_cast. Generalize it into an opt-in AWidget::fullWidth flag so any widget can opt out of being placed in a single cell.
Playback state moves from a single set of scalars to up to 4 layers, each owning its animation source model, animation, time, speed, loop mode and weight. Layer-scoped methods take a trailing layer index defaulting to the base layer, which always exists. Pose evaluation now iterates skeleton nodes instead of animation tracks and looks each layer's track up by node. Only layers holding a track for a node contribute to it, their weights normalized against each other, blending with lerp for position and scale and slerp for rotation. Nodes no layer animates keep their bind pose, so a single layer at any weight above zero reproduces the previous output. Going node-major also makes duplicate tracks pointing at the same node harmless, where accumulating weights would have counted them twice. Layers are added and removed at runtime and drawn as collapsible groups in the inspector, so only the layers actually in use are shown. Per-layer settings are serialized as repeated <layer> elements.
Add the layer management functions and the optional trailing layer argument carried by every layer-scoped function.
| function SkinnedMeshRenderer:RemoveLayer(layer) end | ||
|
|
||
| --- Starts/resumes animation playback on a layer | ||
| ---@param layer? integer |
There was a problem hiding this comment.
'?' marks the param as optional in LuaLS. First time it shows up in the repo, can switch to 'integer|nil' if you'd rather ?
There was a problem hiding this comment.
I'm fine to start using type? instead of type|nil, good catch!
We'll update other type|nil to type? as we go
There was a problem hiding this comment.
I think using AI to do that could be a huge time saver
There was a problem hiding this comment.
Yeah AI is great for refactoring code like that :)
Could be done in another PR
| for (auto it = m_widgets.begin(); it != m_widgets.end(); ++it) | ||
| { | ||
| if (dynamic_cast<Visual::Separator*>(it->first)) | ||
| if (it->first->fullWidth || dynamic_cast<Visual::Separator*>(it->first)) |
There was a problem hiding this comment.
Needed for the collapsible layer groups : The inspector pins column 0 at 200px, so a
GroupCollapsable dropped in a cell gets crushed. Only 'Separator' could break out of the table
before, via this hardcoded cast, the flag just generalizes it. Defaults to false, nothing else
changes
Gopmyc
left a comment
There was a problem hiding this comment.
Just a few small changes to clarify things that I didn't find particularly explicit in the code itself
<component>
<type>class OvCore::ECS::Components::CSkinnedMeshRenderer</type>
<data>
<mesh_bounds_scale>1.5</mesh_bounds_scale>
<pose_eval_rate>60</pose_eval_rate>
<layers>
<layer>
<animation_source>?</animation_source>
<playing>true</playing>
<looping>true</looping>
<playback_speed>1</playback_speed>
<time_ticks>0</time_ticks>
<weight>1</weight>
<animation>root|idle</animation>
</layer>
</layers>
</data>
</component>We have to modify Animation.ovscene in the Showroom project - maybe we could add something to convert the legacy format to the new |
I don’t think adding backward compatibility for this case is really worth it, as the implementation and maintenance cost would be quite high for a fairly small migration issue. I’d say the same for a separate conversion tool. Existing Scenes/Prefabs using a SkinnedMeshRenderer can simply be opened in Overload, updated to use the desired animation on Layer 0, and saved again. The Lua API should remain backward compatible since the layer parameter defaults to 0. I’ll also make a follow-up PR soon to fix this in Showroom. |
| { | ||
| animationChoice.choices.clear(); | ||
| animationChoice.choices.emplace(-1, "<None>"); | ||
| return HasCompatibleModel() ? std::string{} : std::string{ "No skinned model assigned" }; |
There was a problem hiding this comment.
Added a Ready at the component level like Behaviour does, and colored the layer messages. Left
the layers quiet when they're fine though, repeating "Ready" on each one gets noisy fast
Tried disabling the field first, but ExecutePlugins runs inside the if (enabled) block in
AWidget::Draw, so a widget that hides itself from its own gatherer never comes back. Probably
worth fixing on its own






Description
CSkinnedMeshRendererplayed a single clip. It now drives up to 4 layers, each owning itsanimation source model, animation, time, speed, loop mode and weight. Layer-scoped methods take a
trailing layer index defaulting to the base layer, which always exists
Pose evaluation iterates skeleton nodes instead of animation tracks, resolving each layer's track
per node. Only layers holding a track for a node contribute to it, their weights normalized against
each other, lerp for position and scale, slerp for rotation. Nodes no layer animates keep their
bind pose
Related Issue(s)
Fixes #833
Review Guidance
the bind pose.
weight reaches zero, where the bone falls back to bind pose. Documented on
SetLayerWeight.accumulating weights would have counted them twice.
AssimpParsercan emit those.<layers>element. Existing scenes loseplaying,looping,playback_speed,time_ticks,animationandanimation_source, andreset to one default layer. No migration path.
Columnsonly drewSeparatorfull width, through a hardcodeddynamic_cast. That is now anopt-in
AWidget::fullWidthflag, required for the collapsible layer groups. Default false, noexisting widget affected.
contributes bind translation, matching the previous single-clip behaviour.
Screenshots/GIFs
N/A
AI Usage Disclosure
Documentation + Refactored code
Checklist