Loading...
Searching...
No Matches
InverseKinematics.hpp
1#pragma once
2#include <halp/controls.hpp>
3#include <halp/meta.hpp>
4
5#include <ossia/dataflow/geometry_port.hpp>
6
7#include <QMatrix4x4>
8#include <QQuaternion>
9#include <QVector3D>
10
11#include <algorithm>
12#include <cmath>
13#include <cstdint>
14#include <cstring>
15#include <memory>
16#include <string>
17
18namespace Threedim
19{
20
21// Two-bone analytical IK solver operating on a scene_spec's skeleton.
22//
23// Given a 3-joint chain (root -> mid -> end), a target world-space position and a
24// pole vector to disambiguate the elbow plane, produces the joint rotations that
25// make the end effector reach, or come as close as possible to, the target.
26// Law-of-cosines closed form, about 50 floating-point ops, no iteration.
27//
28// The solver reads the input skeleton's TRS, finds the named end joint, walks two
29// parents up to identify the chain, and emits a scene_spec with only those three
30// joints' local rotations modified; the rest of the skeleton and the mesh and
31// material data pass through unchanged.
32//
33// This is "reach for that door handle" IK. Full articulated rigs with more than
34// two bones, spine chains or pole-axis constraints want a sequence of these per
35// limb, or a FABRIK/CCD successor on N-joint chains; the interface is
36// deliberately narrow so swapping one in later does not break patches.
37//
38// Limitations: no joint limits or rotation constraints, no twist decomposition,
39// the chain must be a direct parent line rather than a branch, and an unreachable
40// target extends the chain fully toward it, the natural straight-arm stretch.
42{
43public:
44 halp_meta(name, "Inverse Kinematics (2-bone)")
45 halp_meta(category, "Visuals/3D/Scene")
46 halp_meta(c_name, "inverse_kinematics")
47 halp_meta(description, "Solve a two-bone skeleton chain toward a target using a pole vector to control the bend.")
48 halp_meta(authors, "ossia team")
49 halp_meta(
50 manual_url,
51 "https://ossia.io/score-docs/processes/inverse-kinematics.html")
52 halp_meta(uuid, "6e9f2a4c-1b85-4d3e-a7f6-8c2b4d5e9a0f")
53
54 struct ins
55 {
56 struct
57 {
58 halp_meta(name, "Scene In");
59 ossia::scene_spec scene;
60 uint8_t dirty{0};
61 } scene_in;
62
63 halp::lineedit<"End joint name", "hand_r"> end_joint;
64
65 halp::xyz_spinboxes_f32<
66 "Target",
67 halp::range{-10000., 10000., 0.}>
68 target;
69 halp::xyz_spinboxes_f32<
70 "Pole vector",
71 halp::range{-10000., 10000., 0.}>
72 pole;
73
74 halp::hslider_f32<"Weight", halp::range{0., 1., 1.}> weight;
75 } inputs;
76
77 struct outs
78 {
79 struct
80 {
81 halp_meta(name, "Scene Out");
82 ossia::scene_spec scene;
83 uint8_t dirty{0};
84 } scene_out;
85 } outputs;
86
87 std::shared_ptr<ossia::scene_state> m_state;
88 int64_t m_version{0};
89
90 static QVector3D toVec(const float v[3]) { return QVector3D(v[0], v[1], v[2]); }
91 static QQuaternion toQuat(const float v[4])
92 {
93 return QQuaternion(v[3], v[0], v[1], v[2]);
94 }
95 static void fromQuat(float v[4], const QQuaternion& q)
96 {
97 v[0] = q.x(); v[1] = q.y(); v[2] = q.z(); v[3] = q.scalar();
98 }
99
100 // Compute world-space position of joint `idx` by walking up the parent
101 // chain and composing TRS transforms.
102 static QVector3D worldJointPos(
103 const ossia::skeleton_component& skel, int32_t idx)
104 {
105 if(idx < 0 || idx >= (int32_t)skel.joints.size())
106 return QVector3D();
107
108 // Build a chain from root to idx, then compose forward.
109 ossia::small_vector<int32_t, 16> chain;
110 for(int32_t i = idx; i >= 0; i = skel.joints[i].parent_index)
111 chain.push_back(i);
112 std::reverse(chain.begin(), chain.end());
113
114 QMatrix4x4 M;
115 for(int32_t i : chain)
116 {
117 const auto& j = skel.joints[i];
118 QMatrix4x4 T;
119 T.translate(j.translation[0], j.translation[1], j.translation[2]);
120 T.rotate(QQuaternion(
121 j.rotation[3], j.rotation[0], j.rotation[1], j.rotation[2]));
122 T.scale(j.scale[0], j.scale[1], j.scale[2]);
123 M = M * T;
124 }
125 return M.map(QVector3D());
126 }
127
128 // 2-bone IK core: given three world positions + target + pole, compute
129 // the rotations (world-space) to apply at the root and mid joints so that
130 // end reaches the target. Returns the delta rotations as quaternions.
131 struct Solution
132 {
133 QQuaternion rootDelta;
134 QQuaternion midDelta;
135 };
136 static Solution solve2Bone(
137 QVector3D root, QVector3D mid, QVector3D end,
138 QVector3D target, QVector3D pole)
139 {
140 const float eps = 1e-6f;
141 QVector3D r2m = mid - root;
142 QVector3D m2e = end - mid;
143 QVector3D r2e = end - root;
144 QVector3D r2t = target - root;
145
146 const float lA = r2m.length();
147 const float lB = m2e.length();
148 const float lTgt = std::min(r2t.length(), lA + lB - eps);
149 if(lA < eps || lB < eps || lTgt < eps)
150 return {QQuaternion(), QQuaternion()};
151
152 // New elbow interior angle via law of cosines:
153 // cos(theta) = (lA² + lB² - lTgt²) / (2 lA lB)
154 const float cosNew = std::clamp(
155 (lA * lA + lB * lB - lTgt * lTgt) / (2.0f * lA * lB), -1.0f, 1.0f);
156 const float thetaNew = std::acos(cosNew);
157
158 // Current elbow interior angle.
159 const float cosCur = std::clamp(
160 QVector3D::dotProduct(-r2m.normalized(), m2e.normalized()),
161 -1.0f, 1.0f);
162 const float thetaCur = std::acos(cosCur);
163
164 // Rotation axis for the elbow: perpendicular to the current arm plane,
165 // oriented by the pole vector so we pick the "elbow side".
166 QVector3D planeNormal = QVector3D::crossProduct(r2m, m2e);
167 if(planeNormal.lengthSquared() < eps)
168 {
169 // Arm is straight → use pole vector's projected perpendicular.
170 QVector3D poleDir = (pole - root).normalized();
171 planeNormal = QVector3D::crossProduct(r2e.normalized(), poleDir);
172 if(planeNormal.lengthSquared() < eps)
173 planeNormal = QVector3D(0, 1, 0);
174 }
175 planeNormal.normalize();
176
177 QQuaternion elbowDelta = QQuaternion::fromAxisAndAngle(
178 planeNormal, (thetaCur - thetaNew) * 180.0f / float(M_PI));
179
180 // Rotate the shoulder so the end effector lands on the target. The
181 // root realignment must be computed from the POST-bend end direction:
182 // bending the elbow changes the root->end direction whenever the elbow
183 // angle changes, so aligning the pre-bend direction would leave the
184 // end effector off target. With root fixed, the elbow bend alone puts
185 // the end at mid + elbowDelta*(end - mid); by the law of cosines its
186 // distance from the root is exactly lTgt, so aligning it with the
187 // target direction reaches any reachable target.
188 QVector3D newEnd = mid + elbowDelta.rotatedVector(m2e);
189 QVector3D r2t_n = r2t.normalized();
190 QVector3D r2e_n = (newEnd - root).normalized();
191 QQuaternion rootDelta = QQuaternion::rotationTo(r2e_n, r2t_n);
192
193 return {rootDelta, elbowDelta};
194 }
195
196 void operator()()
197 {
198 const auto& in = inputs.scene_in.scene;
199 if(!in.state || !in.state->roots)
200 {
201 outputs.scene_out.scene.state.reset();
202 outputs.scene_out.dirty = 0;
203 return;
204 }
205
206 // Find the skeleton: first skeleton_component referenced by any mesh.
207 const ossia::skeleton_component* srcSkel = nullptr;
208 if(in.state->skeletons && !in.state->skeletons->empty())
209 srcSkel = (*in.state->skeletons)[0].get();
210 if(!srcSkel || srcSkel->joints.empty())
211 {
212 outputs.scene_out.scene = in; // passthrough
213 outputs.scene_out.dirty = 0;
214 return;
215 }
216
217 const std::string endName = inputs.end_joint.value;
218 int32_t endIdx = srcSkel->find_joint(endName);
219 if(endIdx < 0 || srcSkel->joints[endIdx].parent_index < 0)
220 {
221 outputs.scene_out.scene = in;
222 outputs.scene_out.dirty = 0;
223 return;
224 }
225 const int32_t midIdx = srcSkel->joints[endIdx].parent_index;
226 if(srcSkel->joints[midIdx].parent_index < 0)
227 {
228 outputs.scene_out.scene = in;
229 outputs.scene_out.dirty = 0;
230 return;
231 }
232 const int32_t rootIdx = srcSkel->joints[midIdx].parent_index;
233
234 // Current world-space joint positions.
235 QVector3D wRoot = worldJointPos(*srcSkel, rootIdx);
236 QVector3D wMid = worldJointPos(*srcSkel, midIdx);
237 QVector3D wEnd = worldJointPos(*srcSkel, endIdx);
238
239 QVector3D target(
240 inputs.target.value.x, inputs.target.value.y, inputs.target.value.z);
241 QVector3D pole(
242 inputs.pole.value.x, inputs.pole.value.y, inputs.pole.value.z);
243
244 Solution sol = solve2Bone(wRoot, wMid, wEnd, target, pole);
245
246 // Blend by weight. At weight=0 the output scene is the input unchanged.
247 const float w = std::clamp(inputs.weight.value, 0.0f, 1.0f);
248 if(w <= 0.0f)
249 {
250 outputs.scene_out.scene = in;
251 outputs.scene_out.dirty = 0;
252 return;
253 }
254 QQuaternion rootDelta = QQuaternion::slerp(QQuaternion(), sol.rootDelta, w);
255 QQuaternion midDelta = QQuaternion::slerp(QQuaternion(), sol.midDelta, w);
256
257 // Copy the skeleton and mutate the two rotations. Keep other joints
258 // untouched so downstream animation / rendering sees a minimal diff.
259 auto newSkel = std::make_shared<ossia::skeleton_component>(*srcSkel);
260
261 // These deltas are in world space. Translate to local (parent-relative)
262 // rotation by undoing the parent's accumulated rotation.
263 auto worldRotOf = [&](int32_t idx) {
264 QQuaternion q;
265 for(int32_t i = idx; i >= 0; i = srcSkel->joints[i].parent_index)
266 {
267 QQuaternion local(
268 srcSkel->joints[i].rotation[3],
269 srcSkel->joints[i].rotation[0],
270 srcSkel->joints[i].rotation[1],
271 srcSkel->joints[i].rotation[2]);
272 q = local * q;
273 }
274 return q;
275 };
276 QQuaternion parentRoot = srcSkel->joints[rootIdx].parent_index >= 0
277 ? worldRotOf(srcSkel->joints[rootIdx].parent_index)
278 : QQuaternion();
279 QQuaternion parentMid = worldRotOf(rootIdx);
280
281 QQuaternion rootLocalNew
282 = parentRoot.inverted() * rootDelta * parentRoot
283 * toQuat(srcSkel->joints[rootIdx].rotation);
284 QQuaternion midLocalNew
285 = parentMid.inverted() * midDelta * parentMid
286 * toQuat(srcSkel->joints[midIdx].rotation);
287
288 fromQuat(newSkel->joints[rootIdx].rotation, rootLocalNew);
289 fromQuat(newSkel->joints[midIdx].rotation, midLocalNew);
290 newSkel->dirty_index++;
291
292 // Build the output scene_state — shallow copy of input, swap the
293 // skeletons vector to contain our mutated skeleton.
294 if(!m_state || m_state->version != in.state->version - 1)
295 m_state = std::make_shared<ossia::scene_state>(*in.state);
296 else
297 *m_state = *in.state;
298
299 auto skels = std::make_shared<std::vector<ossia::skeleton_component_ptr>>();
300 if(in.state->skeletons)
301 *skels = *in.state->skeletons;
302 if(skels->empty())
303 skels->push_back(newSkel);
304 else
305 (*skels)[0] = newSkel;
306 m_state->skeletons = std::move(skels);
307 m_version++;
308 m_state->version = m_version;
309
310 outputs.scene_out.scene.state = m_state;
311 outputs.scene_out.dirty = ossia::scene_port::dirty_transform;
312 }
313};
314
315}
Definition InverseKinematics.hpp:42
Definition InverseKinematics.hpp:132
Definition InverseKinematics.hpp:78