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