VisuTwin Canvas
C++ 3D Engine — Metal Backend
Loading...
Searching...
No Matches
animationComponent.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2025-2026 Arnis Lektauers
4
6#include "framework/entity.h"
7#include "scene/graphNode.h"
8#include "spdlog/spdlog.h"
9
10namespace visutwin::canvas
11{
17
19 {
20 const auto it = std::find(_instances.begin(), _instances.end(), this);
21 if (it != _instances.end()) {
22 _instances.erase(it);
23 }
24 }
25
26 void AnimationComponent::setAnimations(const std::unordered_map<std::string, AnimationResource>& value)
27 {
28 _animations = value;
30 }
31
32 void AnimationComponent::addAnimation(const std::string& name, const std::shared_ptr<Animation>& animation)
33 {
34 _animations[name] = animation;
36 }
37
38 void AnimationComponent::addAnimation(const std::string& name, const std::shared_ptr<AnimTrack>& animationTrack)
39 {
40 _animations[name] = animationTrack;
42 }
43
45 {
46 if (_skeleton) {
47 _skeleton->setCurrentTime(currentTime);
48 }
49
50 if (_animEvaluator) {
51 for (auto& clip : _animEvaluator->clips()) {
52 if (clip) {
53 clip->setTime(currentTime);
54 }
55 }
56 }
57 }
58
60 {
61 if (_skeleton) {
62 return _skeleton->currentTime();
63 }
64
65 if (_animEvaluator) {
66 const auto& clips = _animEvaluator->clips();
67 if (!clips.empty() && clips.back()) {
68 return clips.back()->time();
69 }
70 }
71
72 return 0.0f;
73 }
74
76 {
77 if (_currAnim.empty()) {
78 spdlog::warn("No animation is playing to get a duration. Returning 0.");
79 return 0.0f;
80 }
81
82 if (const auto animation = getCurrentAnimation()) {
83 return animation->duration();
84 }
85
86 if (const auto track = getCurrentAnimTrack()) {
87 return track->duration();
88 }
89
90 return 0.0f;
91 }
92
93 void AnimationComponent::setLoop(const bool value)
94 {
95 _loop = value;
96
97 if (_skeleton) {
98 _skeleton->setLooping(value);
99 }
100
101 if (_animEvaluator) {
102 for (auto& clip : _animEvaluator->clips()) {
103 if (clip) {
104 clip->setLoop(value);
105 }
106 }
107 }
108 }
109
111 {
112 if (model == _model) {
113 return;
114 }
115
116 resetAnimationController();
117 _model = model;
118
119 if (!_currAnim.empty() && _animations.contains(_currAnim)) {
120 play(_currAnim);
121 }
122 }
123
124 std::shared_ptr<Animation> AnimationComponent::getAnimation(const std::string& name) const
125 {
126 const auto it = _animations.find(name);
127 if (it == _animations.end()) {
128 return nullptr;
129 }
130
131 if (const auto* animation = std::get_if<std::shared_ptr<Animation>>(&it->second)) {
132 return *animation;
133 }
134
135 return nullptr;
136 }
137
138 std::shared_ptr<AnimTrack> AnimationComponent::getAnimTrack(const std::string& name) const
139 {
140 const auto it = _animations.find(name);
141 if (it == _animations.end()) {
142 return nullptr;
143 }
144
145 if (const auto* animationTrack = std::get_if<std::shared_ptr<AnimTrack>>(&it->second)) {
146 return *animationTrack;
147 }
148
149 return nullptr;
150 }
151
152 std::shared_ptr<Animation> AnimationComponent::getCurrentAnimation() const
153 {
154 return getAnimation(_currAnim);
155 }
156
157 std::shared_ptr<AnimTrack> AnimationComponent::getCurrentAnimTrack() const
158 {
159 return getAnimTrack(_currAnim);
160 }
161
162 void AnimationComponent::play(const std::string& name, const float blendTime)
163 {
164 if (!enabled() || !_entity || !_entity->enabled()) {
165 return;
166 }
167
168 if (!_animations.contains(name)) {
169 spdlog::error("Trying to play animation '{}' which doesn't exist", name);
170 return;
171 }
172
173 _prevAnim = _currAnim;
174 _currAnim = name;
175
176 if (_model) {
177 if (!_skeleton && !_animEvaluator) {
178 createAnimationController();
179 }
180
181 _blending = blendTime > 0.0f && !_prevAnim.empty();
182 if (_blending) {
183 _blend = 0.0f;
184 _blendSpeed = 1.0f / blendTime;
185 }
186
187 if (_skeleton) {
188 if (_blending) {
189 if (const auto prevAnim = getAnimation(_prevAnim)) {
190 _fromSkel->setAnimation(prevAnim.get());
191 _fromSkel->addTime(_skeleton->currentTime());
192 }
193 if (const auto currAnim = getAnimation(_currAnim)) {
194 _toSkel->setAnimation(currAnim.get());
195 }
196 } else if (const auto currAnim = getAnimation(_currAnim)) {
197 _skeleton->setAnimation(currAnim.get());
198 }
199 }
200
201 if (_animEvaluator) {
202 auto& clips = _animEvaluator->clips();
203 if (_blending) {
204 while (clips.size() > 1) {
205 _animEvaluator->removeClip(0);
206 }
207 } else {
208 _animEvaluator->removeClips();
209 }
210
211 if (const auto track = getAnimTrack(_currAnim)) {
212 auto clip = std::make_shared<AnimClip>(track, 0.0f, 1.0f, true, _loop);
213 clip->setName(_currAnim);
214 clip->setBlendWeight(_blending ? 0.0f : 1.0f);
215 clip->reset();
216 _animEvaluator->addClip(clip);
217 }
218 }
219 }
220
221 _playing = true;
222 }
223
225 {
226 if (_entity && !_model) {
227 // DEVIATION: ModelComponent is not yet fully implemented,
228 // so the animation component binds to the entity graph root.
230 }
231
232 if (_currAnim.empty() && _activate && enabled() && _entity && _entity->enabled()) {
233 if (!_animations.empty()) {
234 play(_animations.begin()->first);
235 }
236 }
237 }
238
239 void AnimationComponent::resetAnimationController()
240 {
241 _skeleton.reset();
242 _fromSkel.reset();
243 _toSkel.reset();
244
245 _animEvaluator.reset();
246 }
247
248 void AnimationComponent::createAnimationController()
249 {
250 bool hasJson = false;
251 bool hasGlb = false;
252
253 for (const auto& [_, animation] : _animations) {
254 if (std::holds_alternative<std::shared_ptr<AnimTrack>>(animation)) {
255 hasGlb = true;
256 } else {
257 hasJson = true;
258 }
259 }
260
261 if (hasJson) {
262 _fromSkel = std::make_unique<Skeleton>(_model);
263 _toSkel = std::make_unique<Skeleton>(_model);
264 _skeleton = std::make_unique<Skeleton>(_model);
265 _skeleton->setLooping(_loop);
266 _skeleton->setGraph(_model);
267 } else if (hasGlb) {
268 _animEvaluator = std::make_unique<AnimEvaluator>(std::make_unique<DefaultAnimBinder>(_entity));
269 }
270 }
271
272 void AnimationComponent::stopCurrentAnimation()
273 {
274 _currAnim.clear();
275
276 _playing = false;
277 if (_skeleton) {
278 _skeleton->setCurrentTime(0.0f);
279 _skeleton->setAnimation(nullptr);
280 }
281 if (_animEvaluator) {
282 for (auto& clip : _animEvaluator->clips()) {
283 if (clip) {
284 clip->stop();
285 }
286 }
287 _animEvaluator->update(0.0f);
288 _animEvaluator->removeClips();
289 }
290 }
291
293 {
295
296 if (_activate && _currAnim.empty() && !_animations.empty()) {
297 play(_animations.begin()->first);
298 }
299 }
300
302 {
303 _skeleton.reset();
304 _fromSkel.reset();
305 _toSkel.reset();
306
307 _animEvaluator.reset();
308 }
309
310 void AnimationComponent::update(const float dt)
311 {
312 float blendAlpha = _blend;
313
314 if (_blending) {
315 _blend += dt * _blendSpeed;
316 if (_blend >= 1.0f) {
317 _blend = 1.0f;
318 }
319
320 blendAlpha = _blend;
321 if (_hasBlendCurve) {
322 // DEVIATION: upstream AnimationComponent blends linearly; this implementation allows optional curve remap.
323 blendAlpha = std::clamp(_blendCurve.value(_blend), 0.0f, 1.0f);
324 }
325 }
326
327 if (_playing) {
328 if (_skeleton && _model) {
329 if (_blending) {
330 _skeleton->blend(_fromSkel.get(), _toSkel.get(), blendAlpha);
331 } else {
332 const float delta = dt * _speed;
333 _skeleton->addTime(delta);
334 if (_speed > 0.0f && (_skeleton->currentTime() == duration()) && !_loop) {
335 _playing = false;
336 } else if (_speed < 0.0f && _skeleton->currentTime() == 0.0f && !_loop) {
337 _playing = false;
338 }
339 }
340
341 if (_blending && (_blend == 1.0f)) {
342 _skeleton->setAnimation(_toSkel->animation());
343 }
344
345 _skeleton->updateGraph();
346 }
347 }
348
349 if (_animEvaluator) {
350 for (auto& clip : _animEvaluator->clips()) {
351 if (!clip) {
352 continue;
353 }
354
355 clip->setSpeed(_speed);
356 if (!_playing) {
357 clip->pause();
358 } else {
359 clip->resume();
360 }
361 }
362
363 if (_blending && _animEvaluator->clips().size() > 1 && _animEvaluator->clips()[1]) {
364 _animEvaluator->clips()[1]->setBlendWeight(blendAlpha);
365 }
366
367 _animEvaluator->update(dt);
368 }
369
370 if (_blending && _blend == 1.0f) {
371 _blending = false;
372 }
373 }
374
376 {
377 _blendCurve = curve;
378 _hasBlendCurve = true;
379 }
380
382 {
383 _hasBlendCurve = false;
384 }
385}
void setAnimations(const std::unordered_map< std::string, AnimationResource > &value)
std::shared_ptr< AnimTrack > getAnimTrack(const std::string &name) const
void play(const std::string &name, float blendTime=0.0f)
void addAnimation(const std::string &name, const std::shared_ptr< Animation > &animation)
std::shared_ptr< Animation > getAnimation(const std::string &name) const
AnimationComponent(IComponentSystem *system, Entity *entity)
Entity * entity() const
Definition component.cpp:16
virtual bool enabled() const
Definition component.h:49
Component(IComponentSystem *system, Entity *entity)
Definition component.cpp:12
virtual void onEnable()
Definition component.h:56
IComponentSystem * system() const
Definition component.h:47
ECS entity — a GraphNode that hosts components defining its behavior.
Definition entity.h:32
Hierarchical scene graph node with local/world transforms and parent-child relationships.
Definition graphNode.h:28
std::vector< GraphNode * > find(const std::function< bool(GraphNode *)> &predicate)
Find all descendants (and self) matching a predicate.