Rigid bodies: the source
Open the live demo · Read the guide · View on GitHub
Regions the guide quotes: world (line 41), body (line 63), snapshot (line 104), live (line 114), shove (line 133), restore (line 142), check (line 168).
1/// Crates dropped one after another onto a floor: they land, stack, settle and
2/// fall asleep, and a snapshot taken before any of it winds the pile back to
3/// where it began. The physics runs live, a step to a frame.
4///
5/// Quoted by `rigid_bodies.md` and shown whole in the Source tab.
6library;
8import 'dart:math' as math;
10import 'package:flutter3d/flutter3d.dart';
11import 'package:flutter3d_physics/flutter3d_physics.dart';
12import 'package:flutter3d_showcase/src/demo/demo.dart';
13import 'package:vector_math/vector_math.dart';
15final class RigidBodiesDemo extends ShowcaseDemo {
16 late final CollisionWorld _world;
17 late final Dynamics _dynamics;
18 final List<RigidBody> _crates = <RigidBody>[];
19 final List<double> _halves = <double>[];
20 final List<MeshNode> _meshes = <MeshNode>[];
21 final List<Map<String, Object?>> _start = <Map<String, Object?>>[];
23 bool _shoveAsked = false;
24 double _atRest = 0.0;
25 double _age = 0.0;
27 static const int _count = 8;
28 static const double _step = 1 / 60;
31 void configureView(DemoContext context) {
32 context.orbit
33 ..distance = 8.0
34 ..pitch = 0.35
35 ..yaw = 0.6;
36 context.orbit.target.setValues(0.0, 1.0, 0.0);
37 }
40 Scene build(DemoContext context) {
41 _world = CollisionWorld();
42 _world.addBox(Vector3(0.0, -0.5, 0.0), Vector3(6.0, 1.0, 6.0));
43 _dynamics = Dynamics(world: _world);
45 final Scene scene = Scene()
46 ..ambientColor = Vector3(0.5, 0.55, 0.65)
47 ..ambientIntensity = 0.25
48 ..add(
49 MeshNode(
50 DeviceMesh.upload(
51 context.device,
52 CuboidShape(size: Vector3(6.0, 1.0, 6.0)).build(),
53 ),
54 Material(name: 'floor', baseColor: Vector4(0.42, 0.46, 0.43, 1.0)),
55 name: 'floor',
56 )..setPosition(0.0, -0.5, 0.0),
57 )
58 ..add(
59 LightNode(name: 'sun', intensity: 2.5)
60 ..setLocalForward(Vector3(-0.3, -0.6, -0.4)),
61 );
63 // Eight crates, each higher up and a little off to one side of the last,
64 // so they arrive one at a time and land on one another as often as on
65 // the floor.
66 for (var i = 0; i < _count; i++) {
67 final double half = 0.3 + 0.08 * (i % 3);
68 final RigidBody crate = _dynamics.add(
69 RigidBody(
70 world: _world,
71 shape: CollisionBox(Vector3(half, half, half)),
72 position: Vector3(
73 0.5 * math.sin(i * 2.4),
74 1.2 + i * 1.1,
75 0.5 * math.cos(i * 1.9),
76 ),
77 mass: 2.0,
78 friction: 0.7,
79 ),
80 );
81 _crates.add(crate);
82 _halves.add(half);
83 final MeshNode mesh = MeshNode(
84 DeviceMesh.upload(
85 context.device,
86 CuboidShape(size: Vector3.all(half * 2)).build(),
87 ),
88 Material(
89 name: 'crate $i',
90 baseColor: Vector4(
91 0.55 + 0.4 * math.sin(i * 0.9),
92 0.5 + 0.3 * math.sin(i * 1.7 + 1.0),
93 0.35 + 0.3 * math.sin(i * 2.3 + 2.0),
94 1.0,
95 ),
96 roughness: 0.7,
97 ),
98 name: 'crate $i',
99 )..setPositionFrom(crate.position);
100 _meshes.add(mesh);
101 scene.add(mesh);
102 }
104 // Taken before a single step: everything a body needs to be put back
105 // exactly here — position, velocity, whether it is asleep.
106 for (final RigidBody crate in _crates) {
107 _start.add(crate.save());
108 }
109 return scene;
110 }
113 void update(DemoContext context, double dt) {
114 _age += dt;
115 // A step of a sixtieth of a second, however long the frame took: the
116 // same drop lands the same way on every machine.
117 _dynamics.step(_step);
119 if (_shoveAsked) {
120 _shoveAsked = false;
121 _shove();
122 }
124 _atRest = _crates.every((RigidBody c) => c.isAsleep) ? _atRest + dt : 0.0;
125 // Watched for a moment at rest, then wound back and dropped again.
126 if (_atRest > 1.5 || _age > 12.0) _rewind();
128 for (var i = 0; i < _count; i++) {
129 _meshes[i].setPositionFrom(_crates[i].position);
130 }
131 }
133 void _shove() {
134 for (var i = 0; i < _count; i++) {
135 _crates[i].applyImpulse(
136 Vector3(math.sin(i * 2.1) * 3.0, 4.0, math.cos(i * 1.7) * 3.0),
137 );
138 }
139 _atRest = 0.0;
140 }
142 void _rewind() {
143 for (var i = 0; i < _count; i++) {
144 _crates[i].restore(_start[i]);
145 }
146 _atRest = 0.0;
147 _age = 0.0;
148 }
151 List<DemoControl> controls(DemoContext context) => <DemoControl>[
152 ToggleControl(
153 'Shove them',
154 value: () => _shoveAsked,
155 onChanged: (bool v) => _shoveAsked = v,
156 ),
157 ToggleControl(
158 'Rewind to the start',
159 value: () => false,
160 onChanged: (bool v) {
161 if (v) _rewind();
162 },
163 ),
164 ];
167 void verify(Scene scene, FrameResult frame) {
168 // Let the pile come to rest. Every crate has to end up on the floor or
169 // on another crate: none fallen through, none left hanging in the air.
170 for (var i = 0; i < 600; i++) {
171 _dynamics.step(_step);
172 }
173 for (var i = 0; i < _count; i++) {
174 final double y = _crates[i].position.y;
175 if (y < _halves[i] - 0.05) {
176 throw StateError('crate $i fell through the floor: y = $y');
177 }
178 if (y > 8.0) {
179 throw StateError('crate $i is still in the air: y = $y');
180 }
181 }
182 if (!_crates.every((RigidBody c) => c.isAsleep)) {
183 throw StateError('the pile should have come to rest and gone to sleep');
184 }
186 // Winding back has to undo all of it, exactly.
187 _rewind();
188 for (var i = 0; i < _count; i++) {
189 final Vector3 back = _crates[i].position;
190 final Vector3 saved = Vector3(
191 0.5 * math.sin(i * 2.4),
192 1.2 + i * 1.1,
193 0.5 * math.cos(i * 1.9),
194 );
195 if ((back - saved).length > 1e-6) {
196 throw StateError('restore should have put crate $i back where it was');
197 }
198 }
199 if (frame.drawCalls < 1) {
200 throw StateError('the crates did not reach the frame');
201 }
202 }
203}