Thirty-two lights and the fade band: the source
Open the live demo · Read the guide · View on GitHub
Regions the guide quotes: floor (line 45), torches (line 78), settings (line 95), check (line 111).
1/// Thirty-two lights on one draw: eight in the shader's own slots and
2/// twenty-four more through the light list, with `lightFadeBand` softening
3/// the edge between them. Tiled rather than one giant plane, so which
4/// torches reach a given tile is a real question — see `#region floor`.
5///
6/// Quoted by `many_lights.md` and shown whole in the Source tab.
7library;
9import 'dart:math' as math;
11import 'package:flutter3d/flutter3d.dart';
12import 'package:flutter3d_showcase/src/demo/demo.dart';
13import 'package:vector_math/vector_math.dart';
15final class ManyLightsDemo extends ShowcaseDemo {
16 double fadeBand = 0.5;
18 /// Eight direct slots plus the twenty-four-light tail is thirty-two; eight
19 /// more than that is what a floor this crowded actually has to drop.
20 static const int _torchCount = 40;
22 /// The floor as a grid of tiles rather than one plane — see `#region
23 /// floor` for why.
24 static const int _tilesPerSide = 5;
25 static const double _floorSize = 20.0;
26 static const double _tileSize = _floorSize / _tilesPerSide;
28 /// Wide enough that every tile, edge or centre, has more than the
29 /// thirty-two the list can carry within reach — the case the fade band
30 /// needs, and past a torch's own visible glow long before its falloff
31 /// actually reaches this far.
32 static const double _torchRange = 12.0;
35 void configureView(DemoContext context) {
36 context.orbit
37 ..distance = 12.0
38 ..pitch = 0.7;
39 }
42 Scene build(DemoContext context) {
43 final Scene scene = Scene();
45 // A grid of tiles, not one plane. A single 20x20 draw's own bounding
46 // sphere (radius about 14) reaches every torch on the ring below, six
47 // units out — every one of them lands *inside* it, and `_relevanceIn`
48 // scores a light inside an object's sphere at that object's ceiling
49 // regardless of exactly where inside it sits. Selection then cannot
50 // tell one torch from another, and `lightFadeBand` has nothing to
51 // soften: every candidate ties, so the torch that finally overflows the
52 // list is exactly as strong as the thirty-two that did not. Twenty-five
53 // four-by-four tiles each carry a bounding sphere small enough, next to
54 // a torch's own wide range, that how far a tile sits from the ring is a
55 // real question with a real answer — the tile at the very centre is
56 // furthest from every torch at once, and is where the band has
57 // something to soften.
58 final DeviceMesh tile = DeviceMesh.upload(
59 context.device,
60 PlaneShape(width: _tileSize, depth: _tileSize).build(),
61 );
62 final Material floorMaterial = Material(
63 baseColor: Vector4(0.5, 0.5, 0.55, 1.0),
64 roughness: 0.85,
65 doubleSided: true,
66 );
67 for (var row = 0; row < _tilesPerSide; row++) {
68 for (var col = 0; col < _tilesPerSide; col++) {
69 final double x = (col - (_tilesPerSide - 1) / 2) * _tileSize;
70 final double z = (row - (_tilesPerSide - 1) / 2) * _tileSize;
71 scene.add(
72 MeshNode(tile, floorMaterial, name: 'floor $row,$col')
73 ..setPosition(x, 0.0, z),
74 );
75 }
76 }
78 for (var i = 0; i < _torchCount; i++) {
79 final double angle = i / _torchCount * 2.0 * math.pi;
80 scene.add(
81 LightNode(
82 name: 'torch $i',
83 type: LightType.point,
84 intensity: 2.5,
85 range: _torchRange,
86 )..setPosition(math.cos(angle) * 6.0, 0.6, math.sin(angle) * 6.0),
87 );
88 }
90 return scene;
91 }
94 RenderSettings settings(DemoContext context) => RenderSettings(
95 lightFadeBand: fadeBand,
96 );
99 List<DemoControl> controls(DemoContext context) => <DemoControl>[
100 SliderControl(
101 'Fade band',
102 min: 0,
103 max: 1,
104 value: () => fadeBand,
105 onChanged: (double v) => fadeBand = v,
106 ),
107 ];
110 void verify(Scene scene, FrameResult frame) {
111 if (scene.lights.length != _torchCount) {
112 throw StateError(
113 'expected $_torchCount torches, found '
114 '${scene.lights.length}',
115 );
116 }
117 final int expectedDropped = math.max(
118 _torchCount - LightBuffer.maxLights - LightBuffer.maxExtraLights,
119 0,
120 );
121 if (expectedDropped == 0) {
122 throw StateError(
123 'this scene needs more torches than the cap to prove '
124 'anything is actually dropped',
125 );
126 }
127 if (frame.lightsDropped != expectedDropped) {
128 throw StateError(
129 'expected $expectedDropped lights past the eight slots and the '
130 'twenty-four-light tail, the frame reports ${frame.lightsDropped}',
131 );
132 }
133 final int tileCount = _tilesPerSide * _tilesPerSide;
134 if (scene.meshes.length != tileCount) {
135 throw StateError(
136 'expected $tileCount floor tiles, found ${scene.meshes.length} — a '
137 'floor back to one plane gives every torch the same bounding-sphere '
138 'ceiling and the fade band nothing to soften',
139 );
140 }
141 if (frame.drawCalls < tileCount) {
142 throw StateError('fewer than $tileCount tiles were drawn');
143 }
144 }
145}