flutter3d
Showcase Changelog 38 packages API reference

Cascades that keep what stands still: the source

since 0.8.0 Shadows

Regions the guide quotes: field (line 56), ball (line 74), sun (line 88), walk (line 97), roll (line 103), flag (line 108), settings (line 118), count (line 146), check (line 161).

1/// A field of blocks that never move, kept in a cascade atlas of their own,
2/// so a walking camera and a rolling ball redraw only what changed.
3///
4/// Quoted by `static_cascades.md` and shown whole in the Source tab.
5library;
6
7import 'dart:math' as math;
8
9import 'package:flutter3d/flutter3d.dart';
10import 'package:flutter3d_showcase/src/demo/demo.dart';
11import 'package:vector_math/vector_math.dart';
13final class StaticCascadesDemo extends ShowcaseDemo {
14 bool blocksAreStatic = true;
15 bool walk = true;
16 bool roll = true;
17 bool showAtlas = false;
19 static const int _cascades = 3;
21 late final DemoContext _context;
22 late final List<MeshNode> _blocks;
23 late final MeshNode _ball;
24 bool _blocksWere = true;
25 double _walked = 0.0;
26 double _rolled = 0.0;
28 @override
29 void configureView(DemoContext context) {
30 context.orbit
31 ..distance = 9.0
32 ..pitch = 0.65
33 ..yaw = 0.35;
34 }
36 @override
37 Scene build(DemoContext context) {
38 _context = context;
39 final Scene scene = Scene()
40 ..add(
41 MeshNode(
42 DeviceMesh.upload(
43 context.device,
44 const PlaneShape(width: 18, depth: 12).build(),
45 ),
46 Material(
47 name: 'ground',
48 baseColor: Vector4(0.55, 0.55, 0.53, 1.0),
49 roughness: 0.9,
50 doubleSided: true,
51 ),
52 name: 'ground',
53 )..shadowCasting = ShadowCastingMode.off,
54 );
56 final DeviceMesh block = DeviceMesh.upload(
57 context.device,
58 CuboidShape(size: Vector3(0.4, 0.8, 0.4)).build(),
59 );
60 final Material stone = Material(
61 name: 'stone',
62 baseColor: Vector4(0.5, 0.48, 0.45, 1.0),
63 roughness: 0.85,
64 );
65 _blocks = <MeshNode>[
66 for (var i = 0; i < 12; i++)
67 for (var j = 0; j < 5; j++)
68 MeshNode(block, stone, name: 'block $i $j')
69 ..setPosition(-6.0 + i * 1.1, 0.4, -3.0 + j * 1.2)
70 ..shadowIsStatic = true,
71 ];
72 _blocks.forEach(scene.add);
74 _ball = MeshNode(
75 DeviceMesh.upload(
76 context.device,
77 const SphereShape(radius: 0.25, segments: 24, rings: 12).build(),
78 ),
79 Material(
80 name: 'clay',
81 baseColor: Vector4(0.6, 0.32, 0.22, 1.0),
82 roughness: 0.7,
83 ),
84 name: 'ball',
85 )..setPosition(0.05, 0.3, -0.6);
86 scene.add(_ball);
88 scene.add(
89 LightNode(name: 'sun', intensity: 1.8)
90 ..setLocalForward(Vector3(-4.0, -5.0, -0.01).normalized()),
91 );
92 return scene;
93 }
95 @override
96 void update(DemoContext context, double dt) {
97 if (walk) {
98 _walked += dt;
99 context.orbit.target.x = 3.0 * math.sin(0.35 * _walked);
100 context.orbit.apply();
101 }
103 if (roll) {
104 _rolled += dt;
105 _ball.setPosition(0.05, 0.3, -0.6 + 2.2 * math.sin(0.8 * _rolled));
106 }
108 if (blocksAreStatic != _blocksWere) {
109 for (final MeshNode block in _blocks) {
110 block.shadowIsStatic = blocksAreStatic;
111 }
112 _blocksWere = blocksAreStatic;
113 }
114 }
116 @override
117 RenderSettings settings(DemoContext context) => RenderSettings(
118 shadows: const ShadowSettings(cascades: _cascades),
119 showShadowMap: showAtlas,
120 );
122 @override
123 List<DemoControl> controls(DemoContext context) => <DemoControl>[
124 ToggleControl(
125 'Blocks are static',
126 value: () => blocksAreStatic,
127 onChanged: (bool v) => blocksAreStatic = v,
128 ),
129 ToggleControl(
130 'Camera walks',
131 value: () => walk,
132 onChanged: (bool v) => walk = v,
133 ),
134 ToggleControl(
135 'Ball rolls',
136 value: () => roll,
137 onChanged: (bool v) => roll = v,
138 ),
139 ToggleControl(
140 'Show the sun\'s atlas',
141 value: () => showAtlas,
142 onChanged: (bool v) => showAtlas = v,
143 ),
144 ];
146 /// What the frame says the sun's atlas cost: every draw into it, casters,
147 /// copies and resets alike.
148 static int _shadowDraws(FrameResult frame) => frame.passes
149 .where((FramePass p) => p.name == 'directional shadows')
150 .fold(0, (int sum, FramePass p) => sum + p.drawCalls);
152 @override
153 void verify(Scene scene, FrameResult frame) {
154 final int first = _shadowDraws(frame);
155 if (first < _blocks.length) {
156 throw StateError(
157 'the first frame drew $first things into the sun\'s atlas, '
158 'fewer than the ${_blocks.length} blocks',
159 );
160 }
161 // The camera stays where it is and only the ball moves.
162 _ball.translate(0.0, 0.0, 0.2);
163 final int next = _shadowDraws(
164 _context.renderer.render(
165 width: 320,
166 height: 180,
167 scene: scene,
168 views: views(_context),
169 settings: settings(_context),
170 ),
171 );
172 // A copy of the kept tile and the ball, in each cascade at most.
173 if (blocksAreStatic && next > 2 * _cascades) {
174 throw StateError(
175 'moving the ball drew $next things into the sun\'s atlas, '
176 'more than a copy and the ball in each of $_cascades cascades',
177 );
178 }
179 }