flutter3d
Showcase Changelog 38 packages API reference

Smoke lit by the scene: the source

since 0.8.0 Physics and particles

Regions the guide quotes: upload (line 40), puffs (line 53), lights (line 59), orbit (line 107), swap (line 117), check (line 159), bake (line 168).

1/// Three puffs of smoke between a red light and a blue one, each side of each
2/// puff taking the light on that side through a six-way sheet.
3///
4/// Quoted by `six_way_smoke.md` and shown whole in the Source tab.
5library;
6
7import 'dart:math' as math;
8import 'dart:typed_data';
9
10import 'package:flutter3d/flutter3d.dart';
11import 'package:flutter3d_particles/flutter3d_particles.dart';
12import 'package:flutter3d_showcase/src/demo/demo.dart';
13import 'package:vector_math/vector_math.dart';
15final class SixWaySmokeDemo extends ShowcaseDemo {
16 bool sixWay = true;
17 double angle = 0.0;
18 double intensity = 12.0;
19 double ambient = 0.0;
21 late final ParticleSystem _particles;
22 late final SixWayMaterial _sheet;
23 late final ParticleContributor _lit;
24 late final ParticleContributor _plain;
25 late final LightNode _red;
26 late final LightNode _blue;
28 static const int _cell = 24;
30 @override
31 void configureView(DemoContext context) {
32 context.orbit
33 ..distance = 4.0
34 ..pitch = 0.0
35 ..yaw = 0.0;
36 }
38 @override
39 Scene build(DemoContext context) {
40 final ({Uint8List positive, Uint8List negative}) baked = _bakeBall(_cell);
41 TextureHandle upload(Uint8List bytes) =>
42 context.device.createTextureFromPixels(
43 width: _cell,
44 height: _cell,
45 format: TextureFormat.r8g8b8a8UNormInt,
46 pixels: ByteData.sublistView(bytes),
47 )!;
48 _sheet = SixWayMaterial(
49 positive: upload(baked.positive),
50 negative: upload(baked.negative),
51 );
53 _particles = ParticleSystem(capacity: 8);
54 _burst();
55 _lit = ParticleContributor(_particles, sixWay: _sheet);
56 _plain = ParticleContributor(_particles);
57 context.renderer.addContributor(_lit);
59 LightNode point(Vector3 colour) =>
60 LightNode(type: LightType.point, intensity: intensity, color: colour)
61 ..castsShadow = false;
62 _red = point(Vector3(1.0, 0.2, 0.1));
63 _blue = point(Vector3(0.1, 0.2, 1.0));
64 final Scene scene = Scene()
65 ..ambientIntensity = 0.0
66 ..defaultLightWhenUnlit = false
67 ..add(_red)
68 ..add(_blue);
70 // A dark wall far behind, so the frame has something with bounds.
71 return scene..add(
72 MeshNode(
73 DeviceMesh.upload(
74 context.device,
75 CuboidShape(size: Vector3(40.0, 40.0, 0.1)).build(),
76 ),
77 Material(
78 lighting: LightingModel.unlit,
79 baseColor: Vector4(0.04, 0.04, 0.05, 1.0),
80 ),
81 name: 'backdrop',
82 )..setPosition(0.0, 0.0, -6.0),
83 );
84 }
86 /// Three puffs in a row that stand still for eight seconds.
87 void _burst() {
88 for (final double x in <double>[-0.4, 0.0, 0.4]) {
89 _particles.burst(
90 ParticleEffect(
91 count: 1,
92 emitter: const SphereEmitter(speed: Range.exact(0.0)),
93 lifetime: const Range.exact(8.0),
94 size: const Range.exact(1.6),
95 color: Vector4(1.0, 1.0, 1.0, 1.0),
96 ),
97 Vector3(x, 0.0, x * 0.5),
98 );
99 }
100 }
102 @override
103 void update(DemoContext context, double dt) {
104 _particles.advance(dt);
105 if (_particles.aliveCount == 0) _burst();
107 final double c = math.cos(angle) * 3.0;
108 final double s = math.sin(angle) * 3.0;
109 _red
110 ..setPosition(-c, 0.0, -s)
111 ..intensity = intensity;
112 _blue
113 ..setPosition(c, 0.0, s)
114 ..intensity = intensity;
115 _sheet.ambient.setValues(ambient, ambient, ambient);
117 final ParticleContributor shown = sixWay ? _lit : _plain;
118 final ParticleContributor hidden = sixWay ? _plain : _lit;
119 if (context.renderer.removeContributor(hidden)) {
120 context.renderer.addContributor(shown);
121 }
122 }
124 @override
125 List<DemoControl> controls(DemoContext context) => <DemoControl>[
126 ToggleControl(
127 'Six-way sheet',
128 value: () => sixWay,
129 onChanged: (bool v) => sixWay = v,
130 ),
131 SliderControl(
132 'Lights around the puffs',
133 min: 0.0,
134 max: 2.0 * math.pi,
135 value: () => angle,
136 onChanged: (double v) => angle = v,
137 format: (double v) => '${(v * 180.0 / math.pi).round()} deg',
138 ),
139 SliderControl(
140 'Light intensity',
141 min: 0.0,
142 max: 24.0,
143 value: () => intensity,
144 onChanged: (double v) => intensity = v,
145 format: (double v) => v.toStringAsFixed(1),
146 ),
147 SliderControl(
148 'Ambient',
149 min: 0.0,
150 max: 0.3,
151 value: () => ambient,
152 onChanged: (double v) => ambient = v,
153 format: (double v) => v.toStringAsFixed(2),
154 ),
155 ];
157 @override
158 void verify(Scene scene, FrameResult frame) {
159 if (_particles.aliveCount != 3 || !_lit.isActive) {
160 throw StateError('the three puffs are not alive');
161 }
162 if (frame.drawCalls < 1) {
163 throw StateError('the frame drew nothing');
164 }
165 }
168/// A six-way sheet of one frame, [n] pixels square, of a soft ball of smoke.
169///
170/// Single scattering along the six axes: the light reaching a voxel from one
171/// side is the density summed along its row to that side's face, and each
172/// pixel marches once from the viewer through its column. The layout is the
173/// one `SixWayMaterial` reads: positive holds right, top, back and coverage,
174/// negative holds left, bottom, front and emission, unpremultiplied, and the
175/// rows run bottom to top.
176({Uint8List positive, Uint8List negative}) _bakeBall(int n) {
177 const double extinction = 6.0;
178 final double step = 2.0 / n;
179 double at(int i) => -1.0 + (i + 0.5) * step;
180 int index(int x, int y, int z) => (z * n + y) * n + x;
182 final Float64List sigma = Float64List(n * n * n);
183 for (var z = 0; z < n; z++) {
184 for (var y = 0; y < n; y++) {
185 for (var x = 0; x < n; x++) {
186 final double r =
187 math.sqrt(at(x) * at(x) + at(y) * at(y) + at(z) * at(z)) / 0.8;
188 sigma[index(x, y, z)] = extinction * ((1.0 - r) * 2.5).clamp(0.0, 1.0);
189 }
190 }
191 }
193 // How much the smoke takes out between each voxel and the face each light
194 // comes in through: right, left, top, bottom, back (from -z), front.
195 final List<Float64List> depth = List<Float64List>.generate(
196 6,
197 (_) => Float64List(n * n * n),
198 );
199 void sweep(Float64List into, int Function(int a, int b, int s) voxel) {
200 for (var a = 0; a < n; a++) {
201 for (var b = 0; b < n; b++) {
202 var total = 0.0;
203 for (var s = 0; s < n; s++) {
204 final int i = voxel(a, b, s);
205 into[i] = total + sigma[i] * step * 0.5;
206 total += sigma[i] * step;
207 }
208 }
209 }
210 }
212 sweep(depth[0], (int y, int z, int s) => index(n - 1 - s, y, z));
213 sweep(depth[1], (int y, int z, int s) => index(s, y, z));
214 sweep(depth[2], (int x, int z, int s) => index(x, n - 1 - s, z));
215 sweep(depth[3], (int x, int z, int s) => index(x, s, z));
216 sweep(depth[4], (int x, int y, int s) => index(x, y, s));
217 sweep(depth[5], (int x, int y, int s) => index(x, y, n - 1 - s));
219 int byte(double v) => (v.clamp(0.0, 1.0) * 255.0 + 0.5).floor();
220 final Uint8List positive = Uint8List(n * n * 4);
221 final Uint8List negative = Uint8List(n * n * 4);
222 final Float64List lit = Float64List(6);
223 for (var y = 0; y < n; y++) {
224 for (var x = 0; x < n; x++) {
225 lit.fillRange(0, 6, 0.0);
226 var through = 1.0;
227 for (var z = n - 1; z >= 0; z--) {
228 final int i = index(x, y, z);
229 final double absorbed = 1.0 - math.exp(-sigma[i] * step);
230 final double weight = through * absorbed;
231 for (var d = 0; d < 6; d++) {
232 lit[d] += weight * math.exp(-depth[d][i]);
233 }
234 through *= 1.0 - absorbed;
235 }
236 final double coverage = 1.0 - through;
237 final double scale = coverage > 1e-4 ? 1.0 / coverage : 0.0;
238 final int out = (y * n + x) * 4;
239 positive
240 ..[out] = byte(lit[0] * scale)
241 ..[out + 1] = byte(lit[2] * scale)
242 ..[out + 2] = byte(lit[4] * scale)
243 ..[out + 3] = byte(coverage);
244 negative
245 ..[out] = byte(lit[1] * scale)
246 ..[out + 1] = byte(lit[3] * scale)
247 ..[out + 2] = byte(lit[5] * scale)
248 ..[out + 3] = 0;
249 }
250 }
251 return (positive: positive, negative: negative);