The cached shadow map: the source
Open the live demo · Read the guide · View on GitHub
Regions the guide quotes: walls (line 58), mover (line 81), orbit (line 105), invalidate (line 108), atlas (line 119).
1/// Walls that never move are drawn into a lamp's shadow atlas once and kept;
2/// only the things that move are redrawn.
3///
4/// Quoted by `static_shadow_cache.md` and shown whole in the Source tab.
5library;
7import 'dart:math' as math;
9import 'package:flutter3d/flutter3d.dart';
10import 'package:flutter3d_showcase/src/demo/demo.dart';
11import 'package:vector_math/vector_math.dart';
13final class StaticShadowCacheDemo extends ShowcaseDemo {
14 bool wallsAreStatic = true;
15 bool moverOrbits = true;
16 bool showStatic = false;
17 bool showMoving = false;
19 late final Scene _scene;
20 late final List<MeshNode> _walls;
21 late final MeshNode _mover;
22 bool _wallsWere = true;
23 double _angle = 0.0;
26 void configureView(DemoContext context) {
27 context.orbit
28 ..distance = 9.0
29 ..pitch = 0.7
30 ..yaw = 0.8;
31 }
34 Scene build(DemoContext context) {
35 final Material stone = Material(
36 name: 'stone',
37 baseColor: Vector4(0.78, 0.76, 0.72, 1.0),
38 roughness: 0.9,
39 );
40 final Material floorStone = stone.copy()..doubleSided = true;
41 final Material clay = Material(
42 name: 'clay',
43 baseColor: Vector4(0.85, 0.45, 0.3, 1.0),
44 roughness: 0.7,
45 );
46 _scene = Scene()
47 ..add(
48 MeshNode(
49 DeviceMesh.upload(
50 context.device,
51 const PlaneShape(width: 12, depth: 12).build(),
52 ),
53 floorStone,
54 name: 'floor',
55 ),
56 );
58 _walls = <MeshNode>[
59 MeshNode(
60 DeviceMesh.upload(
61 context.device,
62 CuboidShape(size: Vector3(0.3, 3.0, 4.0)).build(),
63 ),
64 stone,
65 name: 'west wall',
66 )..setPosition(-2.5, 1.5, 0.0),
67 MeshNode(
68 DeviceMesh.upload(
69 context.device,
70 CuboidShape(size: Vector3(4.0, 3.0, 0.3)).build(),
71 ),
72 stone,
73 name: 'north wall',
74 )..setPosition(0.0, 1.5, -2.5),
75 ];
76 for (final MeshNode wall in _walls) {
77 wall.shadowIsStatic = true;
78 _scene.add(wall);
79 }
81 _mover = MeshNode(
82 DeviceMesh.upload(
83 context.device,
84 SphereShape(radius: 0.4, segments: 24, rings: 12).build(),
85 ),
86 clay,
87 name: 'mover',
88 );
89 _scene.add(_mover);
91 _scene.add(
92 LightNode(
93 name: 'lamp',
94 type: LightType.point,
95 intensity: 50.0,
96 range: 14.0,
97 castsShadow: true,
98 )..setPosition(0.0, 3.6, 0.0),
99 );
100 return _scene;
101 }
104 void update(DemoContext context, double dt) {
105 if (moverOrbits) _angle += dt;
106 _mover.setPosition(1.4 * math.cos(_angle), 0.6, 1.4 * math.sin(_angle));
108 if (wallsAreStatic != _wallsWere) {
109 for (final MeshNode wall in _walls) {
110 wall.shadowIsStatic = wallsAreStatic;
111 }
112 _scene.invalidateStaticShadows();
113 _wallsWere = wallsAreStatic;
114 }
115 }
118 RenderSettings settings(DemoContext context) => RenderSettings(
119 shadows: const ShadowSettings(cubeResolution: 256),
120 showStaticShadowMap: showStatic,
121 showShadowMap: showMoving,
122 );
125 List<DemoControl> controls(DemoContext context) => <DemoControl>[
126 ToggleControl(
127 'Walls are static',
128 value: () => wallsAreStatic,
129 onChanged: (bool v) => wallsAreStatic = v,
130 ),
131 ToggleControl(
132 'Mover circles',
133 value: () => moverOrbits,
134 onChanged: (bool v) => moverOrbits = v,
135 ),
136 ToggleControl(
137 'Show static atlas',
138 value: () => showStatic,
139 onChanged: (bool v) {
140 showStatic = v;
141 if (v) showMoving = false;
142 },
143 ),
144 ToggleControl(
145 'Show moving atlas',
146 value: () => showMoving,
147 onChanged: (bool v) {
148 showMoving = v;
149 if (v) showStatic = false;
150 },
151 ),
152 ];
155 void verify(Scene scene, FrameResult frame) {
156 final FramePass? bake = frame.passes
157 .where((FramePass p) => p.name == 'point shadows (static)')
158 .firstOrNull;
159 if (bake == null) {
160 throw StateError('the static shadow atlas was never baked');
161 }
162 if (bake.drawCalls < 1) {
163 throw StateError('the first frame did not draw the walls into the atlas');
164 }
165 }
166}