flutter3d
Showcase Changelog 38 packages API reference

Variants and animation pointers: the source

since 0.8.0 Formats and I/O

Regions the guide quotes: materials (line 33), variants (line 59), clip (line 76), roundtrip (line 108), bind (line 153), play (line 166), select (line 175), tick (line 182), check (line 212).

1/// A model that changes its look by name, and a clip that animates a material
2/// and a light, both written into a GLB and read back.
3///
4/// Quoted by `variants_and_pointers.md` and shown whole in the Source tab.
5library;
6
7import 'dart:typed_data';
8
9import 'package:flutter3d/flutter3d.dart';
10import 'package:flutter3d_showcase/src/demo/demo.dart';
11import 'package:vector_math/vector_math.dart';
13final class VariantsAndPointersDemo extends ShowcaseDemo {
14 /// 0 is the default look, then one option for each of the file's variants.
15 int variant = 1;
17 late final GltfAsset _readBack;
18 late final ModelAsset _asset;
19 late final ModelInstance _instance;
20 late final AnimationPlayer _player;
21 late final LightNode _sun;
23 @override
24 void configureView(DemoContext context) {
25 context.orbit
26 ..distance = 4.5
27 ..pitch = 0.35
28 ..yaw = 0.6;
29 }
31 @override
32 Future<void> prepare(DemoContext context) async {
33 // Five materials. The body wears the first and the ball the second
34 // unless a variant says otherwise; the other three are only ever worn
35 // through a variant.
36 final List<SurfaceMaterial> materials = <SurfaceMaterial>[
37 SurfaceMaterial(name: 'red paint', baseColor: Vector4(0.6, 0.2, 0.15, 1)),
38 SurfaceMaterial(
39 name: 'cream',
40 baseColor: Vector4(0.6, 0.55, 0.45, 1),
41 roughness: 0.6,
42 ),
43 SurfaceMaterial(
44 name: 'teal paint',
45 baseColor: Vector4(0.12, 0.45, 0.42, 1),
46 ),
47 SurfaceMaterial(
48 name: 'graphite',
49 baseColor: Vector4(0.2, 0.2, 0.22, 1),
50 roughness: 0.35,
51 ),
52 SurfaceMaterial(
53 name: 'chalk',
54 baseColor: Vector4(0.55, 0.55, 0.55, 1),
55 roughness: 0.8,
56 ),
57 ];
59 // `variantMaterials` maps a variant, by its index in `variants`, to the
60 // material the surface wears in it. The body changes in both variants;
61 // the ball only in "graphite".
62 final List<ModelSurface> surfaces = <ModelSurface>[
63 ModelSurface(
64 mesh: CuboidShape(size: Vector3(1.2, 0.8, 0.8)).build(),
65 materialIndex: 0,
66 variantMaterials: const <int, int>{0: 2, 1: 3},
67 ),
68 ModelSurface(
69 mesh: SphereShape(radius: 0.4, segments: 32, rings: 16).build(),
70 materialIndex: 1,
71 variantMaterials: const <int, int>{1: 4},
72 ),
73 ];
74 const List<String> variants = <String>['teal', 'graphite'];
76 // Two pointer tracks and no node track. One warms the ball's material
77 // (index 1) from cream to amber and back; the other dims the file's
78 // light (index 0) from 110 lux to 45 and back. Colour keys are linear,
79 // as glTF keeps them; the first is the cream above.
80 AnimationTrack track(AnimationPointer pointer, List<double> values) =>
81 AnimationTrack(
82 nodeIndex: -1,
83 path: AnimationPath.pointer,
84 interpolation: AnimationInterpolation.linear,
85 times: Float32List.fromList(<double>[0.0, 1.5, 3.0]),
86 values: Float32List.fromList(values),
87 componentCount: pointer.property.componentCount,
88 pointer: pointer,
89 );
90 final AnimationClip evening = AnimationClip(
91 name: 'evening',
92 tracks: <AnimationTrack>[
93 track(
94 AnimationPointer.of(AnimationPointerProperty.baseColor, 1),
95 <double>[
96 0.319, 0.262, 0.171, 1, //
97 0.32, 0.09, 0.02, 1, //
98 0.319, 0.262, 0.171, 1,
99 ],
100 ),
101 track(
102 AnimationPointer.of(AnimationPointerProperty.lightIntensity, 0),
103 <double>[110, 45, 110],
104 ),
105 ],
106 );
108 final PlainModelDocument document = PlainModelDocument(
109 surfaces: surfaces,
110 materials: materials,
111 lights: <ModelLight>[
112 ModelLight(type: ModelLightType.directional, intensity: 110),
113 ],
114 nodes: <ModelNode>[
115 ModelNode(
116 name: 'body',
117 surfaces: <int>[0],
118 translation: Vector3(0, 0.4, 0),
119 ),
120 ModelNode(
121 name: 'ball',
122 surfaces: <int>[1],
123 translation: Vector3(0, 1.2, 0),
124 ),
125 ModelNode(name: 'sun', lightIndex: 0),
126 ],
127 variants: variants,
128 animations: <AnimationClip>[evening],
129 );
130 final Uint8List bytes = GltfWriter(document).writeGlb();
131 _readBack = await GltfLoader().load(bytes);
132 _asset = await ModelAsset.fromDocument(_readBack, device: context.device);
133 }
135 @override
136 Scene build(DemoContext context) {
137 final Scene scene = Scene()
138 ..add(
139 MeshNode(
140 DeviceMesh.upload(
141 context.device,
142 const PlaneShape(width: 8, depth: 8).build(),
143 ),
144 Material(
145 name: 'floor',
146 baseColor: Vector4(0.45, 0.45, 0.45, 1.0),
147 roughness: 0.9,
148 ),
149 name: 'floor',
150 ),
151 );
153 // Instantiating creates no lights, so the page places its own sun and
154 // hands it to the instance as the file's light 0. The intensity track
155 // reaches it through `pointerTargets`; the material tracks reach the
156 // asset's materials the same way, by their index in the file.
157 _instance = _asset.instantiate(scene);
158 final ModelLight light = _readBack.lights.single;
159 _sun = LightNode(
160 name: 'sun',
161 color: light.color,
162 intensity: Photometric.fromLux(light.intensity),
163 )..setLocalForward(Vector3(-0.5, -0.8, -0.4));
164 _instance.bindLight(0, _sun);
166 // The page opens halfway through the clip, where both tracks are
167 // furthest from what the file says, and plays on from there.
168 _player = _instance.player!
169 ..play(0)
170 ..seek(1.5);
171 _wear(variant);
172 return scene..add(_sun);
173 }
175 void _wear(int option) {
176 variant = option;
177 _instance.selectVariant(option == 0 ? null : _asset.variants[option - 1]);
178 }
180 @override
181 void update(DemoContext context, double dt) {
182 _player.update(dt);
183 }
185 @override
186 List<DemoControl> controls(DemoContext context) => <DemoControl>[
187 ChoiceControl(
188 'Variant',
189 options: <String>['Default', ..._asset.variants],
190 index: () => variant,
191 onChanged: _wear,
192 ),
193 ToggleControl(
194 'Play clip',
195 value: () => _player.isPlaying,
196 onChanged: (bool v) => v ? _player.play() : _player.pause(),
197 ),
198 SliderControl(
199 'Time',
200 min: 0,
201 max: _player.duration,
202 value: () => _player.time,
203 onChanged: (double v) => _player
204 ..pause()
205 ..seek(v),
206 format: (double v) => '${v.toStringAsFixed(2)} s',
207 ),
208 ];
210 @override
211 void verify(Scene scene, FrameResult frame) {
212 // One mesh node a surface, in the order the nodes are walked: the body,
213 // then the ball. Each must point at the material the file gives it in
214 // the variant being worn, and that material is the asset's own.
215 final int worn = _instance.variant == null
216 ? -1
217 : _asset.variants.indexOf(_instance.variant!);
218 for (var i = 0; i < _readBack.surfaces.length; i++) {
219 final ModelSurface surface = _readBack.surfaces[i];
220 final int expected =
221 surface.variantMaterials[worn] ?? surface.materialIndex!;
222 if (!identical(
223 _instance.meshes[i].material,
224 _asset.materials[expected],
225 )) {
226 throw StateError(
227 'surface $i is not wearing material $expected, which '
228 '${_instance.variant ?? 'the default look'} gives it',
229 );
230 }
231 }
233 // The clip is past its first key, so both tracks have moved their
234 // targets away from what the file says.
235 final Vector4 authored = _readBack.materials[1].baseColor;
236 final Vector4 now = _asset.materials[1]!.baseColor;
237 if ((now.xyz - authored.xyz).length < 0.05) {
238 throw StateError("the colour track did not move the ball's material");
239 }
240 final double fileSun = Photometric.fromLux(_readBack.lights[0].intensity);
241 if ((_sun.intensity - fileSun).abs() < 0.1) {
242 throw StateError('the intensity track did not move the sun');
243 }
244 if (frame.drawCalls < 1) {
245 throw StateError('the model was not drawn');
246 }
247 }