23 if (seconds < 0.f)
throw Exception(
"AnimClip.setDuration: duration must be >= 0");
24 for (
const auto& event : events_)
25 if (event.t > seconds)
26 throw Exception(
"AnimClip.setDuration: event '%s' lies past duration", event.name.c_str());
27 for (
const auto& marker : syncMarkers_)
28 if (marker.t > seconds)
29 throw Exception(
"AnimClip.setDuration: sync marker '%s' lies past duration", marker.name.c_str());
34 if (hz <= 0.f)
throw Exception(
"AnimClip.setSampleRate: hz must be > 0");
38void AnimClip::ensureBone(
int boneIndex) {
39 if (boneIndex < 0)
throw Exception(
"AnimClip: invalid bone index %d", boneIndex);
40 if (boneIndex >=
static_cast<int>(tracks_.size())) {
41 tracks_.resize(
static_cast<size_t>(boneIndex) + 1);
46 if (
time < 0.f)
throw Exception(
"AnimClip.addPositionKey: time must be >= 0");
47 ensureBone(boneIndex);
48 auto& keys = tracks_[
static_cast<size_t>(boneIndex)].
positions;
49 keys.push_back({
time,
x,
y,
z});
50 if (keys.size() > 1 && keys[keys.size() - 2].t >=
time)
51 std::sort(keys.begin(), keys.end(), [](
const Vec3Key&
a,
const Vec3Key&
b) {
return a.t <
b.t; });
52 if (
time > duration_) duration_ =
time;
56 if (
time < 0.f)
throw Exception(
"AnimClip.addRotationKey: time must be >= 0");
57 ensureBone(boneIndex);
69 auto& keys = tracks_[
static_cast<size_t>(boneIndex)].rotations;
71 if (keys.size() > 1 && keys[keys.size() - 2].t >=
time)
72 std::sort(keys.begin(), keys.end(), [](
const QuatKey&
a,
const QuatKey&
b) {
return a.t <
b.t; });
73 if (
time > duration_) duration_ =
time;
77 if (
time < 0.f)
throw Exception(
"AnimClip.addScaleKey: time must be >= 0");
78 ensureBone(boneIndex);
79 auto& keys = tracks_[
static_cast<size_t>(boneIndex)].
scales;
80 keys.push_back({
time,
x,
y,
z});
81 if (keys.size() > 1 && keys[keys.size() - 2].t >=
time)
82 std::sort(keys.begin(), keys.end(), [](
const Vec3Key&
a,
const Vec3Key&
b) {
return a.t <
b.t; });
83 if (
time > duration_) duration_ =
time;
87 if (
time < 0.f)
throw Exception(
"AnimClip.setPositionKey: time must be >= 0");
88 if (boneIndex < 0 || boneIndex >=
getTrackCount())
return false;
89 auto& keys = tracks_[
static_cast<size_t>(boneIndex)].
positions;
90 if (keyIndex < 0 || keyIndex >=
static_cast<int>(keys.size()))
return false;
91 keys[
static_cast<size_t>(keyIndex)] = {
time,
x,
y,
z};
92 std::stable_sort(keys.begin(), keys.end(), [](
const Vec3Key&
a,
const Vec3Key&
b) { return a.t < b.t; });
93 if (
time > duration_) duration_ =
time;
98 if (
time < 0.f)
throw Exception(
"AnimClip.setRotationKey: time must be >= 0");
99 if (boneIndex < 0 || boneIndex >=
getTrackCount())
return false;
100 auto& keys = tracks_[
static_cast<size_t>(boneIndex)].rotations;
101 if (keyIndex < 0 || keyIndex >=
static_cast<int>(keys.size()))
return false;
108 keys[
static_cast<size_t>(keyIndex)] = {
time, normalized.
qx, normalized.
qy, normalized.
qz, normalized.
qw};
109 std::stable_sort(keys.begin(), keys.end(), [](
const QuatKey&
a,
const QuatKey&
b) { return a.t < b.t; });
110 if (
time > duration_) duration_ =
time;
115 if (
time < 0.f)
throw Exception(
"AnimClip.setScaleKey: time must be >= 0");
116 if (boneIndex < 0 || boneIndex >=
getTrackCount())
return false;
117 auto& keys = tracks_[
static_cast<size_t>(boneIndex)].
scales;
118 if (keyIndex < 0 || keyIndex >=
static_cast<int>(keys.size()))
return false;
119 keys[
static_cast<size_t>(keyIndex)] = {
time,
x,
y,
z};
120 std::stable_sort(keys.begin(), keys.end(), [](
const Vec3Key&
a,
const Vec3Key&
b) { return a.t < b.t; });
121 if (
time > duration_) duration_ =
time;
126 if (boneIndex < 0 || boneIndex >=
getTrackCount())
return false;
127 auto& keys = tracks_[
static_cast<size_t>(boneIndex)].
positions;
128 if (keyIndex < 0 || keyIndex >=
static_cast<int>(keys.size()))
return false;
129 keys.erase(keys.begin() + keyIndex);
134 if (boneIndex < 0 || boneIndex >=
getTrackCount())
return false;
135 auto& keys = tracks_[
static_cast<size_t>(boneIndex)].rotations;
136 if (keyIndex < 0 || keyIndex >=
static_cast<int>(keys.size()))
return false;
137 keys.erase(keys.begin() + keyIndex);
142 if (boneIndex < 0 || boneIndex >=
getTrackCount())
return false;
143 auto& keys = tracks_[
static_cast<size_t>(boneIndex)].
scales;
144 if (keyIndex < 0 || keyIndex >=
static_cast<int>(keys.size()))
return false;
145 keys.erase(keys.begin() + keyIndex);
150 if (boneIndex < 0 || boneIndex >=
getTrackCount())
return false;
151 tracks_[
static_cast<size_t>(boneIndex)] = {};
156 if (
time < 0.f)
throw Exception(
"AnimClip.addEvent: time must be >= 0");
157 if (duration_ > 0.f &&
time > duration_)
throw Exception(
"AnimClip.addEvent: time lies past clip duration");
158 if (
name.empty())
throw Exception(
"AnimClip.addEvent: name is empty");
160 std::stable_sort(events_.begin(), events_.end(),
161 [](
const EventMarker&
a,
const EventMarker&
b) { return a.t < b.t; });
166 if (
time < 0.f)
throw Exception(
"AnimClip.setEvent: time must be >= 0");
167 if (duration_ > 0.f &&
time > duration_)
throw Exception(
"AnimClip.setEvent: time lies past clip duration");
168 if (
name.empty())
throw Exception(
"AnimClip.setEvent: name is empty");
170 std::stable_sort(events_.begin(), events_.end(),
171 [](
const EventMarker&
a,
const EventMarker&
b) { return a.t < b.t; });
177 events_.erase(events_.begin() +
index);
183 return events_[
static_cast<size_t>(
index)].
t;
188 return events_[
static_cast<size_t>(
index)].
name;
197 return !
name.empty() &&
198 std::any_of(events_.begin(), events_.end(), [
name](
const EventMarker& event) { return event.name == name; });
203 const std::string&
name = requiredNames[
index];
207 "requiredNames[" + std::to_string(
index) +
"]", {},
"animation"));
217 if (
time < 0.f)
throw Exception(
"AnimClip.addSyncMarker: time must be >= 0");
218 if (duration_ > 0.f &&
time > duration_)
throw Exception(
"AnimClip.addSyncMarker: time lies past clip duration");
219 if (
name.empty())
throw Exception(
"AnimClip.addSyncMarker: name is empty");
220 syncMarkers_.push_back({
time,
name});
221 std::stable_sort(syncMarkers_.begin(), syncMarkers_.end(),
222 [](
const SyncMarker&
a,
const SyncMarker&
b) { return a.t < b.t; });
227 if (
time < 0.f)
throw Exception(
"AnimClip.setSyncMarker: time must be >= 0");
228 if (duration_ > 0.f &&
time > duration_)
throw Exception(
"AnimClip.setSyncMarker: time lies past clip duration");
229 if (
name.empty())
throw Exception(
"AnimClip.setSyncMarker: name is empty");
231 std::stable_sort(syncMarkers_.begin(), syncMarkers_.end(),
232 [](
const SyncMarker&
a,
const SyncMarker&
b) { return a.t < b.t; });
238 syncMarkers_.erase(syncMarkers_.begin() +
index);
244 return syncMarkers_[
static_cast<size_t>(
index)].
t;
249 return syncMarkers_[
static_cast<size_t>(
index)].
name;
253 if (!
target || duration_ <= 1e-8f || target->duration_ <= 1e-8f)
return false;
254 std::vector<const SyncMarker*> sourceCommon;
255 std::vector<const SyncMarker*> targetCommon;
256 for (
const SyncMarker& marker : syncMarkers_) {
257 if (std::any_of(
target->syncMarkers_.begin(),
target->syncMarkers_.end(),
258 [&](
const SyncMarker& other) { return other.name == marker.name; }))
259 sourceCommon.push_back(&marker);
261 for (
const SyncMarker& marker :
target->syncMarkers_) {
262 if (std::any_of(syncMarkers_.begin(), syncMarkers_.end(),
263 [&](
const SyncMarker& other) { return other.name == marker.name; }))
264 targetCommon.push_back(&marker);
266 if (sourceCommon.size() < 2 || targetCommon.size() < 2)
return false;
267 const size_t sourcePairCount = loop_ ? sourceCommon.size() : sourceCommon.size() - 1;
268 const size_t targetPairCount =
target->loop_ ? targetCommon.size() : targetCommon.size() - 1;
269 for (
size_t sourceIndex = 0; sourceIndex < sourcePairCount; ++sourceIndex) {
270 const std::string&
previous = sourceCommon[sourceIndex]->name;
271 const std::string& next = sourceCommon[(sourceIndex + 1) % sourceCommon.size()]->name;
272 for (
size_t targetIndex = 0; targetIndex < targetPairCount; ++targetIndex)
274 targetCommon[(targetIndex + 1) % targetCommon.size()]->name == next)
281 if (!
target || duration_ <= 1e-8f || target->duration_ <= 1e-8f)
return 0.f;
285 std::vector<const SyncMarker*> sourceCommon;
286 std::vector<const SyncMarker*> targetCommon;
287 for (
const SyncMarker& marker : syncMarkers_) {
288 if (std::any_of(
target->syncMarkers_.begin(),
target->syncMarkers_.end(),
289 [&](
const SyncMarker& other) { return other.name == marker.name; }))
290 sourceCommon.push_back(&marker);
292 for (
const SyncMarker& marker :
target->syncMarkers_) {
293 if (std::any_of(syncMarkers_.begin(), syncMarkers_.end(),
294 [&](
const SyncMarker& other) { return other.name == marker.name; }))
295 targetCommon.push_back(&marker);
299 if (!loop_ && (sourceTime < sourceCommon.front()->t || sourceTime > sourceCommon.back()->t))
300 return normalizedFallback;
301 size_t sourcePrevious = sourceCommon.size() - 1;
302 for (
size_t i = 0; i < sourceCommon.size(); ++i) {
303 if (sourceCommon[i]->
t <= sourceTime)
308 const size_t sourceNext = (sourcePrevious + 1) % sourceCommon.size();
309 const float sourceStart = sourceCommon[sourcePrevious]->t;
310 float sourceEnd = sourceCommon[sourceNext]->t;
311 float adjustedSourceTime = sourceTime;
312 if (sourceNext == 0) {
313 sourceEnd += duration_;
314 if (adjustedSourceTime < sourceStart) adjustedSourceTime += duration_;
316 const float span = sourceEnd - sourceStart;
317 if (span <= 1e-8f)
return normalizedFallback;
318 const float alpha =
clampf((adjustedSourceTime - sourceStart) / span, 0.f, 1.f);
320 const std::string& previousName = sourceCommon[sourcePrevious]->name;
321 const std::string& nextName = sourceCommon[sourceNext]->name;
323 float bestDistance =
target->duration_ + 1.f;
324 const float sourcePhase = sourceStart / duration_;
325 const size_t targetPairCount =
target->loop_ ? targetCommon.size() : targetCommon.size() - 1;
326 for (
size_t i = 0; i < targetPairCount; ++i) {
327 if (targetCommon[i]->
name != previousName || targetCommon[(i + 1) % targetCommon.size()]->name != nextName)
329 const float targetPhase = targetCommon[i]->t /
target->duration_;
330 const float distance = std::abs(targetPhase - sourcePhase);
332 if (circularDistance < bestDistance) {
333 bestDistance = circularDistance;
334 bestTarget =
static_cast<int>(i);
337 if (bestTarget < 0)
return normalizedFallback;
338 const size_t targetPrevious =
static_cast<size_t>(bestTarget);
339 const size_t targetNext = (targetPrevious + 1) % targetCommon.size();
340 const float targetStart = targetCommon[targetPrevious]->t;
341 float targetEnd = targetCommon[targetNext]->t;
342 if (targetNext == 0) targetEnd +=
target->duration_;
343 float mapped = targetStart + alpha * (targetEnd - targetStart);
344 if (
target->getLoop()) mapped = std::fmod(mapped,
target->duration_);
345 return target->wrapTime(mapped);
349 if (events_.empty() || duration_ <= 0.f || currentTime == previousTime)
return;
352 auto appendRange = [&](
float lo,
float hi,
bool includeLo) {
353 for (
const EventMarker& event : events_) {
354 if ((includeLo ? event.t >= lo :
event.t > lo) && event.t <= hi) out.push_back(event.name);
357 if (
loop && (currentTime - previousTime >= duration_ ||
to <
from)) {
358 appendRange(
from, duration_,
false);
359 appendRange(0.f,
to,
true);
361 appendRange(
from,
to,
false);
366 if (boneIndex < 0 || boneIndex >=
static_cast<int>(tracks_.size()))
return 0;
367 return static_cast<int>(tracks_[
static_cast<size_t>(boneIndex)].
positions.size());
371 if (boneIndex < 0 || boneIndex >=
static_cast<int>(tracks_.size()))
return 0;
372 return static_cast<int>(tracks_[
static_cast<size_t>(boneIndex)].rotations.size());
376 if (boneIndex < 0 || boneIndex >=
static_cast<int>(tracks_.size()))
return 0;
377 return static_cast<int>(tracks_[
static_cast<size_t>(boneIndex)].
scales.size());
381void requireKey(
int boneIndex,
int keyIndex,
int count,
const char* what) {
382 if (boneIndex < 0 || keyIndex < 0 || keyIndex >=
count) {
383 throw Exception(
"AnimClip: invalid %s key bone=%d index=%d", what, boneIndex, keyIndex);
390 return tracks_[
static_cast<size_t>(boneIndex)].
positions[
static_cast<size_t>(keyIndex)].t;
394 return tracks_[
static_cast<size_t>(boneIndex)].
positions[
static_cast<size_t>(keyIndex)].x;
398 return tracks_[
static_cast<size_t>(boneIndex)].
positions[
static_cast<size_t>(keyIndex)].y;
402 return tracks_[
static_cast<size_t>(boneIndex)].
positions[
static_cast<size_t>(keyIndex)].z;
407 return tracks_[
static_cast<size_t>(boneIndex)].rotations[
static_cast<size_t>(keyIndex)].t;
411 return tracks_[
static_cast<size_t>(boneIndex)].rotations[
static_cast<size_t>(keyIndex)].x;
415 return tracks_[
static_cast<size_t>(boneIndex)].rotations[
static_cast<size_t>(keyIndex)].y;
419 return tracks_[
static_cast<size_t>(boneIndex)].rotations[
static_cast<size_t>(keyIndex)].z;
423 return tracks_[
static_cast<size_t>(boneIndex)].rotations[
static_cast<size_t>(keyIndex)].w;
428 return tracks_[
static_cast<size_t>(boneIndex)].
scales[
static_cast<size_t>(keyIndex)].t;
432 return tracks_[
static_cast<size_t>(boneIndex)].
scales[
static_cast<size_t>(keyIndex)].x;
436 return tracks_[
static_cast<size_t>(boneIndex)].
scales[
static_cast<size_t>(keyIndex)].y;
440 return tracks_[
static_cast<size_t>(boneIndex)].
scales[
static_cast<size_t>(keyIndex)].z;
444 ensureBone(boneIndex);
445 auto& keys = tracks_[
static_cast<size_t>(boneIndex)].
positions;
448 keys.push_back({0.f, 0.f, 0.f, 0.f});
449 keys.push_back({duration_, speedX * duration_, 0.f, speedZ * duration_});
455 if (keys.front().t > 0.f) {
456 auto start = keys.front();
458 keys.insert(keys.begin(),
start);
460 if (keys.back().t < duration_) {
461 auto end = keys.back();
465 for (
auto& k : keys) {
472 if (duration_ <= 1e-8f)
return 0.f;
482 auto result = std::make_unique<AnimClip>(name_);
483 result->duration_ = duration_;
484 result->loop_ = loop_;
485 result->sampleRate_ = sampleRate_;
486 result->tracks_ = tracks_;
487 result->events_ = events_;
488 result->syncMarkers_ = syncMarkers_;
492void AnimClip::sampleVec3(
const std::vector<Vec3Key>& keys,
float time,
float&
x,
float&
y,
float&
z,
bool&
ok) {
494 if (keys.empty())
return;
496 if (
time <= keys.front().t || keys.size() == 1) {
502 if (
time >= keys.back().t) {
508 const auto upper = std::upper_bound(keys.data(), keys.data() + keys.size(),
time,
509 [](
float value,
const Vec3Key&
key) { return value < key.t; });
510 const auto&
b = *upper;
511 const auto&
a = *(upper - 1);
512 const float den =
b.t -
a.t;
513 const float t = den > 1e-8f ? (
time -
a.t) / den : 0.f;
519void AnimClip::sampleQuat(
const std::vector<QuatKey>& keys,
float time,
float&
x,
float&
y,
float&
z,
float&
w,
522 if (keys.empty())
return;
524 if (
time <= keys.front().t || keys.size() == 1) {
531 if (
time >= keys.back().t) {
538 const auto upper = std::upper_bound(keys.data(), keys.data() + keys.size(),
time,
539 [](
float value,
const QuatKey&
key) { return value < key.t; });
540 const auto&
b = *upper;
541 const auto&
a = *(upper - 1);
542 const float den =
b.t -
a.t;
543 const float t = den > 1e-8f ? (
time -
a.t) / den : 0.f;
544 slerpQuat(
a.x,
a.y,
a.z,
a.w,
b.x,
b.y,
b.z,
b.w,
t,
x,
y,
z,
w);
547TransformTRS AnimClip::sampleBone(
int boneIndex,
float time,
const TransformTRS& fallback)
const {
548 TransformTRS out = fallback;
549 if (boneIndex < 0 || boneIndex >=
static_cast<int>(tracks_.size()))
return out;
550 const BoneTrack&
tr = tracks_[
static_cast<size_t>(boneIndex)];
580 if (!out || !skeleton)
throw Exception(
"AnimClip.sampleLod: pose or skeleton is null");
581 if (lodLevel < 0)
throw Exception(
"AnimClip.sampleLod: lodLevel must be >= 0");
585 for (
int i = 0; i < boneCount; ++i) {
592 if (!out)
throw Exception(
"AnimClip.sample: pose is null");
593 const int boneCount = skeleton ? skeleton->
getBoneCount() :
static_cast<int>(tracks_.size());
596 for (
int i = 0; i < boneCount; ++i) {
603 if (positionError < 0.f || rotationErrorDegrees < 0.f || scaleError < 0.f)
604 throw Exception(
"AnimClip.compress: tolerances must be >= 0");
605 const float rotationError = rotationErrorDegrees * 0.01745329251994329577f;
607 for (BoneTrack& track : tracks_) {
608 auto reduceVec3 = [&](std::vector<Vec3Key>& keys,
float tolerance) {
609 if (keys.size() <= 2)
return;
610 std::vector<unsigned char> keep(keys.size(), 0);
611 keep.front() = keep.back() = 1;
612 std::function<void(
size_t,
size_t)>
split = [&](
size_t first,
size_t last) {
613 if (last <=
first + 1)
return;
614 const float span = keys[last].t - keys[
first].t;
616 size_t worstIndex =
first;
617 for (
size_t i =
first + 1; i < last; ++i) {
618 const float alpha = span > 1e-8f ? (keys[i].t - keys[
first].t) / span : 0.f;
619 const float dx = keys[i].x -
lerpf(keys[
first].
x, keys[last].
x, alpha);
620 const float dy = keys[i].y -
lerpf(keys[
first].
y, keys[last].
y, alpha);
621 const float dz = keys[i].z -
lerpf(keys[
first].
z, keys[last].
z, alpha);
628 if (worst > tolerance) {
629 keep[worstIndex] = 1;
631 split(worstIndex, last);
634 split(0, keys.size() - 1);
635 std::vector<Vec3Key> reduced;
636 reduced.reserve(keys.size());
637 for (
size_t i = 0; i < keys.size(); ++i)
638 if (keep[i]) reduced.push_back(keys[i]);
639 removed +=
static_cast<int>(keys.size() - reduced.size());
642 auto reduceQuat = [&](std::vector<QuatKey>& keys) {
643 if (keys.size() <= 2)
return;
644 std::vector<unsigned char> keep(keys.size(), 0);
645 keep.front() = keep.back() = 1;
646 std::function<void(
size_t,
size_t)>
split = [&](
size_t first,
size_t last) {
647 if (last <=
first + 1)
return;
648 const float span = keys[last].t - keys[
first].t;
650 size_t worstIndex =
first;
651 for (
size_t i =
first + 1; i < last; ++i) {
652 const float alpha = span > 1e-8f ? (keys[i].t - keys[
first].t) / span : 0.f;
655 keys[last].
z, keys[last].
w, alpha,
x,
y,
z,
w);
656 const float dot = std::abs(
x * keys[i].
x +
y * keys[i].
y +
z * keys[i].
z +
w * keys[i].
w);
657 const float error = 2.f * std::acos(
clampf(dot, -1.f, 1.f));
663 if (worst > rotationError) {
664 keep[worstIndex] = 1;
666 split(worstIndex, last);
669 split(0, keys.size() - 1);
670 std::vector<QuatKey> reduced;
671 reduced.reserve(keys.size());
672 for (
size_t i = 0; i < keys.size(); ++i)
673 if (keep[i]) reduced.push_back(keys[i]);
674 removed +=
static_cast<int>(keys.size() - reduced.size());
677 reduceVec3(track.positions, positionError);
678 reduceQuat(track.rotations);
679 reduceVec3(track.scales, scaleError);
685 if (sourceBone.empty() || targetBone.empty())
686 throw Exception(
"AnimRetargetProfile.addBoneMapping: bone name must not be empty");
687 for (Mapping& mapping : mappings_) {
688 if (mapping.target == targetBone) {
689 mapping.source = sourceBone;
693 mappings_.push_back({sourceBone, targetBone});
699 sourceRoot_ = sourceBone;
700 targetRoot_ = targetBone;
704 if (horizontal < 0.f || vertical < 0.f)
705 throw Exception(
"AnimRetargetProfile.setRootTranslationScale: scales must be >= 0");
706 rootHorizontalScale_ = horizontal;
707 rootVerticalScale_ = vertical;
712 return unmatchedTargetBones_[
static_cast<size_t>(
index)];
716 if (
distance < 0.f)
throw Exception(
"AnimRetargetProfile.setInteractionContactThreshold: distance must be >= 0");
717 interactionContactThreshold_ =
distance;
721 if (weight < 0.f || weight > 1.f)
722 throw Exception(
"AnimRetargetProfile.setInteractionCorrectionWeight: weight must be in [0,1]");
723 interactionCorrectionWeight_ =
weight;
727 const std::string& tipBone) {
728 if (rootBone.empty() || midBone.empty() || tipBone.empty())
729 throw Exception(
"AnimRetargetProfile.addInteractionIkChain: bone name must not be empty");
730 interactionIkChains_.push_back(
AnimSmrIkChain{rootBone, midBone, tipBone});
738 float x = 0.f,
y = 0.f,
z = 0.f,
w = 1.f;
741RetargetQuat retargetMul(
const RetargetQuat&
a,
const RetargetQuat&
b) {
742 return {
a.w *
b.x +
a.x *
b.w +
a.y *
b.z -
a.z *
b.y,
a.w *
b.y -
a.x *
b.z +
a.y *
b.w +
a.z *
b.x,
743 a.w *
b.z +
a.x *
b.y -
a.y *
b.x +
a.z *
b.w,
a.w *
b.w -
a.x *
b.x -
a.y *
b.y -
a.z *
b.z};
746RetargetQuat retargetInverse(
const RetargetQuat&
q) {
return {-
q.x, -
q.y, -
q.z,
q.w}; }
748RetargetQuat rotationOf(
const TransformTRS& transform) {
749 return {transform.qx, transform.qy, transform.qz, transform.qw};
752std::string normalizedBoneName(
const std::string&
name) {
756 for (
size_t i =
begin; i <
name.size(); ++i) {
757 const unsigned char c =
static_cast<unsigned char>(
name[i]);
758 if (std::isalnum(
c)) out.push_back(
static_cast<char>(std::tolower(
c)));
763int findNormalizedBone(
const AnimSkeleton* skeleton,
const std::string&
name) {
764 const std::string normalized = normalizedBoneName(
name);
766 for (
int bone = 0;
bone < skeleton->getBoneCount(); ++
bone) {
767 if (normalizedBoneName(skeleton->getBoneName(
bone)) != normalized)
continue;
768 if (match >= 0)
return -1;
774float skeletonExtent(
const AnimSkeleton* skeleton) {
776 skeleton->applyBindPose(&bind);
777 bind.computeWorld(skeleton);
778 if (skeleton->getBoneCount() == 0)
return 1.f;
779 const TransformTRS&
root = bind.world(0);
781 for (
int bone = 1;
bone < skeleton->getBoneCount(); ++
bone) {
782 const TransformTRS&
world = bind.world(
bone);
786 extent = std::max(extent, std::sqrt(
dx *
dx +
dy *
dy +
dz *
dz));
788 return std::max(extent, 1e-6f);
791float retargetRootMeasure(
const AnimSkeleton* skeleton,
const AnimPose& bindPose,
int retargetRoot) {
792 if (retargetRoot < 0)
return skeletonExtent(skeleton);
793 int hierarchyRoot = retargetRoot;
794 while (skeleton->getParent(hierarchyRoot) >= 0) hierarchyRoot = skeleton->getParent(hierarchyRoot);
795 const TransformTRS&
root = bindPose.world(hierarchyRoot);
796 const TransformTRS& pelvis = bindPose.world(retargetRoot);
797 const float dx = pelvis.px -
root.px;
798 const float dy = pelvis.py -
root.py;
799 const float dz = pelvis.pz -
root.pz;
813 if (!sourceSkeleton || !targetSkeleton)
throw Exception(
"AnimClip.retargetWithProfile: skeleton is null");
814 if (!profile)
throw Exception(
"AnimClip.retargetWithProfile: profile is null");
816 auto out = std::make_unique<AnimClip>(name_ +
"_retargeted");
817 out->duration_ = duration_;
819 out->sampleRate_ = sampleRate_;
820 out->events_ = events_;
821 out->syncMarkers_ = syncMarkers_;
822 out->tracks_.resize(
static_cast<size_t>(targetSkeleton->
getBoneCount()));
824 std::vector<int> sourceForTarget(
static_cast<size_t>(targetSkeleton->
getBoneCount()), -1);
825 profile->matchedBoneCount_ = 0;
826 profile->unmatchedTargetBones_.clear();
827 for (
int targetBone = 0; targetBone < targetSkeleton->
getBoneCount(); ++targetBone) {
828 const std::string targetName = targetSkeleton->
getBoneName(targetBone);
830 bool explicitlyMapped =
false;
831 for (
const auto& mapping : profile->mappings_) {
832 if (mapping.target == targetName) {
833 explicitlyMapped =
true;
834 sourceBone = sourceSkeleton->
findBone(mapping.source);
838 if (!explicitlyMapped) sourceBone = sourceSkeleton->
findBone(targetName);
839 if (!explicitlyMapped && sourceBone < 0 && profile->normalizedNameMatching_)
840 sourceBone = findNormalizedBone(sourceSkeleton, targetName);
841 if (sourceBone < 0) {
842 profile->unmatchedTargetBones_.push_back(targetName);
845 sourceForTarget[
static_cast<size_t>(targetBone)] = sourceBone;
846 ++profile->matchedBoneCount_;
849 int targetRoot = profile->targetRoot_.empty() ? -1 : targetSkeleton->
findBone(profile->targetRoot_);
850 int sourceRoot = profile->sourceRoot_.empty() ? -1 : sourceSkeleton->
findBone(profile->sourceRoot_);
851 if ((!profile->targetRoot_.empty() && targetRoot < 0) || (!profile->sourceRoot_.empty() && sourceRoot < 0))
852 throw Exception(
"AnimClip.retargetWithProfile: configured root bone was not found");
853 if (targetRoot < 0 || sourceRoot < 0) {
854 for (
int targetBone = 0; targetBone < targetSkeleton->
getBoneCount(); ++targetBone) {
855 const int candidate = sourceForTarget[
static_cast<size_t>(targetBone)];
856 if (candidate < 0 || candidate >=
static_cast<int>(tracks_.size()) ||
857 tracks_[
static_cast<size_t>(candidate)].
positions.empty())
859 const std::string
name = normalizedBoneName(targetSkeleton->
getBoneName(targetBone));
860 if (
name.find(
"hips") != std::string::npos ||
name.find(
"pelvis") != std::string::npos) {
861 targetRoot = targetBone;
862 sourceRoot = candidate;
867 if (targetRoot < 0 || sourceRoot < 0) {
868 for (
int targetBone = 0; targetBone < targetSkeleton->
getBoneCount(); ++targetBone) {
869 if (targetSkeleton->
getParent(targetBone) == -1 && sourceForTarget[
static_cast<size_t>(targetBone)] >= 0) {
870 targetRoot = targetBone;
871 sourceRoot = sourceForTarget[
static_cast<size_t>(targetBone)];
877 AnimPose sourceBindPose, targetBindPose, sourcePose;
882 const float automaticScale = profile->autoRootScale_
883 ? retargetRootMeasure(targetSkeleton, targetBindPose, targetRoot) /
884 retargetRootMeasure(sourceSkeleton, sourceBindPose, sourceRoot)
887 const int frameCount = duration_ <= 0.f ? 1 : std::max(1,
static_cast<int>(std::ceil(duration_ * sampleRate_)));
888 std::vector<TransformTRS> targetWorld(
static_cast<size_t>(targetSkeleton->
getBoneCount()));
889 for (
int frame = 0; frame <= frameCount; ++frame) {
890 if (duration_ <= 0.f && frame > 0)
break;
891 const float time = duration_ <= 0.f ? 0.f : duration_ *
static_cast<float>(frame) / frameCount;
895 for (
int targetBone = 0; targetBone < targetSkeleton->
getBoneCount(); ++targetBone) {
896 const int sourceBone = sourceForTarget[
static_cast<size_t>(targetBone)];
898 if (sourceBone >= 0 && sourceBone <
static_cast<int>(tracks_.size())) {
899 const BoneTrack& sourceTrack = tracks_[
static_cast<size_t>(sourceBone)];
903 const float sourceLength = std::sqrt(sourceBind.
px * sourceBind.
px + sourceBind.
py * sourceBind.
py +
904 sourceBind.
pz * sourceBind.
pz);
905 const float targetLength = std::sqrt(targetBind.
px * targetBind.
px + targetBind.
py * targetBind.
py +
906 targetBind.
pz * targetBind.
pz);
907 const float boneScale =
908 sourceLength > 1e-6f && targetLength > 1e-6f ? targetLength / sourceLength : 1.f;
909 if (!sourceTrack.positions.empty()) {
910 if (targetBone == targetRoot && sourceBone == sourceRoot) {
911 const float horizontal = automaticScale * profile->rootHorizontalScale_;
912 const float vertical = automaticScale * profile->rootVerticalScale_;
913 local.px = targetBind.
px + (sourceLocal.px - sourceBind.
px) * horizontal;
914 local.py = targetBind.
py + (sourceLocal.py - sourceBind.
py) * vertical;
915 local.pz = targetBind.
pz + (sourceLocal.pz - sourceBind.
pz) * horizontal;
917 local.px = targetBind.
px + (sourceLocal.px - sourceBind.
px) * boneScale;
918 local.py = targetBind.
py + (sourceLocal.py - sourceBind.
py) * boneScale;
919 local.pz = targetBind.
pz + (sourceLocal.pz - sourceBind.
pz) * boneScale;
921 out->tracks_[
static_cast<size_t>(targetBone)].
positions.push_back(
924 if (!sourceTrack.rotations.empty()) {
925 RetargetQuat desired;
926 if (profile->skeletonSpaceRotation_) {
927 desired = retargetMul(rotationOf(targetBindPose.
world(targetBone)),
928 retargetMul(retargetInverse(rotationOf(sourceBindPose.
world(sourceBone))),
929 rotationOf(sourcePose.
world(sourceBone))));
932 desired = retargetMul(retargetInverse(rotationOf(targetWorld[
static_cast<size_t>(
parent)])),
936 retargetMul(rotationOf(targetBind),
937 retargetMul(retargetInverse(rotationOf(sourceBind)), rotationOf(sourceLocal)));
939 local.qx = desired.x;
940 local.qy = desired.y;
941 local.qz = desired.z;
942 local.qw = desired.w;
943 local.normalizeRotation();
944 out->tracks_[
static_cast<size_t>(targetBone)].rotations.push_back(
947 if (!sourceTrack.scales.empty()) {
948 local.sx = targetBind.sx *
949 (std::abs(sourceBind.sx) > 1e-6f ? sourceLocal.sx / sourceBind.sx : sourceLocal.sx);
950 local.sy = targetBind.sy *
951 (std::abs(sourceBind.sy) > 1e-6f ? sourceLocal.sy / sourceBind.sy : sourceLocal.sy);
952 local.sz = targetBind.sz *
953 (std::abs(sourceBind.sz) > 1e-6f ? sourceLocal.sz / sourceBind.sz : sourceLocal.sz);
954 out->tracks_[
static_cast<size_t>(targetBone)].
scales.push_back(
958 const int parent = targetSkeleton->getParent(targetBone);
960 targetWorld[
static_cast<size_t>(targetBone)] =
local;
962 const TransformTRS& parentWorld = targetWorld[
static_cast<size_t>(
parent)];
963 TransformTRS&
world = targetWorld[
static_cast<size_t>(targetBone)];
964 const RetargetQuat worldRotation = retargetMul(rotationOf(parentWorld), rotationOf(
local));
966 world.qx = worldRotation.x;
967 world.qy = worldRotation.y;
968 world.qz = worldRotation.z;
969 world.qw = worldRotation.w;
973 if (profile->skinnedInteractionPreserve_) {
976 profile->interactionCorrectionCount_ = 0;
978 return out.release();
982 std::swap(name_, other.name_);
983 std::swap(duration_, other.duration_);
984 std::swap(loop_, other.loop_);
985 std::swap(sampleRate_, other.sampleRate_);
986 std::swap(tracks_, other.tracks_);
987 std::swap(events_, other.events_);
988 std::swap(syncMarkers_, other.syncMarkers_);
std::array< double, 10 > q
std::vector< float > positions
HexCoordinates to
Cell the unit walks towards on this segment.
graphics::Canvas * previous
std::vector< float > scales
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
EVENGINE_API_FOUNDATION public API.
Move-only operation result carrying either a value or Status.
static Result success(T value)
Construct a successful result owning value.
static Result failure(Status status)
Construct a failed result from a structured status.
static Status success(StatusCode code=StatusCode::Ok)
Construct a successful status with an explicit non-error outcome.
static void unregister(AnimClip *clip)
Forget a clip everywhere (called from ~AnimClip).
Keyframed skeletal animation clip (local TRS tracks per bone). Script type: AnimClip.
void sample(float time, AnimPose *out, const AnimSkeleton *skeleton=nullptr) const
Sample local pose at time (seconds). If skeleton non-null, missing tracks fall back to bind pose; oth...
AnimClip * retargetWithProfile(const AnimSkeleton *sourceSkeleton, const AnimSkeleton *targetSkeleton, AnimRetargetProfile *profile) const
Retarget using an Avatar-like mapping/profile and update its diagnostics. Skeleton-space rotation pre...
void addSyncMarker(float time, const std::string &name)
Add a named locomotion sync marker at clip-local time.
float getPositionKeyZ(int boneIndex, int keyIndex) const
Returns the position key z.
bool hasCompatibleSyncMarkers(const AnimClip *target) const
Whether this clip and target share a usable ordered marker interval.
bool setScaleKey(int boneIndex, int keyIndex, float time, float x, float y, float z)
Replace and re-sort one scale key.
float getRotationKeyW(int boneIndex, int keyIndex) const
Returns the rotation key w.
bool setPositionKey(int boneIndex, int keyIndex, float time, float x, float y, float z)
Replace and re-sort one position key.
float getSyncMarkerTime(int index) const
Return a sync marker's time, or 0 for an invalid index.
void addScaleKey(int boneIndex, float time, float x, float y, float z)
Adds scale key.
float getEventTime(int index) const
Return an event marker's time, or 0 for an invalid index.
void sampleLod(float time, AnimPose *out, const AnimSkeleton *skeleton, int lodLevel) const
Sample only bones enabled for lodLevel; culled tracks use target bind pose.
int getEventCount() const
Return the number of event markers.
float getScaleKeyY(int boneIndex, int keyIndex) const
Returns the scale key y.
bool hasEvent(std::string_view name) const noexcept
Whether this clip contains at least one event with the stable semantic name.
void setDuration(float seconds)
Sets the duration.
float getPositionKeyTime(int boneIndex, int keyIndex) const
Returns the position key time.
bool setRotationKey(int boneIndex, int keyIndex, float time, float x, float y, float z, float w)
Replace and re-sort one rotation key.
void addPositionKey(int boneIndex, float time, float x, float y, float z)
Adds position key.
bool setEvent(int index, float time, const std::string &name, const std::string &payload="")
Replace and re-sort one event marker.
bool removePositionKey(int boneIndex, int keyIndex)
Delete one position key.
float getRotationKeyTime(int boneIndex, int keyIndex) const
Returns the rotation key time.
std::string getEventName(int index) const
Return an event marker's name, or empty for an invalid index.
int getRotationKeyCount(int boneIndex) const
Returns the rotation key count.
float getScaleKeyTime(int boneIndex, int keyIndex) const
Returns the scale key time.
void addEvent(float time, const std::string &name, const std::string &payload="")
Add a named event marker at clip-local time. Events are kept time-sorted.
void applyPlanarRootMotion(int boneIndex, float speedX, float speedZ)
Bake planar root motion onto an existing position track (or create one). For each position key at tim...
float mapSyncTimeTo(float time, const AnimClip *target) const
Map local time into target marker space, falling back to normalized duration.
bool removeSyncMarker(int index)
Delete one sync marker.
std::unique_ptr< AnimClip > clone() const
Return a fully owning deep copy suitable for runtime hot replacement.
bool clearTrack(int boneIndex)
Clear every transform channel for one bone.
float getRotationKeyZ(int boneIndex, int keyIndex) const
Returns the rotation key z.
bool setSyncMarker(int index, float time, const std::string &name)
Replace and re-sort one sync marker.
int getScaleKeyCount(int boneIndex) const
Returns the scale key count.
void setSampleRate(float hz)
Sample rate hint used by MotionDatabase baking (Hz).
void collectEvents(float previousTime, float currentTime, bool loop, std::vector< std::string > &out) const
Collect notifies crossed by forward playback, including loop wrap.
float getPositionKeyX(int boneIndex, int keyIndex) const
Returns the position key x.
int getPositionKeyCount(int boneIndex) const
Returns the position key count.
void addRotationKey(int boneIndex, float time, float x, float y, float z, float w)
Adds rotation key.
float getRotationKeyX(int boneIndex, int keyIndex) const
Returns the rotation key x.
AnimClip * retarget(const AnimSkeleton *sourceSkeleton, const AnimSkeleton *targetSkeleton) const
Bake this clip onto a target skeleton by matching bone names and preserving bind-pose deltas....
int getSyncMarkerCount() const
Return the number of locomotion sync markers.
float getRotationKeyY(int boneIndex, int keyIndex) const
Returns the rotation key y.
bool removeRotationKey(int boneIndex, int keyIndex)
Delete one rotation key.
bool removeScaleKey(int boneIndex, int keyIndex)
Delete one scale key.
float getPositionKeyY(int boneIndex, int keyIndex) const
Returns the position key y.
int compress(float positionError=0.001f, float rotationErrorDegrees=0.1f, float scaleError=0.001f)
Remove interpolation-redundant keys while keeping sampled error below the supplied tolerances.
AnimClip(std::string name="")
Anim clip.
int getTrackCount() const
Return the number of allocated bone tracks, including empty tracks.
std::string getSyncMarkerName(int index) const
Return a sync marker's name, or empty for an invalid index.
eve::Result< void > validateNotifyContract(const std::vector< std::string > &requiredNames) const
Validates that every required semantic notify exists before gameplay registration.
float wrapTime(float time) const
Wrap or clamp time according to loop flag.
float getScaleKeyX(int boneIndex, int keyIndex) const
Returns the scale key x.
float getScaleKeyZ(int boneIndex, int keyIndex) const
Returns the scale key z.
std::string getEventPayload(int index) const
Return an event marker's optional payload.
bool removeEvent(int index)
Delete one event marker.
Evaluated local (and optional world) pose for an AnimSkeleton. Script type: AnimPose.
const TransformTRS & world(int boneIndex) const
World.
TransformTRS & local(int boneIndex)
Local.
void computeWorld(const AnimSkeleton *skeleton)
Compute world transforms from local pose + skeleton hierarchy. World values readable via getWorld* af...
void resize(int boneCount)
Resize.
Settings and diagnostics for offline skeletal animation retargeting.
void setRootTranslationScale(float horizontal, float vertical)
Set additional horizontal and vertical multipliers for root translation.
void setInteractionCorrectionWeight(float weight)
Blend weight [0,1] applied to interaction IK corrections.
int getUnmatchedBoneCount() const
Number of target bones left at bind pose by the most recent operation.
std::string getUnmatchedTargetBone(int index) const
Name of an unmatched target bone, or empty for an invalid index.
void addInteractionIkChain(const std::string &rootBone, const std::string &midBone, const std::string &tipBone)
Register a two-bone IK chain used to realize tip-sensor corrections.
void clearInteractionIkChains()
Remove all explicitly registered interaction IK chains (auto-detect remains).
void addBoneMapping(const std::string &sourceBone, const std::string &targetBone)
Map one source bone to one target bone; replaces an existing mapping for that target.
void clearBoneMappings()
Remove all explicit bone mappings.
void setInteractionContactThreshold(float distance)
Absolute source-space distance below which a sensor pair counts as interacting. Values <= 0 select 8%...
void setRootBones(const std::string &sourceBone, const std::string &targetBone)
Select the source and target pelvis/root used for proportional translation.
3D bone hierarchy + bind-pose local TRS for skeletal animation. Independent of ik::Skeleton3D (FABRIK...
int getBoneCount() const
Returns the bone count.
int getParent(int boneIndex) const
Returns the parent.
const TransformTRS & bindLocal(int boneIndex) const
Binds local.
int getBoneLodLimit(int boneIndex) const
Highest LOD at which the bone is sampled; defaults to all LODs.
std::string getBoneName(int boneIndex) const
Returns the bone name.
int findBone(const std::string &name) const
Finds bone.
void applyBindPose(class AnimPose *pose) const
Fill pose locals with bind pose.
void slerpQuat(float ax, float ay, float az, float aw, float bx, float by, float bz, float bw, float t, float &ox, float &oy, float &oz, float &ow)
Slerp quat.
float lerpf(float a, float b, float t)
Lerpf.
float clampf(float v, float lo, float hi)
Clampf.
int smrRefineRetargetedClip(const AnimClip &sourceClip, AnimClip &targetClip, const AnimSkeleton *sourceSkeleton, const AnimSkeleton *targetSkeleton, AnimRetargetProfile &profile)
Refine an FK-retargeted clip so source body-part proximity/contact is preserved on the target.
WidgetDesc separator(std::string id)
Horizontal separator line.
Two-bone IK chain used to realize an interaction correction.