Two-bone IK: the source
Open the live demo · Read the guide · View on GitHub
Regions the guide quotes: pose (line 37), solve (line 112).
1/// An arm bent to reach a point: `TwoBoneIk` solves on a `Pose` alone, with
2/// no scene behind it, and only afterwards is the result written onto real
3/// nodes so this page has something to draw.
4///
5/// Quoted by `two_bone_ik.md` and shown whole in the Source tab.
6library;
8import 'dart:typed_data';
10import 'package:flutter3d/flutter3d.dart';
11import 'package:flutter3d_showcase/src/demo/demo.dart';
12import 'package:vector_math/vector_math.dart';
14final class TwoBoneIkDemo extends ShowcaseDemo {
15 double reach = 0.5;
17 late final Pose _pose;
18 late final SceneNode _shoulder;
19 late final SceneNode _elbow;
20 late final SceneNode _wrist;
21 late final MeshNode _targetMarker;
22 double _lastError = 0.0;
24 static const double _boneLength = 1.0;
25 static Vector3 get _pole => Vector3(1.0, 0.0, 0.0);
28 void configureView(DemoContext context) {
29 context.orbit
30 ..distance = 5.0
31 ..pitch = 0.2
32 ..yaw = 0.3;
33 }
36 Scene build(DemoContext context) {
37 _pose = Pose(
38 parents: <int>[-1, 0, 1],
39 restTranslations: Float32List.fromList(<double>[
40 0.0, 0.0, 0.0, // shoulder
41 0.0, -_boneLength, 0.0, // elbow, relative to the shoulder
42 0.0, -_boneLength, 0.0, // wrist, relative to the elbow
43 ]),
44 restRotations: Float32List.fromList(<double>[
45 0.0,
46 0.0,
47 0.0,
48 1.0,
49 0.0,
50 0.0,
51 0.0,
52 1.0,
53 0.0,
54 0.0,
55 0.0,
56 1.0,
57 ]),
58 restScales: Float32List.fromList(<double>[
59 1.0,
60 1.0,
61 1.0,
62 1.0,
63 1.0,
64 1.0,
65 1.0,
66 1.0,
67 1.0,
68 ]),
69 );
71 final DeviceMesh joint = DeviceMesh.upload(
72 context.device,
73 SphereShape(radius: 0.12, segments: 16, rings: 8).build(),
74 );
75 _shoulder = MeshNode(
76 joint,
77 Material(name: 'shoulder', baseColor: Vector4(0.6, 0.6, 0.65, 1.0)),
78 name: 'shoulder',
79 );
80 _elbow = MeshNode(
81 joint,
82 Material(name: 'elbow', baseColor: Vector4(0.6, 0.6, 0.65, 1.0)),
83 name: 'elbow',
84 );
85 _wrist = MeshNode(
86 joint,
87 Material(name: 'wrist', baseColor: Vector4(0.9, 0.5, 0.2, 1.0)),
88 name: 'wrist',
89 );
90 _shoulder.add(_elbow);
91 _elbow.add(_wrist);
92 _targetMarker = MeshNode(
93 DeviceMesh.upload(
94 context.device,
95 SphereShape(radius: 0.08, segments: 12, rings: 6).build(),
96 ),
97 Material(name: 'target', baseColor: Vector4(0.9, 0.2, 0.2, 1.0)),
98 name: 'target',
99 );
101 return Scene()
102 ..add(_shoulder)
103 ..add(_targetMarker)
104 ..add(
105 LightNode(name: 'sun', intensity: 3.0)
106 ..setLocalForward(Vector3(-0.4, -0.7, -0.5)),
107 );
108 }
111 void update(DemoContext context, double dt) {
112 final double maxReach = _boneLength * 2;
113 final Vector3 direction = Vector3(0.6, -0.7, 0.3)..normalize();
114 final Vector3 target = direction * (reach * maxReach * 0.8);
116 _pose.resetToRest();
117 _lastError = TwoBoneIk.solve(
118 _pose,
119 root: 0,
120 mid: 1,
121 tip: 2,
122 target: target,
123 pole: _pole,
124 );
125 _pose.writeTo(<SceneNode?>[_shoulder, _elbow, _wrist]);
126 _targetMarker.setPositionFrom(target);
127 }
130 List<DemoControl> controls(DemoContext context) => <DemoControl>[
131 SliderControl(
132 'Reach',
133 min: 0,
134 max: 1,
135 value: () => reach,
136 onChanged: (double v) => reach = v,
137 ),
138 ];
141 void verify(Scene scene, FrameResult frame) {
142 if (_lastError > 1e-3) {
143 throw StateError(
144 'the wrist did not reach the target; missed by $_lastError',
145 );
146 }
147 }
148}