flutter3d
Showcase Changelog 38 packages API reference

Occlusion culling: the source

since 0.8.0 Scene and geometry

Regions the guide quotes: wall (line 70), crates (line 85), lookout (line 107), probe (line 141), settings (line 160), check (line 188).

1/// A wall marked as an occluder, a yard of crates behind it, and a lookout in
2/// front: the crates the lookout cannot see are the ones occlusion culling
3/// leaves out.
4///
5/// Quoted by `occlusion_culling.md` and shown whole in the Source tab.
6library;
7
8import 'dart:math' as math;
9
10import 'package:flutter3d/flutter3d.dart';
11import 'package:flutter3d_showcase/src/demo/demo.dart';
12import 'package:vector_math/vector_math.dart';
14final class OcclusionCullingDemo extends ShowcaseDemo {
15 double wallX = 0.0;
16 bool wallOccludes = true;
17 int mode = 1;
19 static const List<OcclusionMode> _modes = <OcclusionMode>[
20 OcclusionMode.none,
21 OcclusionMode.software,
22 OcclusionMode.hiZ,
23 ];
25 /// The lookout's picture is wider than it is tall, like the 256 by 128
26 /// buffer the software test draws into.
27 static const double _lookoutAspect = 2.0;
29 late final MeshNode _wall;
30 late final List<MeshNode> _crates;
31 late final CameraNode _lookout;
32 late final Material _seen;
33 late final Material _hidden;
35 final SoftwareOcclusion _probe = SoftwareOcclusion();
36 int _hiddenCount = 0;
38 @override
39 void configureView(DemoContext context) {
40 context.orbit
41 ..distance = 11.0
42 ..pitch = 0.75
43 ..yaw = 0.35;
44 }
46 @override
47 Scene build(DemoContext context) {
48 final Scene scene = Scene()
49 ..ambientColor = Vector3(0.42, 0.48, 0.6)
50 ..ambientIntensity = 0.18
51 ..add(
52 MeshNode(
53 DeviceMesh.upload(
54 context.device,
55 const PlaneShape(width: 10, depth: 10).build(),
56 ),
57 Material(
58 name: 'yard',
59 baseColor: Vector4(0.5, 0.5, 0.48, 1.0),
60 roughness: 0.9,
61 ),
62 name: 'ground',
63 )..setPosition(0.0, 0.0, -0.5),
64 )
65 ..add(
66 LightNode(name: 'sun', intensity: 3.0)
67 ..setLocalForward(Vector3(-0.4, -0.8, -0.45)),
68 );
70 _wall = MeshNode(
71 DeviceMesh.upload(
72 context.device,
73 CuboidShape(size: Vector3(2.6, 1.6, 0.15)).build(),
74 ),
75 Material(
76 name: 'wall',
77 baseColor: Vector4(0.36, 0.42, 0.5, 1.0),
78 roughness: 0.8,
79 ),
80 name: 'wall',
81 )..setPosition(0.0, 0.8, 1.5);
82 _wall.occluder = true;
83 scene.add(_wall);
85 _seen = Material(
86 name: 'seen',
87 baseColor: Vector4(0.92, 0.55, 0.2, 1.0),
88 roughness: 0.6,
89 );
90 _hidden = Material(
91 name: 'hidden',
92 baseColor: Vector4(0.22, 0.22, 0.24, 1.0),
93 roughness: 0.6,
94 );
95 final DeviceMesh crate = DeviceMesh.upload(
96 context.device,
97 CuboidShape(size: Vector3.all(0.5)).build(),
98 );
99 _crates = <MeshNode>[
100 for (var row = 0; row < 3; row++)
101 for (var column = 0; column < 5; column++)
102 MeshNode(crate, _seen, name: 'crate $row.$column')
103 ..setPosition((column - 2) * 1.2, 0.25, -0.5 - row * 1.0),
104 ];
105 _crates.forEach(scene.add);
107 _lookout =
108 CameraNode(
109 name: 'lookout',
110 projection: const PerspectiveProjection(
111 fovYRadians: 60 * math.pi / 180,
112 near: 0.1,
113 far: 30.0,
114 ),
115 )
116 ..setPosition(0.0, 1.0, 4.0)
117 ..lookAt(Vector3(0.0, 0.6, -1.0));
118 scene.add(
119 MeshNode(
120 DeviceMesh.upload(
121 context.device,
122 SphereShape(radius: 0.15, segments: 16, rings: 8).build(),
123 ),
124 Material(
125 name: 'lookout marker',
126 baseColor: Vector4(0.3, 0.8, 0.95, 1.0),
127 roughness: 0.4,
128 ),
129 name: 'lookout marker',
130 )..setPosition(0.0, 1.0, 4.0),
131 );
132 return scene;
133 }
135 @override
136 void update(DemoContext context, double dt) {
137 _wall
138 ..setPosition(wallX, 0.8, 1.5)
139 ..occluder = wallOccludes;
141 final Matrix4 viewProjection = _lookout.viewProjection(_lookoutAspect);
142 final OcclusionTest test = _probe.prepare(
143 meshes: <MeshNode>[_wall, ..._crates],
144 viewProjection: viewProjection,
145 frustum: Frustum.matrix(viewProjection),
146 eye: _lookout.readWorldPosition(),
147 layerMask: ~0,
148 );
149 var hidden = 0;
150 for (final MeshNode crate in _crates) {
151 final bool visible = test.mayBeVisible(crate.worldBounds);
152 crate.material = visible ? _seen : _hidden;
153 if (!visible) hidden++;
154 }
155 _hiddenCount = hidden;
156 }
158 @override
159 RenderSettings settings(DemoContext context) => RenderSettings(
160 occlusion: _modes[mode],
161 );
163 @override
164 List<DemoControl> controls(DemoContext context) => <DemoControl>[
165 SliderControl(
166 'Wall position',
167 min: -2.5,
168 max: 2.5,
169 value: () => wallX,
170 onChanged: (double v) => wallX = v,
171 format: (double v) => '${v.toStringAsFixed(2)} m',
172 ),
173 ToggleControl(
174 'Wall is an occluder',
175 value: () => wallOccludes,
176 onChanged: (bool v) => wallOccludes = v,
177 ),
178 ChoiceControl(
179 'This view culls with',
180 options: const <String>['none', 'software', 'hiZ'],
181 index: () => mode,
182 onChanged: (int i) => mode = i,
183 ),
184 ];
186 @override
187 void verify(Scene scene, FrameResult frame) {
188 if (_probe.occluders != 1) {
189 throw StateError('the wall was not drawn into the occlusion buffer');
190 }
191 if (_hiddenCount == 0 || _hiddenCount == _crates.length) {
192 throw StateError(
193 'the wall should hide some crates from the lookout and not all of '
194 'them; it hid $_hiddenCount of ${_crates.length}',
195 );
196 }
197 if (frame.drawCalls < 1) {
198 throw StateError('the yard did not reach the frame');
199 }
200 }