Additive layers: the source
Open the live demo · Read the guide · View on GitHub
Regions the guide quotes: clips (line 77), additive (line 168), live (line 186), check (line 232).
1/// Three heads turn at the same steady rate. Every couple of seconds the
2/// middle and right ones are nudged with the same quick nod: laid on top of
3/// the turn additively on the middle head, replacing it on the right one —
4/// `AnimationBlend.additive` and `AnimationClip.referenceTime`.
5///
6/// Quoted by `additive_blend.md` and shown whole in the Source tab.
7library;
9import 'dart:math' as math;
10import 'dart:typed_data';
12import 'package:flutter3d/flutter3d.dart';
13import 'package:flutter3d_showcase/src/demo/demo.dart';
14import 'package:vector_math/vector_math.dart';
16final class AdditiveBlendDemo extends ShowcaseDemo {
17 late final AnimationTrack _turnTrack;
18 late final AnimationTrack _nodTrack;
19 late final AnimationPlayer _plain;
20 late final AnimationPlayer _additive;
21 late final AnimationPlayer _override;
22 late final MeshNode _additiveHead;
23 late final MeshNode _overrideHead;
24 AnimationLayer? _additiveNod;
25 AnimationLayer? _overrideNod;
27 double turnSpeed = 1.0;
28 double _sinceNod = 0.0;
30 /// How often the nod is played again, in seconds of the demo's own clock.
31 static const double _period = 2.2;
34 void configureView(DemoContext context) {
35 context.orbit
36 ..distance = 7.0
37 ..pitch = 0.25
38 ..yaw = 0.0;
39 }
42 Scene build(DemoContext context) {
43 final DeviceMesh box = DeviceMesh.upload(
44 context.device,
45 CuboidShape(size: Vector3(1.0, 1.0, 1.0)).build(),
46 );
47 final DeviceMesh nose = DeviceMesh.upload(
48 context.device,
49 CuboidShape(size: Vector3(0.3, 0.3, 0.5)).build(),
50 );
51 MeshNode head(String name, Vector4 colour, double x) {
52 final MeshNode node = MeshNode(
53 box,
54 Material(name: name, baseColor: colour, roughness: 0.6),
55 name: name,
56 )..setPosition(x, 0.0, 0.0);
57 // A face, so a turn and a nod each read as one: a plain cube looks
58 // the same after a quarter turn.
59 node.add(
60 MeshNode(
61 nose,
62 Material(name: '$name nose', baseColor: Vector4(0.95, 0.9, 0.8, 1.0)),
63 name: '$name nose',
64 )..setPosition(0.0, 0.0, 0.7),
65 );
66 return node;
67 }
69 final MeshNode plainHead = head(
70 'turn only',
71 Vector4(0.6, 0.6, 0.65, 1),
72 -2.2,
73 );
74 _additiveHead = head('additive', Vector4(0.35, 0.75, 0.45, 1), 0.0);
75 _overrideHead = head('override', Vector4(0.9, 0.5, 0.3, 1), 2.2);
77 _turnTrack = _turnAroundY();
78 final AnimationClip turn = AnimationClip(
79 name: 'turn',
80 tracks: <AnimationTrack>[_turnTrack],
81 );
82 _nodTrack = _nodAroundX();
83 final AnimationClip nod = AnimationClip(
84 name: 'nod',
85 tracks: <AnimationTrack>[_nodTrack],
86 // The rest pose the nod is a difference from: at time zero it asks
87 // for nothing, so laid on top of a turn it leaves the turn alone.
88 referenceTime: 0.0,
89 );
91 AnimationPlayer playerFor(MeshNode node) => AnimationPlayer(
92 clips: <AnimationClip>[turn, nod],
93 targets: <AnimationTarget?>[node],
94 )..play(0);
95 _plain = playerFor(plainHead);
96 _additive = playerFor(_additiveHead);
97 _override = playerFor(_overrideHead);
99 _nodBoth();
100 // Start a fifth of a second in, with the nod at its deepest, so the
101 // first frame already shows the difference between the two blends.
102 for (final AnimationPlayer player in <AnimationPlayer>[
103 _plain,
104 _additive,
105 _override,
106 ]) {
107 player.update(0.2);
108 }
109 _sinceNod = 0.2;
111 return Scene()
112 ..add(plainHead)
113 ..add(_additiveHead)
114 ..add(_overrideHead)
115 ..add(
116 LightNode(name: 'sun', intensity: 3.0)
117 ..setLocalForward(Vector3(-0.3, -0.7, -0.5)),
118 );
119 }
121 /// A full turn about Y in quarter turns, a keyframe each: a single
122 /// keyframe pair from zero to a full turn would interpolate along the
123 /// shortest path, which is no turn at all.
124 static AnimationTrack _turnAroundY() {
125 const int keys = 5;
126 final Float32List values = Float32List(keys * 4);
127 for (var i = 0; i < keys; i++) {
128 final Quaternion q = Quaternion.axisAngle(
129 Vector3(0.0, 1.0, 0.0),
130 i * math.pi / 2,
131 );
132 values.setRange(i * 4, i * 4 + 4, <double>[q.x, q.y, q.z, q.w]);
133 }
134 return AnimationTrack(
135 nodeIndex: 0,
136 path: AnimationPath.rotation,
137 interpolation: AnimationInterpolation.linear,
138 times: Float32List.fromList(<double>[0.0, 1.5, 3.0, 4.5, 6.0]),
139 values: values,
140 componentCount: 4,
141 );
142 }
144 /// Nothing, a nod forward, nothing again, over four tenths of a second.
145 static AnimationTrack _nodAroundX() {
146 final List<double> angles = <double>[0.0, 0.7, 0.0];
147 final Float32List values = Float32List(angles.length * 4);
148 for (var i = 0; i < angles.length; i++) {
149 final Quaternion q = Quaternion.axisAngle(
150 Vector3(1.0, 0.0, 0.0),
151 angles[i],
152 );
153 values.setRange(i * 4, i * 4 + 4, <double>[q.x, q.y, q.z, q.w]);
154 }
155 return AnimationTrack(
156 nodeIndex: 0,
157 path: AnimationPath.rotation,
158 interpolation: AnimationInterpolation.linear,
159 times: Float32List.fromList(<double>[0.0, 0.2, 0.4]),
160 values: values,
161 componentCount: 4,
162 );
163 }
165 /// Plays the nod once on the two heads that get one, each in its own blend
166 /// mode, taking away the previous nod first.
167 void _nodBoth() {
168 if (_additiveNod != null) _additive.layers.remove(_additiveNod);
169 _additiveNod = _additive.playLayer(
170 1,
171 wrap: AnimationWrap.once,
172 fadeIn: 0.0,
173 blend: AnimationBlend.additive,
174 );
175 if (_overrideNod != null) _override.layers.remove(_overrideNod);
176 _overrideNod = _override.playLayer(
177 1,
178 wrap: AnimationWrap.once,
179 fadeIn: 0.0,
180 blend: AnimationBlend.override,
181 );
182 }
185 void update(DemoContext context, double dt) {
186 _sinceNod += dt;
187 if (_sinceNod >= _period) {
188 _sinceNod -= _period;
189 _nodBoth();
190 }
191 for (final AnimationPlayer player in <AnimationPlayer>[
192 _plain,
193 _additive,
194 _override,
195 ]) {
196 player.speed = turnSpeed;
197 }
198 _plain.update(dt);
199 _additive.update(dt);
200 _override.update(dt);
201 // A finished override layer would go on holding its last pose, which is
202 // the head facing dead ahead: taking it off is what lets the turn resume.
203 final AnimationLayer? finished = _overrideNod;
204 if (finished != null && finished.isFinished) {
205 _override.layers.remove(finished);
206 _overrideNod = null;
207 }
208 }
211 List<DemoControl> controls(DemoContext context) => <DemoControl>[
212 SliderControl(
213 'Turn speed',
214 min: 0,
215 max: 2,
216 value: () => turnSpeed,
217 onChanged: (double v) => turnSpeed = v,
218 ),
219 ];
221 Quaternion _sample(AnimationTrack track, double time) {
222 final Float32List out = Float32List(4);
223 track.sample(time, out);
224 return Quaternion(out[0], out[1], out[2], out[3]);
225 }
227 static double _dot(Quaternion a, Quaternion b) =>
228 (a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w).abs();
231 void verify(Scene scene, FrameResult frame) {
232 final AnimationLayer? nod = _additiveNod;
233 if (nod == null) throw StateError('the additive head has no nod playing');
235 final Quaternion turn = _sample(_turnTrack, _additive.time);
236 final Quaternion delta =
237 (_sample(_nodTrack, 0.0).conjugated() * _sample(_nodTrack, nod.time))
238 ..normalize();
239 if ((delta.w - 1.0).abs() < 1e-3) {
240 throw StateError('the nod has not moved yet; nothing to add');
241 }
243 Quaternion rotationOf(MeshNode head) =>
244 Quaternion.fromRotation(head.worldMatrix.getRotation());
245 final Quaternion added = rotationOf(_additiveHead);
246 if (_dot(added, (turn * delta)..normalize()) < 0.999) {
247 throw StateError('the middle head is not the turn with the nod on top');
248 }
249 if (_dot(added, rotationOf(_overrideHead)) > 0.999) {
250 throw StateError(
251 'the right head should have had its turn replaced by the nod, and '
252 'looks the same as the one that kept it',
253 );
254 }
255 if (frame.drawCalls < 1) throw StateError('the heads were not drawn');
256 }
257}