1use super::{
11 LitSurface, PlacedLight, SceneControl, SceneDef, SceneOptions, allocate_surface,
12 camera_looking_at, grid,
13};
14use unlit_ecs::bundle;
15use unlit_wgpu::pipeline::LitShader;
16use unlit3d::prelude::*;
17
18pub static CLUSTER_SCENE: SceneDef = SceneDef {
30 id: "lit_clusters",
31 title: "Cluster grid",
32 description: "a floor receding into the distance with the cluster grid drawn over it",
33 size: super::BASELINE_SIZE,
34 baseline: Some(super::BASELINE_SIZE),
35 clear: None,
36 frames: 1,
37 step_seconds: None,
38 samples: 1,
39 depth: true,
40 ui: false,
41 reproducible_ui: false,
42 build: build_clusters,
43};
44
45const CLUSTER_NEAR: f32 = 1.0;
52const CLUSTER_FAR: f32 = 120.0;
53
54const CLUSTER_FLOOR: (f32, f32, f32) = (700.0, 600.0, 10.0);
61
62const CLUSTER_EYE: glam::Vec3 = glam::Vec3::new(0.0, 9.5, 9.0);
68const CLUSTER_TARGET: glam::Vec3 = glam::Vec3::new(0.0, 0.0, -6.5);
69
70const CLUSTER_COLUMNS: [f32; 5] = [-8.0, -4.0, 0.0, 4.0, 8.0];
78
79const CLUSTER_ROWS: [(f32, f32, f32); 3] =
86 [(-1.0, 1.0, 5.5), (-9.0, 5.0, 12.0), (-18.0, 1.5, 10.0)];
87
88fn build_clusters(
95 world: &mut World,
96 context: RenderContext,
97 pass: Entity,
98 _ui: Option<Entity>,
99 size: (u32, u32),
100 _options: SceneOptions,
101) -> SceneControl {
102 let mut source = MeshSource::new(&world.root_scope(), context);
103 source.register_lit_family(&world.root_scope());
104 source.clustering = ClusterConfig {
105 first_slice_depth: CLUSTER_NEAR,
106 far: CLUSTER_FAR,
107 ..ClusterConfig::DEFAULT
108 };
109 let mut options = super::default_options();
110 options.unlit = false;
111 options.shader = Some(LitShader::new("lit::cluster_grid", "", "", cluster_grid()));
112 let key = LitPipelineKey::new(options);
113
114 let source_entity = world
115 .scope_mut(pass)
116 .expect("the pass is alive")
117 .spawn(bundle![Source::new(source)]);
118
119 let mut floor = grid(
125 glam::Vec3::new(0.0, 0.0, CLUSTER_FLOOR.2 - CLUSTER_FLOOR.1 * 0.5),
126 glam::Vec3::X,
127 glam::Vec3::NEG_Z,
128 (CLUSTER_FLOOR.0, CLUSTER_FLOOR.1),
129 (1, 1),
130 );
131 floor.normals = Some(vec![glam::Vec3::Y.to_array(); floor.positions.len()]);
132 let mesh = allocate_surface(world, source_entity, &key, &floor);
133 let pipeline = GpuRenderPipeline::new(key);
134
135 let camera_entity = super::spawn_in(
136 world,
137 source_entity,
138 bundle![camera_looking_at(CLUSTER_EYE, CLUSTER_TARGET, size)],
139 );
140 super::spawn_in(
143 world,
144 source_entity,
145 bundle![AmbientLight::new(glam::Vec4::new(0.08, 0.08, 0.08, 0.0))],
146 );
147 super::spawn_in(world, source_entity, LitSurface { mesh, pipeline });
148
149 let hue_color = |hue: f32| {
153 glam::Vec4::new(
154 0.5 + 0.5 * (hue * std::f32::consts::TAU).cos(),
155 0.5 + 0.5 * (hue * std::f32::consts::TAU + 2.094).cos(),
156 0.5 + 0.5 * (hue * std::f32::consts::TAU + 4.189).cos(),
157 0.0,
158 )
159 };
160 let [
161 (point_depth, point_height, point_range),
162 (spot_depth, spot_height, spot_range),
163 (quad_depth, quad_height, quad_range),
164 ] = CLUSTER_ROWS;
165
166 for (column, x) in CLUSTER_COLUMNS.into_iter().enumerate() {
167 let color = hue_color(column as f32 / CLUSTER_COLUMNS.len() as f32);
168 super::spawn_in(
169 world,
170 source_entity,
171 PlacedLight {
172 light: LightSource::new(PointLight {
173 color,
174 intensity: 0.5,
175 range: point_range,
176 }),
177 translation: glam::Vec3::new(x, point_height, point_depth),
178 rotation: glam::Quat::IDENTITY,
179 },
180 );
181 }
182
183 for (column, x) in CLUSTER_COLUMNS.into_iter().enumerate() {
184 let color = hue_color(column as f32 / CLUSTER_COLUMNS.len() as f32 + 0.33);
185 let position = glam::Vec3::new(x, spot_height, spot_depth);
186 super::spawn_in(
187 world,
188 source_entity,
189 PlacedLight::aiming(
190 LightSource::new(SpotLight {
191 color,
192 intensity: 1.0,
193 range: spot_range,
194 inner_angle: 18f32.to_radians(),
195 outer_angle: 32f32.to_radians(),
196 }),
197 position,
198 position + glam::Vec3::NEG_Y,
199 ),
200 );
201 }
202
203 for (column, x) in CLUSTER_COLUMNS.into_iter().enumerate() {
204 let color = hue_color(column as f32 / CLUSTER_COLUMNS.len() as f32 + 0.66);
205 let position = glam::Vec3::new(x, quad_height, quad_depth);
206 super::spawn_in(
207 world,
208 source_entity,
209 PlacedLight::aiming(
210 LightSource::new(QuadLight::rect(color, 0.6, quad_range, 1.6, 1.2)),
211 position,
212 position + glam::Vec3::NEG_Y,
213 ),
214 );
215 }
216
217 super::spawn_in(
221 world,
222 source_entity,
223 PlacedLight {
224 light: LightSource::new(DirectionalLight {
225 color: glam::Vec4::new(0.9, 0.85, 0.75, 0.0),
226 intensity: 0.2,
227 }),
228 translation: glam::Vec3::ZERO,
229 rotation: glam::Quat::from_rotation_x(-1.0),
230 },
231 );
232
233 SceneControl {
234 advance: Box::new(move |world, _frame, _delta, frame_size| {
235 super::aim_camera(
236 world,
237 source_entity,
238 camera_entity,
239 CLUSTER_EYE,
240 CLUSTER_TARGET,
241 frame_size,
242 );
243 }),
244 snapshot: Box::new(move |frame| (frame == 0).then(|| "lit_clusters.webp".to_owned())),
245 }
246}
247
248fn cluster_grid() -> String {
270 r#"
271let grid_depth = view_depth(in.world);
272let grid_ndc = fragment_ndc(in.world);
273let grid_cell = fragment_cluster_index(grid_ndc, grid_depth);
274let grid_local_count = array_access::load_cell_word(grid_cell, 0u);
275// Fixed bands make equal colors mean equal counts. Red means the record is
276// full and further lights are being dropped.
277var grid = vec3<f32>(0.025, 0.035, 0.10);
278if grid_local_count > 0u { grid = vec3<f32>(0.04, 0.16, 0.48); }
279if grid_local_count > 2u { grid = vec3<f32>(0.02, 0.52, 0.56); }
280if grid_local_count > 4u { grid = vec3<f32>(0.12, 0.62, 0.20); }
281if grid_local_count > 6u { grid = vec3<f32>(0.82, 0.72, 0.08); }
282if grid_local_count > 8u { grid = vec3<f32>(0.95, 0.38, 0.035); }
283if grid_local_count >= cluster.dims.w { grid = vec3<f32>(0.92, 0.025, 0.025); }
284// The cell borders: the product the lookup floors, so a border is where its
285// floor changes. A border is measured in pixels, not in cells -- an edge's
286// distance to the nearest border divided by how much the lattice moves per
287// pixel -- so it stays the same width wherever the floor's depth changes fastest
288// and never thickens into a band.
289let grid_lattice = (grid_ndc * 0.5 + 0.5) * vec2<f32>(cluster.dims.xy);
290let grid_fract = fract(grid_lattice);
291let grid_edge = min(grid_fract, 1.0 - grid_fract);
292let grid_pixels = grid_edge / max(fwidth(grid_lattice), vec2<f32>(1e-6));
293let grid_border = 1.0 - smoothstep(0.5, 1.5, min(grid_pixels.x, grid_pixels.y));
294// The slice borders use the same continuous mapping as the actual lookup.
295let grid_slice = cluster_slice_coordinate(grid_depth);
296let grid_slice_fract = fract(grid_slice);
297let grid_slice_edge = min(grid_slice_fract, 1.0 - grid_slice_fract);
298let grid_slice_pixels = grid_slice_edge / max(fwidth(grid_slice), 1e-6);
299let grid_slice_border = 1.0 - smoothstep(0.5, 1.5, grid_slice_pixels);
300grid = mix(grid, vec3<f32>(0.0), grid_border * 0.9);
301grid = mix(grid, vec3<f32>(0.55), grid_slice_border * 0.7);
302var grid_opacity = 0.48;
303grid_opacity = max(grid_opacity, grid_border * 0.85);
304grid_opacity = max(grid_opacity, grid_slice_border * 0.7);
305// The frame's directional lights are outside the grid, so this edge mark counts
306// them from the uniform instead: one magenta line says the frame carries at
307// least one, and no line says it carries none.
308if lighting.directionals[0].color_intensity.w > 0.0 && grid_ndc.x < -0.985 {
309 grid = vec3<f32>(0.8, 0.08, 0.65);
310 grid_opacity = 0.85;
311}
312color = vec4<f32>(mix(color.rgb, grid, grid_opacity), color.a);
313 "#
314 .to_owned()
315}