19std::string normalizeToken(
const std::string&
name) {
24 for (
size_t i =
begin; i <
name.size(); ++i) {
25 const unsigned char c =
static_cast<unsigned char>(
name[i]);
26 if (std::isalnum(
c)) out.push_back(
static_cast<char>(std::tolower(
c)));
31bool tokenContains(
const std::string&
token,
const char* needle) {
return token.find(needle) != std::string::npos; }
34 const std::string
token = normalizeToken(boneName);
36 if (tokenContains(
token,
"arm") || tokenContains(
token,
"hand") || tokenContains(
token,
"wrist") ||
37 tokenContains(
token,
"elbow") || tokenContains(
token,
"shoulder") || tokenContains(
token,
"clavicle") ||
38 tokenContains(
token,
"finger") || tokenContains(
token,
"forearm"))
40 if (tokenContains(
token,
"leg") || tokenContains(
token,
"foot") || tokenContains(
token,
"toe") ||
41 tokenContains(
token,
"ankle") || tokenContains(
token,
"knee") || tokenContains(
token,
"upleg") ||
42 tokenContains(
token,
"thigh") || tokenContains(
token,
"calf"))
44 if (tokenContains(
token,
"spine") || tokenContains(
token,
"chest") || tokenContains(
token,
"torso") ||
45 tokenContains(
token,
"hips") || tokenContains(
token,
"pelvis") || tokenContains(
token,
"abdomen") ||
46 tokenContains(
token,
"ribcage") || tokenContains(
token,
"belly") ||
token ==
"root" ||
47 tokenContains(
token,
"hip"))
53 if (
a >
b) std::swap(
a,
b);
63 float x = 0.f,
y = 0.f,
z = 0.f;
66Vec3 add(
const Vec3&
a,
const Vec3&
b) {
return {
a.x +
b.x,
a.y +
b.y,
a.z +
b.z}; }
67Vec3 sub(
const Vec3&
a,
const Vec3&
b) {
return {
a.x -
b.x,
a.y -
b.y,
a.z -
b.z}; }
68Vec3 mul(
const Vec3&
a,
float s) {
return {
a.x *
s,
a.y *
s,
a.z *
s}; }
69float length(
const Vec3&
a) {
return std::sqrt(
a.x *
a.x +
a.y *
a.y +
a.z *
a.z); }
71Vec3 transformPoint(
const TransformTRS&
world,
float lx,
float ly,
float lz) {
73 const float x2 =
x +
x, y2 =
y +
y, z2 =
z +
z;
74 const float xx =
x * x2, xy =
x * y2, xz =
x * z2;
75 const float yy =
y * y2, yz =
y * z2, zz =
z * z2;
76 const float wx =
w * x2,
wy =
w * y2,
wz =
w * z2;
77 const float r00 = (1.f - (yy + zz)) *
world.sx;
78 const float r10 = (xy +
wz) *
world.sx;
79 const float r20 = (xz -
wy) *
world.sx;
80 const float r01 = (xy -
wz) *
world.sy;
81 const float r11 = (1.f - (xx + zz)) *
world.sy;
82 const float r21 = (yz +
wx) *
world.sy;
83 const float r02 = (xz +
wy) *
world.sz;
84 const float r12 = (yz -
wx) *
world.sz;
85 const float r22 = (1.f - (xx + yy)) *
world.sz;
86 return {
world.px + r00 * lx + r01 * ly + r02 * lz,
world.py + r10 * lx + r11 * ly + r12 * lz,
87 world.pz + r20 * lx + r21 * ly + r22 * lz};
90float skeletonExtent(
const AnimSkeleton* skeleton) {
92 skeleton->applyBindPose(&bind);
93 bind.computeWorld(skeleton);
94 if (skeleton->getBoneCount() == 0)
return 1.f;
95 const TransformTRS&
root = bind.world(0);
97 for (
int bone = 1;
bone < skeleton->getBoneCount(); ++
bone) {
98 const TransformTRS&
world = bind.world(
bone);
101 return std::max(maxR, 1e-3f);
104int findBoneFlexible(
const AnimSkeleton* skeleton,
const std::string&
name) {
105 if (!skeleton ||
name.empty())
return -1;
106 const int exact = skeleton->findBone(
name);
107 if (exact >= 0)
return exact;
108 const std::string want = normalizeToken(
name);
110 for (
int bone = 0;
bone < skeleton->getBoneCount(); ++
bone) {
111 if (normalizeToken(skeleton->getBoneName(
bone)) != want)
continue;
112 if (
hit >= 0)
return -1;
118struct ResolvedChain {
124std::vector<ResolvedChain> resolveChains(
const AnimSkeleton* skeleton,
125 const std::vector<AnimSmrIkChain>& configuredChains) {
126 std::vector<ResolvedChain> out;
127 for (
const AnimSmrIkChain& chain : configuredChains) {
128 ResolvedChain resolved{findBoneFlexible(skeleton, chain.rootBone), findBoneFlexible(skeleton, chain.midBone),
129 findBoneFlexible(skeleton, chain.tipBone)};
130 if (resolved.root >= 0 && resolved.mid >= 0 && resolved.tip >= 0) out.push_back(resolved);
132 if (!out.empty())
return out;
134 auto tryPush = [&](
const char*
root,
const char*
mid,
const char*
tip) {
135 ResolvedChain resolved{findBoneFlexible(skeleton,
root), findBoneFlexible(skeleton,
mid),
136 findBoneFlexible(skeleton,
tip)};
137 if (resolved.root >= 0 && resolved.mid >= 0 && resolved.tip >= 0) out.push_back(resolved);
139 tryPush(
"LeftShoulder",
"LeftElbow",
"LeftHand");
140 tryPush(
"RightShoulder",
"RightElbow",
"RightHand");
141 tryPush(
"LeftUpLeg",
"LeftLeg",
"LeftFoot");
142 tryPush(
"RightUpLeg",
"RightLeg",
"RightFoot");
143 tryPush(
"leftarm",
"leftforearm",
"lefthand");
144 tryPush(
"rightarm",
"rightforearm",
"righthand");
145 tryPush(
"leftupleg",
"leftleg",
"leftfoot");
146 tryPush(
"rightupleg",
"rightleg",
"rightfoot");
147 tryPush(
"shoulder_l",
"elbow_l",
"hand_l");
148 tryPush(
"shoulder_r",
"elbow_r",
"hand_r");
149 tryPush(
"thigh_l",
"calf_l",
"foot_l");
150 tryPush(
"thigh_r",
"calf_r",
"foot_r");
154int chainForTip(
const std::vector<ResolvedChain>& chains,
int tipBone) {
155 for (
int i = 0; i < static_cast<int>(chains.size()); ++i) {
156 if (chains[
static_cast<size_t>(i)].tip == tipBone)
return i;
161struct InteractionPair {
167void collectInteractions(
const AnimSmrSensorCloud& cloud,
const std::vector<float>& xyz,
float contactDistance,
168 std::vector<InteractionPair>& out) {
170 const int count = cloud.getSensorCount();
171 for (
int i = 0; i <
count; ++i) {
173 for (
int j = i + 1; j <
count; ++j) {
175 if (!partsInteract(partI, partJ))
continue;
185 const Vec3 po{xyz[
static_cast<size_t>(
observer) * 3], xyz[
static_cast<size_t>(
observer) * 3 + 1],
186 xyz[
static_cast<size_t>(
observer) * 3 + 2]};
187 const Vec3 pj{xyz[
static_cast<size_t>(
subject) * 3], xyz[
static_cast<size_t>(
subject) * 3 + 1],
188 xyz[
static_cast<size_t>(
subject) * 3 + 2]};
189 const Vec3 delta = sub(pj, po);
190 if (
length(delta) > contactDistance)
continue;
196int matchSensor(
const AnimSmrSensorCloud& sourceCloud,
int sourceIndex,
const AnimSmrSensorCloud& targetCloud) {
197 const AnimSmrSensor&
source = sourceCloud.getSensor(sourceIndex);
199 for (
int i = 0; i < targetCloud.getSensorCount(); ++i) {
200 const AnimSmrSensor&
target = targetCloud.getSensor(i);
203 const size_t sourceHash =
source.semanticKey.find(
'#');
204 const size_t targetHash =
target.semanticKey.find(
'#');
205 if (sourceHash != std::string::npos && targetHash != std::string::npos &&
206 source.semanticKey.substr(sourceHash) ==
target.semanticKey.substr(targetHash)) {
211 for (
int i = 0; i < targetCloud.getSensorCount(); ++i) {
212 if (targetCloud.getSensorPart(i) ==
source.part)
return i;
217void writeRotationKey(AnimClip&
clip,
int boneIndex,
int keyIndex,
float time,
const TransformTRS&
local) {
218 if (keyIndex >= 0 && keyIndex <
clip.getRotationKeyCount(boneIndex)) {
230 if (!skeleton)
throw Exception(
"AnimSmrSensorCloud.fromSkeletonDense: skeleton is null");
231 if (ringsPerBone < 1 || pointsPerRing < 3)
232 throw Exception(
"AnimSmrSensorCloud.fromSkeletonDense: ringsPerBone>=1 and pointsPerRing>=3 required");
235 std::vector<int> firstChild(
static_cast<size_t>(skeleton->
getBoneCount()), -1);
238 if (
parent >= 0 && firstChild[
static_cast<size_t>(
parent)] < 0) firstChild[
static_cast<size_t>(
parent)] =
bone;
242 const int child = firstChild[
static_cast<size_t>(
bone)];
243 if (child < 0)
continue;
246 std::sqrt(childBind.
px * childBind.
px + childBind.
py * childBind.
py + childBind.
pz * childBind.
pz);
247 if (len < 1e-5f)
continue;
248 const float radius = len * 0.08f;
250 float ax = childBind.
px / len,
ay = childBind.
py / len,
az = childBind.
pz / len;
251 float bx = 0.f,
by = 1.f,
bz = 0.f;
252 if (std::fabs(
ay) > 0.9f) {
258 float cl = std::sqrt(
cx *
cx +
cy *
cy + cz * cz);
259 if (cl < 1e-5f)
continue;
266 for (
int ring = 0; ring < ringsPerBone; ++ring) {
267 const float t = (
static_cast<float>(ring) + 1.f) / (
static_cast<float>(ringsPerBone) + 1.f);
268 for (
int sample = 0; sample < pointsPerRing; ++sample) {
269 const float angle = 6.28318530718f *
static_cast<float>(sample) /
static_cast<float>(pointsPerRing);
270 const float ca = std::cos(
angle), sa = std::sin(
angle);
277 sensor.
semanticKey =
token +
"#r" + std::to_string(ring) +
"p" + std::to_string(sample);
278 cloud.sensors_.push_back(sensor);
286 if (!skeleton)
throw Exception(
"AnimSmrSensorCloud.fromSkeleton: skeleton is null");
289 std::vector<int> firstChild(
static_cast<size_t>(skeleton->
getBoneCount()), -1);
292 if (
parent >= 0 && firstChild[
static_cast<size_t>(
parent)] < 0) firstChild[
static_cast<size_t>(
parent)] =
bone;
302 cloud.sensors_.push_back(joint);
304 const int child = firstChild[
static_cast<size_t>(
bone)];
305 if (child < 0)
continue;
310 mid.localX = childBind.
px * 0.5f;
311 mid.localY = childBind.
py * 0.5f;
312 mid.localZ = childBind.
pz * 0.5f;
314 cloud.sensors_.push_back(
mid);
320 if (index < 0 || index >=
getSensorCount())
throw Exception(
"AnimSmrSensorCloud.getSensor: index out of range");
321 return sensors_[
static_cast<size_t>(
index)];
327 if (!
pose)
throw Exception(
"AnimSmrSensorCloud.evaluateWorldPositions: pose is null");
330 const AnimSmrSensor& sensor = sensors_[
static_cast<size_t>(i)];
332 outXYZ[
static_cast<size_t>(i) * 3] = 0.f;
333 outXYZ[
static_cast<size_t>(i) * 3 + 1] = 0.f;
334 outXYZ[
static_cast<size_t>(i) * 3 + 2] = 0.f;
338 outXYZ[
static_cast<size_t>(i) * 3] =
world.x;
339 outXYZ[
static_cast<size_t>(i) * 3 + 1] =
world.y;
340 outXYZ[
static_cast<size_t>(i) * 3 + 2] =
world.z;
346 if (!sourceSkeleton || !targetSkeleton)
throw Exception(
"smrRefineRetargetedClip: skeleton is null");
348 if (!profile.skinnedInteractionPreserve_) {
349 profile.interactionCorrectionCount_ = 0;
350 profile.neuralInferenceCount_ = 0;
354 profile.neuralInferenceCount_ = 0;
355 if (profile.neuralRetargetEnabled_) {
356 if (
auto* neural = cap::query<ISmrNeuralRetarget>()) {
357 if (neural->isReady(&profile)) {
360 request.targetClip = &targetClip;
361 request.sourceSkeleton = sourceSkeleton;
362 request.targetSkeleton = targetSkeleton;
364 auto neuralResult = neural->retarget(
request);
365 if (neuralResult.ok()) {
366 profile.interactionCorrectionCount_ = neuralResult.value().framesWritten;
367 profile.neuralInferenceCount_ = std::max(1, neuralResult.value().framesWritten);
368 return profile.interactionCorrectionCount_;
376 const std::vector<ResolvedChain> chains = resolveChains(targetSkeleton, profile.interactionIkChains_);
378 profile.interactionCorrectionCount_ = 0;
382 const float sourceExtent = skeletonExtent(sourceSkeleton);
383 const float targetExtent = skeletonExtent(targetSkeleton);
384 const float sizeScale = targetExtent / sourceExtent;
385 const float contactDistance =
386 std::max(1e-4f, profile.interactionContactThreshold_ > 0.f ? profile.interactionContactThreshold_
387 : sourceExtent * 0.08f);
388 const float weight =
clampf(profile.interactionCorrectionWeight_, 0.f, 1.f);
391 const float sampleRate = std::max(targetClip.
getSampleRate(), 1.f);
392 const int frameCount =
duration <= 0.f ? 1 : std::max(1,
static_cast<int>(std::ceil(
duration * sampleRate)));
396 std::vector<float> sourceXYZ;
397 std::vector<float> targetXYZ;
398 std::vector<InteractionPair> pairs;
401 for (
int frame = 0; frame <= frameCount; ++frame) {
402 if (duration <= 0.f && frame > 0)
break;
404 duration <= 0.f ? 0.f :
duration *
static_cast<float>(frame) /
static_cast<float>(frameCount);
405 sourceClip.
sample(
time, &sourcePose, sourceSkeleton);
407 targetClip.
sample(
time, &targetPose, targetSkeleton);
412 collectInteractions(sourceCloud, sourceXYZ, contactDistance, pairs);
414 bool frameChanged =
false;
415 for (
const InteractionPair& pair : pairs) {
416 const int targetObserver = matchSensor(sourceCloud, pair.observer, targetCloud);
417 const int targetSubject = matchSensor(sourceCloud, pair.subject, targetCloud);
418 if (targetObserver < 0 || targetSubject < 0)
continue;
421 const int chainIndex = chainForTip(chains, subjectSensor.
boneIndex);
422 if (chainIndex < 0)
continue;
424 const Vec3 observerWorld{targetXYZ[
static_cast<size_t>(targetObserver) * 3],
425 targetXYZ[
static_cast<size_t>(targetObserver) * 3 + 1],
426 targetXYZ[
static_cast<size_t>(targetObserver) * 3 + 2]};
427 const Vec3 desiredSubject = add(observerWorld, mul(pair.relative, sizeScale));
430 const Vec3 tipPos{tipWorld.
px, tipWorld.
py, tipWorld.
pz};
431 const Vec3 sensorWorld{targetXYZ[
static_cast<size_t>(targetSubject) * 3],
432 targetXYZ[
static_cast<size_t>(targetSubject) * 3 + 1],
433 targetXYZ[
static_cast<size_t>(targetSubject) * 3 + 2]};
434 const Vec3 tipTarget = sub(desiredSubject, sub(sensorWorld, tipPos));
436 const ResolvedChain& chain = chains[
static_cast<size_t>(chainIndex)];
437 if (targetPose.
solveTwoBoneIK(targetSkeleton, chain.root, chain.mid, chain.tip, tipTarget.x, tipTarget.y,
439 writeRotationKey(targetClip, chain.root, frame,
time, targetPose.
local(chain.root));
440 writeRotationKey(targetClip, chain.mid, frame,
time, targetPose.
local(chain.mid));
441 writeRotationKey(targetClip, chain.tip, frame,
time, targetPose.
local(chain.tip));
447 if (frameChanged) ++corrections;
450 profile.interactionCorrectionCount_ = corrections;
eve::EntitySpatialPose pose
const GltfImportRequest & request
std::map< Cell, int > best
const UnitySourceAsset & source
EVENGINE_API_FOUNDATION public API.
Keyframed skeletal animation clip (local TRS tracks per bone). Script type: AnimClip.
float getDuration() const
Returns the duration.
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...
float getSampleRate() const
Returns the sample rate.
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...
bool solveTwoBoneIK(const AnimSkeleton *skeleton, int rootBone, int midBone, int tipBone, float targetX, float targetY, float targetZ, float weight=1.f)
Analytically solve a root-mid-tip chain toward a world-space target.
Settings and diagnostics for offline skeletal animation retargeting.
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.
std::string getBoneName(int boneIndex) const
Returns the bone name.
Bone-attached sensor cloud for skinned-motion-retarget interaction queries. Script type: AnimSmrSenso...
AnimSmrBodyPart getSensorPart(int index) const
Returns the sensor part.
void evaluateWorldPositions(const AnimPose *pose, std::vector< float > &outXYZ) const
Evaluate world-space sensor positions for a pose that already has computeWorld() applied.
int getSensorCount() const
Returns the sensor count.
static AnimSmrSensorCloud fromSkeleton(const AnimSkeleton *skeleton)
Build sensors for every bone: one at the joint and one mid-segment toward the first child.
static AnimSmrSensorCloud fromSkeletonDense(const AnimSkeleton *skeleton, int ringsPerBone, int pointsPerRing)
Build denser MeshRet-style SCS rings around each bone segment.
const AnimSmrSensor & getSensor(int index) const
Returns the sensor.
AnimSmrBodyPart
Coarse body-part tag used to select MeshRet-style interaction pairs.
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.
One semantically consistent sensor attached to a bone (classical SCS stand-in).
std::string semanticKey
Normalized bone name + sample id for cross-skeleton match.
Inputs for neural MeshRet-style skinned motion retarget.
const AnimClip * sourceClip