Loading...
Searching...
No Matches
HumanoidSourceAdapters.hpp
1#pragma once
2
3// Converts the two source-data shapes HumanoidRetarget accepts --
4// keypoint_stream from an ONNX PoseDetector, tracker_bundle_6 from a mocap
5// device -- into the canonical humanoid_pose. Both paths produce PARENT-LOCAL
6// quaternions, which is the invariant HumanoidRetarget depends on.
7
8#include <Threedim/HumanoidPose.hpp>
9#include <Threedim/HumanoidSourceMaps.hpp>
10
11#include <array>
12#include <cmath>
13
14namespace Threedim
15{
16
17// ---------------------------------------------------------------------------
18// Small quaternion helpers. Inline and header-only for zero TU overhead.
19// (x, y, z, w) layout, matching ossia::skeleton_joint::rotation and
20// humanoid_bone::q*.
21// ---------------------------------------------------------------------------
22inline void quat_mul_xyzw(
23 const float a[4], const float b[4], float out[4]) noexcept
24{
25 const float x = a[3] * b[0] + a[0] * b[3] + a[1] * b[2] - a[2] * b[1];
26 const float y = a[3] * b[1] - a[0] * b[2] + a[1] * b[3] + a[2] * b[0];
27 const float z = a[3] * b[2] + a[0] * b[1] - a[1] * b[0] + a[2] * b[3];
28 const float w = a[3] * b[3] - a[0] * b[0] - a[1] * b[1] - a[2] * b[2];
29 out[0] = x; out[1] = y; out[2] = z; out[3] = w;
30}
31
32inline void quat_inv_xyzw(const float q[4], float out[4]) noexcept
33{
34 // Inverse of a unit quaternion = conjugate.
35 out[0] = -q[0]; out[1] = -q[1]; out[2] = -q[2]; out[3] = q[3];
36}
37
38// Shortest-arc rotation from unit vector `from` to unit vector `to`.
39// Produces the quaternion q such that q·from = to. Used to turn a
40// canonical T-pose bone axis into the observed bone direction; this is
41// inherently a 2-DoF answer (the twist around the bone's own length is
42// undefined by just two direction endpoints): a hard limit of single-camera
43// keypoint mocap.
44inline void shortest_arc(
45 const float from[3], const float to[3], float out[4]) noexcept
46{
47 const float d = from[0] * to[0] + from[1] * to[1] + from[2] * to[2];
48 const float eps = 1e-6f;
49
50 if(d >= 1.f - eps)
51 {
52 // Aligned — identity.
53 out[0] = 0.f; out[1] = 0.f; out[2] = 0.f; out[3] = 1.f;
54 return;
55 }
56 if(d <= -1.f + eps)
57 {
58 // Antiparallel — 180° around ANY perpendicular axis. Pick one that
59 // isn't (near-)parallel to `from` for numerical stability.
60 float axis[3];
61 if(std::fabs(from[0]) < 0.9f)
62 {
63 axis[0] = 1.f - from[0] * from[0];
64 axis[1] = -from[0] * from[1];
65 axis[2] = -from[0] * from[2];
66 }
67 else
68 {
69 axis[0] = -from[1] * from[0];
70 axis[1] = 1.f - from[1] * from[1];
71 axis[2] = -from[1] * from[2];
72 }
73 const float len
74 = std::sqrt(axis[0] * axis[0] + axis[1] * axis[1] + axis[2] * axis[2]);
75 if(len > eps)
76 {
77 const float inv = 1.f / len;
78 out[0] = axis[0] * inv;
79 out[1] = axis[1] * inv;
80 out[2] = axis[2] * inv;
81 }
82 else
83 {
84 out[0] = 1.f; out[1] = 0.f; out[2] = 0.f;
85 }
86 out[3] = 0.f;
87 return;
88 }
89
90 // General case — half-vector formulation for numerical stability.
91 const float cross[3] = {
92 from[1] * to[2] - from[2] * to[1],
93 from[2] * to[0] - from[0] * to[2],
94 from[0] * to[1] - from[1] * to[0]};
95 const float s = std::sqrt((1.f + d) * 2.f);
96 const float invs = 1.f / s;
97 out[0] = cross[0] * invs;
98 out[1] = cross[1] * invs;
99 out[2] = cross[2] * invs;
100 out[3] = s * 0.5f;
101}
102
103// ---------------------------------------------------------------------------
104// keypoints -> humanoid_pose.
105//
106// 1. For each bone with a valid (parent_idx, child_idx) edge and both
107// keypoints above the confidence threshold:
108// d_world[b] = normalize(kp[child] - kp[parent])
109// q_world[b] = shortestArc(kRestAxis[b], d_world[b])
110// 2. Walk bones in enum order, which is already topological since a bone's
111// parent has a lower index. No world rotation means validity = 0; a parent
112// without one makes q_world[b] the local rotation, i.e. root-relative;
113// otherwise q_local[b] = inv(q_world[parent(b)]) * q_world[b].
114// 3. Copy the Hips world position from the landmark that best represents it,
115// the hip midpoint for BlazePose and COCO, approximated as left_hip. Used
116// only by the root-motion toggle downstream.
117// ---------------------------------------------------------------------------
118inline humanoid_pose keypoints_to_humanoid_pose(
119 const keypoint_stream& stream,
120 const HumanoidKeypointMap& map,
121 float confidence_threshold = 0.5f) noexcept
122{
123 humanoid_pose out{};
124
125 // Step 1: per-bone world rotations.
126 constexpr std::size_t N = std::size_t(humanoid_bone_index::Count);
127 std::array<std::array<float, 4>, N> q_world{};
128 std::array<bool, N> has_world{};
129
130 const auto& kps = stream.keypoints;
131 const int K = int(kps.size());
132
133 for(std::size_t b = 0; b < N; ++b)
134 {
135 has_world[b] = false;
136 q_world[b] = {0.f, 0.f, 0.f, 1.f};
137
138 const auto& edge = map[b];
139 if(!edge.valid() || edge.parent_idx == edge.child_idx)
140 continue;
141 if(edge.parent_idx >= K || edge.child_idx >= K)
142 continue;
143
144 const auto& p = kps[std::size_t(edge.parent_idx)];
145 const auto& c = kps[std::size_t(edge.child_idx)];
146 if(p.confidence < confidence_threshold
147 || c.confidence < confidence_threshold)
148 continue;
149
150 float d[3] = {c.x - p.x, c.y - p.y, c.z - p.z};
151 const float len = std::sqrt(d[0] * d[0] + d[1] * d[1] + d[2] * d[2]);
152 if(len < 1e-6f)
153 continue;
154 const float inv = 1.f / len;
155 d[0] *= inv; d[1] *= inv; d[2] *= inv;
156
157 const auto& rest = kHumanoidRestAxis[b];
158 shortest_arc(rest.data(), d, q_world[b].data());
159 has_world[b] = true;
160 }
161
162 // Step 2: world → parent-local. Enum order is topological: each bone's
163 // parent has a strictly lower index, so a single forward pass is safe.
164 for(std::size_t b = 0; b < N; ++b)
165 {
166 auto& bone = out.bones[b];
167 if(!has_world[b])
168 {
169 bone.validity = 0.f;
170 bone.qx = 0.f; bone.qy = 0.f; bone.qz = 0.f; bone.qw = 1.f;
171 continue;
172 }
173
174 const auto parent_idx = kHumanoidParent[b];
175 if(parent_idx == humanoid_bone_index::Count
176 || !has_world[std::size_t(parent_idx)])
177 {
178 // Root bone OR parent's world rotation is unknown — emit our world
179 // rotation as local. For root this is correct; for a bone whose
180 // parent failed to resolve this is a reasonable degradation (the
181 // bone will orient absolutely rather than relative to a missing
182 // parent, which at least keeps it visible).
183 bone.qx = q_world[b][0];
184 bone.qy = q_world[b][1];
185 bone.qz = q_world[b][2];
186 bone.qw = q_world[b][3];
187 }
188 else
189 {
190 float inv_parent[4];
191 quat_inv_xyzw(q_world[std::size_t(parent_idx)].data(), inv_parent);
192 float local[4];
193 quat_mul_xyzw(inv_parent, q_world[b].data(), local);
194 bone.qx = local[0]; bone.qy = local[1];
195 bone.qz = local[2]; bone.qw = local[3];
196 }
197 bone.validity = 1.f;
198 }
199
200 // Hip translation — grab the parent keypoint of the Spine edge as the
201 // best "pelvis" proxy (BlazePose landmark 23 = left_hip, COCO 11 =
202 // left_hip). Not the true midpoint, but close enough for single-camera
203 // root motion; the tracker path below carries a real pelvis position.
204 const auto& spine_edge = map[std::size_t(humanoid_bone_index::Spine)];
205 if(spine_edge.parent_idx >= 0 && spine_edge.parent_idx < K)
206 {
207 const auto& hip_kp = kps[std::size_t(spine_edge.parent_idx)];
208 if(hip_kp.confidence >= confidence_threshold)
209 {
210 out.hip_x = hip_kp.x;
211 out.hip_y = hip_kp.y;
212 out.hip_z = hip_kp.z;
213 }
214 }
215
216 return out;
217}
218
219// ---------------------------------------------------------------------------
220// trackers -> humanoid_pose.
221//
222// With 6 trackers (head, hips, 2 hands, 2 feet) only those 6 bones are driven;
223// spine, shoulders, elbows and knees stay at their retarget rest. Making them
224// follow realistically needs either more trackers (10-point Vive Full-Body) or a
225// downstream 2-bone IK chain keyed on shoulder and wrist positions.
226//
227// Tracker quaternions are world-space by convention -- PSN, OSC and VRPN all
228// report world transforms -- so parent-local is produced by inverting the parent
229// bone's tracker rotation when that parent also has a tracker, and inherited
230// directly otherwise.
231// ---------------------------------------------------------------------------
232inline humanoid_pose trackers_to_humanoid_pose(
233 const tracker_bundle_6& t) noexcept
234{
235 humanoid_pose out{};
236
237 // Slot 1:1 mapping — which canonical bone gets which tracker.
238 struct Slot
239 {
240 humanoid_bone_index bone;
241 const tracker_pose* tr;
242 };
243 const Slot slots[] = {
244 {humanoid_bone_index::Hips, &t.hips},
245 {humanoid_bone_index::Head, &t.head},
246 {humanoid_bone_index::LeftHand, &t.left_hand},
247 {humanoid_bone_index::RightHand, &t.right_hand},
248 {humanoid_bone_index::LeftFoot, &t.left_foot},
249 {humanoid_bone_index::RightFoot, &t.right_foot},
250 };
251
252 // Gather world rotations.
253 constexpr std::size_t N = std::size_t(humanoid_bone_index::Count);
254 std::array<std::array<float, 4>, N> q_world{};
255 std::array<bool, N> has_world{};
256 for(std::size_t b = 0; b < N; ++b)
257 {
258 q_world[b] = {0.f, 0.f, 0.f, 1.f};
259 has_world[b] = false;
260 }
261
262 for(const auto& slot : slots)
263 {
264 if(slot.tr->validity < 0.5f)
265 continue;
266 const std::size_t idx = std::size_t(slot.bone);
267 q_world[idx] = {slot.tr->qx, slot.tr->qy, slot.tr->qz, slot.tr->qw};
268 has_world[idx] = true;
269 }
270
271 // World → parent-local, same pattern as the keypoint path. Bones whose
272 // parent has no tracker fall through to "emit world as local", which
273 // makes them pose relative to the world origin — correct for Head /
274 // Hands when their parent chain (Neck, LowerArm) isn't tracker-driven.
275 for(std::size_t b = 0; b < N; ++b)
276 {
277 auto& bone = out.bones[b];
278 if(!has_world[b])
279 {
280 bone.validity = 0.f;
281 continue;
282 }
283
284 const auto parent_idx = kHumanoidParent[b];
285 if(parent_idx == humanoid_bone_index::Count
286 || !has_world[std::size_t(parent_idx)])
287 {
288 bone.qx = q_world[b][0]; bone.qy = q_world[b][1];
289 bone.qz = q_world[b][2]; bone.qw = q_world[b][3];
290 }
291 else
292 {
293 float inv_parent[4];
294 quat_inv_xyzw(q_world[std::size_t(parent_idx)].data(), inv_parent);
295 float local[4];
296 quat_mul_xyzw(inv_parent, q_world[b].data(), local);
297 bone.qx = local[0]; bone.qy = local[1];
298 bone.qz = local[2]; bone.qw = local[3];
299 }
300 bone.validity = 1.f;
301 }
302
303 // Hip position = hips tracker position (if tracking).
304 if(t.hips.validity >= 0.5f)
305 {
306 out.hip_x = t.hips.x;
307 out.hip_y = t.hips.y;
308 out.hip_z = t.hips.z;
309 }
310
311 return out;
312}
313
314} // namespace Threedim