flutter3d
Showcase Changelog 38 packages API reference

Cloth: the source

since 0.7.0 Physics and particles

Regions the guide quotes: cloth (line 45), live (line 140), check (line 174).

1/// A sheet pinned along its top edge, stepped live: a ball swings through it
2/// and a gust of wind comes and goes, and the sheet drapes, is pushed, and
3/// swings back under gravity.
4///
5/// Quoted by `xpbd_cloth.md` and shown whole in the Source tab.
6library;
7
8import 'dart:math' as math;
9import 'dart:typed_data';
11import 'package:flutter3d/flutter3d.dart';
12import 'package:flutter3d_physics/flutter3d_physics.dart';
13import 'package:flutter3d_showcase/src/demo/demo.dart';
14import 'package:vector_math/vector_math.dart';
16final class XpbdClothDemo extends ShowcaseDemo {
17 double wind = 1.5;
18 bool swingBall = true;
20 late final ClothMesh _cloth;
21 late final ClothObstacle _obstacle;
22 late final MeshNode _sheet;
23 late final MeshNode _ball;
24 late final DeviceMesh Function() _upload;
25 double _clock = 0.0;
27 static const int _cols = 16;
28 static const int _rows = 16;
29 static const double _spacing = 0.09;
30 static const double _hangs = 1.7;
31 static const double _ballRadius = 0.2;
32 static const double _step = 1 / 60;
34 @override
35 void configureView(DemoContext context) {
36 context.orbit
37 ..distance = 3.0
38 ..pitch = 0.15
39 ..yaw = 0.7;
40 context.orbit.target.setValues(0.68, 1.0, 0.0);
41 }
43 @override
44 Scene build(DemoContext context) {
45 // Row zero is pinned, which is what stops the sheet falling forever:
46 // everything else hangs from it.
47 _cloth = ClothMesh.grid(
48 cols: _cols,
49 rows: _rows,
50 spacing: _spacing,
51 height: _hangs,
52 );
53 _obstacle = ClothObstacle(
54 CollisionSphere(_ballRadius),
55 Vector3(0.68, 1.0, 0.6),
56 );
58 _upload = () => DeviceMesh.upload(context.device, _mesh());
59 _sheet = MeshNode(
60 _upload(),
61 Material(
62 name: 'cloth',
63 baseColor: Vector4(0.8, 0.3, 0.35, 1.0),
64 roughness: 0.8,
65 doubleSided: true,
66 ),
67 name: 'cloth',
68 );
69 _ball = MeshNode(
70 DeviceMesh.upload(
71 context.device,
72 SphereShape(segments: 24, rings: 12, radius: _ballRadius).build(),
73 ),
74 Material(name: 'ball', baseColor: Vector4(0.4, 0.5, 0.7, 1.0)),
75 name: 'ball',
76 )..setPositionFrom(_obstacle.position);
78 return Scene()
79 ..ambientColor = Vector3(0.5, 0.55, 0.65)
80 ..ambientIntensity = 0.3
81 ..add(_sheet)
82 ..add(_ball)
83 ..add(
84 LightNode(name: 'sun', intensity: 2.5)
85 ..setLocalForward(Vector3(-0.3, -0.5, -0.6)),
86 );
87 }
89 /// A mesh from the particle positions and the triangles `ClothMesh.grid`
90 /// laid out for them, with a normal at each particle from the triangles
91 /// that meet there. Nothing here draws a `ClothMesh` for you; that is the
92 /// same gap the heightfield page's own terrain fills.
93 MeshData _mesh() {
94 final Float64List p = _cloth.positions;
95 final List<Vector3> normals = List<Vector3>.generate(
96 _cloth.particleCount,
97 (int _) => Vector3.zero(),
98 );
99 Vector3 at(int i) => Vector3(p[3 * i], p[3 * i + 1], p[3 * i + 2]);
100 for (var i = 0; i + 2 < _cloth.triangles.length; i += 3) {
101 final int a = _cloth.triangles[i];
102 final int b = _cloth.triangles[i + 1];
103 final int c = _cloth.triangles[i + 2];
104 final Vector3 face = (at(b) - at(a)).cross(at(c) - at(a));
105 normals[a].add(face);
106 normals[b].add(face);
107 normals[c].add(face);
108 }
109 final MeshBuilder builder = MeshBuilder(VertexLayout.standard);
110 for (var i = 0; i < _cloth.particleCount; i++) {
111 builder.addVertex(
112 position: at(i),
113 normal: normals[i].length2 == 0.0
114 ? Vector3(0.0, 0.0, 1.0)
115 : (normals[i]..normalize()),
116 );
117 }
118 for (var i = 0; i + 2 < _cloth.triangles.length; i += 3) {
119 builder.addTriangle(
120 _cloth.triangles[i],
121 _cloth.triangles[i + 1],
122 _cloth.triangles[i + 2],
123 );
124 }
125 return builder.build();
126 }
128 @override
129 void update(DemoContext context, double dt) {
130 _clock += _step;
131 if (swingBall) {
132 // Through the sheet and out the other side, and around again.
133 _obstacle.position.setValues(
134 0.68 + 0.3 * math.sin(_clock * 0.9),
135 1.0 + 0.15 * math.sin(_clock * 1.3),
136 0.55 * math.sin(_clock * 1.1),
137 );
138 }
140 // A gust that rises and falls, blowing the sheet towards the viewer.
141 final double gust = wind * (0.5 + 0.5 * math.sin(_clock * 0.7));
142 stepCloth(
143 _cloth,
144 // Drag 4, not 0.3: until 0.7.4 the wind was added as a velocity, which
145 // made 0.3 several hundred times stronger than its value.
146 ClothSettings(wind: WindSettings(velocityZ: gust, drag: 4.0)),
147 _step,
148 obstacles: <ClothObstacle>[_obstacle],
149 );
151 _ball.setPositionFrom(_obstacle.position);
152 _sheet.mesh = _upload();
153 }
155 @override
156 List<DemoControl> controls(DemoContext context) => <DemoControl>[
157 SliderControl(
158 'Wind',
159 min: 0,
160 max: 6,
161 value: () => wind,
162 onChanged: (double v) => wind = v,
163 format: (double v) => v.toStringAsFixed(1),
164 ),
165 ToggleControl(
166 'Swing the ball',
167 value: () => swingBall,
168 onChanged: (bool v) => swingBall = v,
169 ),
170 ];
172 @override
173 void verify(Scene scene, FrameResult frame) {
174 // The pinned row never moves: its inverse mass is zero, so no force and
175 // no correction ever reaches it.
176 if (_cloth.positions[1] != _hangs) {
177 throw StateError('the pinned row should not have moved');
178 }
179 // Left alone with the ball out of the way, the free edge has to sag
180 // well below the row it hangs from.
181 _obstacle.position.setValues(5.0, 5.0, 5.0);
182 for (var i = 0; i < 300; i++) {
183 stepCloth(_cloth, const ClothSettings(), _step);
184 }
185 final int lastRow = (_rows - 1) * _cols;
186 for (var col = 0; col < _cols; col++) {
187 final double y = _cloth.positions[3 * (lastRow + col) + 1];
188 if (y > _hangs - 0.5 * (_rows - 1) * _spacing) {
189 throw StateError('the free edge should have hung below its start');
190 }
191 }
192 // And a ball pushed into the sheet must never end up inside it.
193 _obstacle.position.setValues(0.68, 1.0, 0.0);
194 for (var i = 0; i < 120; i++) {
195 stepCloth(
196 _cloth,
197 const ClothSettings(),
198 _step,
199 obstacles: <ClothObstacle>[_obstacle],
200 );
201 }
202 for (var i = 0; i < _cloth.particleCount; i++) {
203 final Vector3 at = Vector3(
204 _cloth.positions[3 * i],
205 _cloth.positions[3 * i + 1],
206 _cloth.positions[3 * i + 2],
207 );
208 if (at.distanceTo(_obstacle.position) < _ballRadius - 0.03) {
209 throw StateError('a particle passed through the ball');
210 }
211 }
212 if (frame.drawCalls < 2) {
213 throw StateError('the cloth and the ball did not both reach the frame');
214 }
215 }