Textured billboards: the source
Open the live demo · Read the guide · View on GitHub
Regions the guide quotes: sprite (line 36), texture (line 59), contributor (line 76), check (line 101).
1/// A sprite on every billboard instead of the procedural disc, which is what
2/// turns a particle into any shape you can paint.
3///
4/// Quoted by `textured_particles.md` and shown whole in the Source tab.
5library;
7import 'dart:math' as math;
8import 'dart:typed_data';
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 TexturedParticlesDemo extends ShowcaseDemo {
16 late final ParticleSystem _particles;
17 late final ParticleEffect _sparksEffect;
18 late final ParticleContributor _contributor;
20 static const int _size = 32;
21 static const int _count = 50;
23 // The sprite is the whole point of the page; a burst that never repeated
24 // would leave nothing wearing it after the first second.
25 static const double _pause = 1.0;
26 double _cooldown = 0.0;
29 void configureView(DemoContext context) {
30 context.orbit
31 ..distance = 4.0
32 ..pitch = 0.2
33 ..yaw = 0.4;
34 }
36 /// A four-pointed spark: bright along the axes, dark at the corners. Not a
37 /// shape the procedural disc every untextured particle uses can make.
38 ByteData _sparkSprite() {
39 final Uint8List bytes = Uint8List(_size * _size * 4);
40 for (var y = 0; y < _size; y++) {
41 for (var x = 0; x < _size; x++) {
42 final double dx = (x + 0.5) / _size * 2.0 - 1.0;
43 final double dy = (y + 0.5) / _size * 2.0 - 1.0;
44 final double along = math.max(dx.abs(), dy.abs());
45 final double across = math.min(dx.abs(), dy.abs());
46 final double alpha = (1.0 - along) * (1.0 - across * 3.0);
47 final int at = (y * _size + x) * 4;
48 bytes[at] = 255;
49 bytes[at + 1] = 220;
50 bytes[at + 2] = 160;
51 bytes[at + 3] = (alpha.clamp(0.0, 1.0) * 255).round();
52 }
53 }
54 return bytes.buffer.asByteData();
55 }
58 Scene build(DemoContext context) {
59 final TextureHandle sprite = context.device.createTextureFromPixels(
60 width: _size,
61 height: _size,
62 format: TextureFormat.r8g8b8a8UNormInt,
63 pixels: _sparkSprite(),
64 )!;
66 _particles = ParticleSystem(capacity: _count, seed: 909);
67 _sparksEffect = ParticleEffect(
68 count: _count,
69 emitter: const SphereEmitter(speed: Range(1.0, 2.0)),
70 lifetime: const Range(0.8, 1.4),
71 size: const Range(0.1, 0.16),
72 color: Vector4(1.0, 1.0, 1.0, 1.0),
73 );
74 _particles.burst(_sparksEffect, Vector3.zero());
76 _contributor = context.renderer.addContributor(
77 ParticleContributor(_particles, texture: sprite),
78 );
80 return Scene()..add(
81 LightNode(name: 'sun', intensity: 2.0)
82 ..setLocalForward(Vector3(-0.3, -0.6, -0.4)),
83 );
84 }
87 void update(DemoContext context, double dt) {
88 _particles.advance(dt);
90 if (_particles.aliveCount == 0) {
91 _cooldown -= dt;
92 if (_cooldown <= 0.0) {
93 _particles.burst(_sparksEffect, Vector3.zero());
94 _cooldown = _pause;
95 }
96 }
97 }
100 void verify(Scene scene, FrameResult frame) {
101 if (_contributor.texture == null) {
102 throw StateError('the contributor should be drawing a sprite');
103 }
104 if (_particles.aliveCount != _count || frame.drawCalls < 1) {
105 throw StateError('the sparks did not reach the frame');
106 }
107 }
108}