1#![deny(
131 clippy::unwrap_used,
132 clippy::expect_used,
133 clippy::indexing_slicing,
134 clippy::panic,
135 clippy::unreachable
136)]
137
138pub mod rules;
139mod shader;
140
141use super::common::{self, PaletteParams, PanParams};
142use super::lines::GeneratorConfig;
143use super::{FamilyParam, FamilyRange, ParamGroup, ParamKind, ParamSpec, Scene, default_of};
144use crate::dsp::AnalysisFrame;
145use crate::render::camera::{self, CameraParams};
146use crate::render::gpu;
147use crate::render::palette::{self, Palette};
148use crate::render::tier::GridScale;
149pub use rules::{DEFAULT_RULE, Neighbourhood, RosterRule, Rule, RuleList};
150use std::f32::consts::{FRAC_PI_2, PI, TAU};
151
152pub const DEFAULT_GRID: u32 = 64;
154pub const MIN_GRID: u32 = 16;
157pub const MAX_GRID: u32 = 128;
160
161pub const MAX_STEP_RATE: f32 = 120.0;
165
166const AGE_CAP: u32 = 1023;
169const AGE_SPAN: f32 = 48.0;
172
173const STATE_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::R32Uint;
176
177const WORKGROUP: u32 = 4;
180
181const BRICK: u32 = 8;
184
185const LIGHT_CELL: u32 = 4;
188
189const LIGHT_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float;
192
193const LIGHT_WORKGROUP: u32 = 4;
196
197const MAX_BOUNCE: f32 = 16.0;
200
201fn march_body() -> String {
204 format!(
205 "const MAX_MARCH: i32 = {};\nconst AGE_SPAN: f32 = {:?};\nconst BRICK: i32 = {};\n\
206 const LIGHT_CELL: f32 = {:?};\n{}",
207 3 * MAX_GRID,
208 AGE_SPAN,
209 BRICK,
210 LIGHT_CELL as f32,
211 shader::MARCH_SHADER
212 )
213}
214
215const MARCH_STEP: u32 = 8;
217
218const CUBE_RADIUS: f32 = 1.732_050_8;
221
222const FIELD_SEED_MIX: u32 = 0x564F_584C;
226const STAMP_SEED_MIX: u32 = 0x5253_5658;
229
230const RESEED_THRESHOLD: f32 = 0.5;
233
234pub const MAX_SHELLS: u32 = 16;
237
238const MAX_SHELL_GAIN: f32 = 16.0;
241
242#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
244pub enum Present {
245 #[default]
247 Glow,
248 Solid,
252}
253
254impl Present {
255 pub const ALL: [Present; 2] = [Present::Glow, Present::Solid];
257
258 pub fn as_str(self) -> &'static str {
260 match self {
261 Present::Glow => "glow",
262 Present::Solid => "solid",
263 }
264 }
265
266 pub fn from_name(name: &str) -> Option<Self> {
268 Self::ALL.into_iter().find(|p| p.as_str() == name)
269 }
270}
271
272#[derive(Debug, Clone, Copy, PartialEq)]
275pub struct VoxelConfig {
276 pub grid: u32,
279 pub rules: RuleList,
281 pub seed_radius: f32,
283 pub seed_fill: f32,
285 pub wrap: bool,
287 pub shells: u32,
290 pub present: Present,
292 pub salt: u32,
295}
296
297impl Default for VoxelConfig {
298 fn default() -> Self {
299 let (seed_radius, seed_fill) = rules::DEFAULT_RULE.seed();
300 Self {
301 grid: DEFAULT_GRID,
302 rules: RuleList::default(),
303 seed_radius,
304 seed_fill,
305 wrap: false,
308 shells: 0,
309 present: Present::Glow,
310 salt: 0,
311 }
312 }
313}
314
315const DEFAULT_STEP_RATE: f32 = default_of(PARAMS, "step_rate");
316const DEFAULT_DENSITY: f32 = default_of(PARAMS, "density");
317const DEFAULT_TRAIL: f32 = default_of(PARAMS, "trail");
318const DEFAULT_AGE_TINT: f32 = default_of(PARAMS, "age_tint");
319const DEFAULT_HUE: f32 = default_of(PARAMS, "hue");
320const DEFAULT_HUE_SPREAD: f32 = default_of(PARAMS, "hue_spread");
321const DEFAULT_BRIGHTNESS: f32 = default_of(PARAMS, "brightness");
322const DEFAULT_ZOOM: f32 = default_of(PARAMS, "zoom");
323const DEFAULT_RESEED_RADIUS: f32 = default_of(PARAMS, "reseed_radius");
324const DEFAULT_SHELL_GAIN: f32 = default_of(PARAMS, "shell_gain");
325const DEFAULT_KEY: f32 = default_of(PARAMS, "key");
326const DEFAULT_AMBIENT: f32 = default_of(PARAMS, "ambient");
327const DEFAULT_AO: f32 = default_of(PARAMS, "ao");
328const DEFAULT_BOUNCE: f32 = default_of(PARAMS, "bounce");
329const DEFAULT_LIGHT_YAW: f32 = default_of(PARAMS, "light_yaw");
330const DEFAULT_LIGHT_PITCH: f32 = default_of(PARAMS, "light_pitch");
331const DEFAULT_CUT: f32 = default_of(PARAMS, "cut");
332const DEFAULT_CUT_YAW: f32 = default_of(PARAMS, "cut_yaw");
333const DEFAULT_CUT_PITCH: f32 = default_of(PARAMS, "cut_pitch");
334
335const FOCUS: ParamSpec = ParamSpec {
339 doc: "Inert on voxel: depth of field blurs each drawn primitive, and the march draws none.",
340 ..camera::FOCUS
341};
342const APERTURE: ParamSpec = ParamSpec {
343 doc: "Inert on voxel: depth of field blurs each drawn primitive, and the march draws none.",
344 ..camera::APERTURE
345};
346const FOG: ParamSpec = ParamSpec {
347 doc: "Fades the volume toward black with depth: at 1 the cube's farthest point is black and its nearest keeps its light. 0 is off.",
348 ..camera::FOG
349};
350
351pub const PARAMS: &[ParamSpec] = &[
356 ParamSpec {
357 name: "rule",
358 default: 0.0,
359 range: Some([0.0, (rules::MAX_RULES - 1) as f32]),
360 doc: "Which of the [voxel] rules runs, counting from 0; past the list's end it runs \
361 the last. A change takes effect at the next generation.",
362 kind: ParamKind::Structural,
363 group: ParamGroup::Shape,
364 main: true,
365 },
366 ParamSpec {
367 name: "step_rate",
368 default: 8.0,
369 range: Some([0.0, 30.0]),
370 doc: "How many generations the automaton runs per second, whatever the frame rate; 0 \
371 freezes it.",
372 kind: ParamKind::Modal,
373 group: ParamGroup::Motion,
374 main: true,
375 },
376 ParamSpec {
377 name: "reseed",
378 default: 0.0,
379 range: Some([0.0, 1.0]),
380 doc: "A rise past 0.5 fills one ball of the cube with fresh seeded cells, once per \
381 rise; bind an onset or a beat to it.",
382 kind: ParamKind::Modal,
383 group: ParamGroup::Motion,
384 main: false,
385 },
386 ParamSpec {
387 name: "reseed_radius",
388 default: 0.25,
389 range: Some([0.05, 1.0]),
390 doc: "The radius of the ball a reseed fills, as a fraction of the cube's half-side.",
391 kind: ParamKind::Modal,
392 group: ParamGroup::Shape,
393 main: false,
394 },
395 ParamSpec {
396 name: "cut",
397 default: 0.0,
398 range: Some([0.0, 1.0]),
399 doc: "How much of the cube a section plane hides, so the inside shows: 0.5 cuts it \
400 through the centre, 1 hides it all, 0 is off. Only the view is cut; the \
401 automaton runs on.",
402 kind: ParamKind::Modal,
403 group: ParamGroup::Shape,
404 main: true,
405 },
406 ParamSpec {
407 name: "cut_yaw",
408 default: 0.0,
409 range: Some([0.0, TAU]),
410 doc: "Which way the section plane faces around the cube's vertical axis, in radians: \
411 the side it hides. It turns with the cube, not the camera.",
412 kind: ParamKind::Modal,
413 group: ParamGroup::Shape,
414 main: false,
415 },
416 ParamSpec {
417 name: "cut_pitch",
418 default: 0.0,
419 range: Some([-FRAC_PI_2, FRAC_PI_2]),
420 doc: "How far the section plane's hidden side tilts up toward the cube's top, in \
421 radians; negative tilts it down.",
422 kind: ParamKind::Modal,
423 group: ParamGroup::Shape,
424 main: false,
425 },
426 ParamSpec {
427 name: "shell_gain",
428 default: 0.0,
429 range: Some([0.0, 4.0]),
430 doc: "How strongly the spectrum lights the cube's radial shells, bass at the centre \
431 and treble at the faces; 0 leaves the light as the cells give it.",
432 kind: ParamKind::Modal,
433 group: ParamGroup::Light,
434 main: false,
435 },
436 ParamSpec {
437 name: "density",
438 default: 1.0,
439 range: Some([0.0, 8.0]),
440 doc: "How strongly the volume absorbs the light behind it, per unit of the cube's \
441 width; 0 is pure additive glow with no front or back.",
442 kind: ParamKind::Modal,
443 group: ParamGroup::Light,
444 main: true,
445 },
446 ParamSpec {
447 name: "trail",
448 default: 0.6,
449 range: Some([0.0, 1.0]),
450 doc: "How much of its light a decaying cell keeps at each stage of its decay; 0 draws \
451 only the live cells.",
452 kind: ParamKind::Modal,
453 group: ParamGroup::Light,
454 main: false,
455 },
456 ParamSpec {
457 name: "age_tint",
458 default: 0.3,
459 range: Some([0.0, 1.0]),
460 doc: "How far along the palette a cell's colour travels as it ages; a decaying cell \
461 takes the far end.",
462 kind: ParamKind::Modal,
463 group: ParamGroup::Colour,
464 main: false,
465 },
466 common::hue(0.0),
467 ParamSpec {
468 name: "hue_spread",
469 default: 0.3,
470 range: Some([0.0, 1.0]),
471 doc: "How far along the palette the colour travels from the cube's centre to its \
472 corners.",
473 kind: ParamKind::Modal,
474 group: ParamGroup::Colour,
475 main: false,
476 },
477 common::brightness(1.0),
478 ParamSpec {
479 name: "key",
480 default: 1.0,
481 range: Some([0.0, 4.0]),
482 doc: "How strongly the key light lights a solid face turned toward it; 0 leaves only \
483 the ambient light.",
484 kind: ParamKind::Modal,
485 group: ParamGroup::Light,
486 main: true,
487 },
488 ParamSpec {
489 name: "ambient",
490 default: 0.3,
491 range: Some([0.0, 1.0]),
492 doc: "The light every solid face gets whichever way it turns; 1 with no key light \
493 draws each face in its own colour.",
494 kind: ParamKind::Modal,
495 group: ParamGroup::Light,
496 main: false,
497 },
498 ParamSpec {
499 name: "ao",
500 default: 1.0,
501 range: Some([0.0, 1.0]),
502 doc: "How far the cells around a solid face darken its ambient light toward the \
503 creases and inner corners; 0 is none.",
504 kind: ParamKind::Modal,
505 group: ParamGroup::Light,
506 main: false,
507 },
508 ParamSpec {
509 name: "bounce",
510 default: 0.0,
511 range: Some([0.0, 2.0]),
512 doc: "How much light a solid face takes from the live cells in front of it, so a \
513 structure glows onto itself; soft, reaching a few cells and through thin \
514 walls. 0 is off.",
515 kind: ParamKind::Modal,
516 group: ParamGroup::Light,
517 main: false,
518 },
519 ParamSpec {
520 name: "light_yaw",
521 default: -0.6,
522 range: Some([-PI, PI]),
523 doc: "Where the key light stands around the viewer, in radians: 0 behind the camera, \
524 positive to its right. It turns with the camera.",
525 kind: ParamKind::Modal,
526 group: ParamGroup::Light,
527 main: false,
528 },
529 ParamSpec {
530 name: "light_pitch",
531 default: 0.6,
532 range: Some([-1.5, 1.5]),
533 doc: "How far above the viewer the key light stands, in radians; negative lights from \
534 below. It turns with the camera.",
535 kind: ParamKind::Modal,
536 group: ParamGroup::Light,
537 main: false,
538 },
539 camera::YAW,
540 camera::PITCH,
541 camera::DISTANCE,
542 camera::FOV,
543 FOCUS,
544 APERTURE,
545 FOG,
546 common::SATURATION,
547 common::PALETTE_MIX,
548 common::PALETTE_STEPS,
549 common::zoom(1.0),
550 common::PAN_X,
551 common::PAN_Y,
552];
553
554macro_rules! solid_only {
557 ($name:literal: $range:expr) => {
558 FamilyParam {
559 name: $name,
560 ranges: &[
561 FamilyRange {
562 family: "glow",
563 range: None,
564 },
565 FamilyRange {
566 family: "solid",
567 range: $range,
568 },
569 ],
570 }
571 };
572}
573
574pub const FAMILY_PARAMS: &[FamilyParam] = &[
578 solid_only!("key": Some([0.0, 4.0])),
579 solid_only!("ambient": Some([0.0, 1.0])),
580 solid_only!("ao": Some([0.0, 1.0])),
581 solid_only!("bounce": Some([0.0, 2.0])),
582 solid_only!("light_yaw": Some([-PI, PI])),
583 solid_only!("light_pitch": Some([-1.5, 1.5])),
584];
585
586pub(crate) fn field_seed(salt: u32) -> u32 {
588 salt ^ FIELD_SEED_MIX
589}
590
591pub(crate) fn live_threshold(fill: f32) -> u32 {
594 (fill.clamp(0.0, 1.0) * 16_777_216.0) as u32
595}
596
597pub(crate) fn seed_radius_sq(seed_radius: f32, grid: u32) -> u32 {
601 let r = seed_radius.clamp(0.0, 2.0) * grid as f32;
602 (r * r) as u32
603}
604
605fn mix32(v: u32) -> u32 {
608 let mut h = v;
609 h ^= h >> 16;
610 h = h.wrapping_mul(0x7FEB_352D);
611 h ^= h >> 15;
612 h = h.wrapping_mul(0x846C_A68B);
613 h ^= h >> 16;
614 h
615}
616
617#[derive(Debug, Clone, Copy, PartialEq, Eq)]
620pub(crate) struct Stamp {
621 pub(crate) centre: [u32; 3],
622 pub(crate) radius_sq: u32,
623 pub(crate) seed: u32,
624}
625
626pub(crate) fn stamp(salt: u32, rise: u32, grid: u32, radius: f32) -> Stamp {
630 let base = mix32(rise ^ mix32(salt ^ STAMP_SEED_MIX));
631 let side = grid.max(1);
632 let r = radius.clamp(0.0, 2.0) * side as f32 * 0.5;
633 Stamp {
634 centre: [
635 mix32(base ^ 1) % side,
636 mix32(base ^ 2) % side,
637 mix32(base ^ 3) % side,
638 ],
639 radius_sq: (r * r) as u32,
640 seed: mix32(base ^ 4),
641 }
642}
643
644pub(crate) fn shell_levels(spectrum: &[f32], shells: u32) -> [f32; MAX_SHELLS as usize] {
648 let mut out = [0.0; MAX_SHELLS as usize];
649 let s = (shells.min(MAX_SHELLS) as usize).min(spectrum.len());
650 if s == 0 {
651 return out;
652 }
653 for (i, level) in out.iter_mut().take(s).enumerate() {
654 let lo = i * spectrum.len() / s;
655 let hi = ((i + 1) * spectrum.len() / s).max(lo + 1);
656 let band = spectrum.get(lo..hi).unwrap_or(&[]);
657 let mean = band.iter().sum::<f32>() / band.len().max(1) as f32;
658 *level = if mean.is_finite() { mean.max(0.0) } else { 0.0 };
659 }
660 out
661}
662
663pub(crate) fn applied_step_rate(value: f32) -> f32 {
666 if value.is_finite() {
667 value.clamp(0.0, MAX_STEP_RATE)
668 } else {
669 DEFAULT_STEP_RATE
670 }
671}
672
673fn finite_or(value: f32, fallback: f32) -> f32 {
675 if value.is_finite() { value } else { fallback }
676}
677
678pub(crate) fn grid_clamp(
681 config: VoxelConfig,
682 cap: u32,
683) -> (VoxelConfig, Option<super::CapOverflow>) {
684 if config.grid <= cap {
685 return (config, None);
686 }
687 let overflow = super::CapOverflow {
688 dropped: (config.grid - cap) as usize,
689 context: super::OverflowContext::Voxels(config.grid),
690 cap: cap as usize,
691 };
692 (
693 VoxelConfig {
694 grid: cap,
695 ..config
696 },
697 Some(overflow),
698 )
699}
700
701fn rows(view: &camera::CameraView) -> [[f32; 4]; 3] {
705 let m = view.view_proj;
706 let row = |r: usize| {
707 let at = |c: usize| m.get(c).and_then(|col| col.get(r)).copied().unwrap_or(0.0);
708 [at(0), at(1), at(2), at(3)]
709 };
710 [row(0), row(1), row(3)]
711}
712
713fn eye(view: &camera::CameraView) -> [f32; 3] {
717 let [r0, r1, r3] = rows(view);
718 let (a, b, c) = (xyz(r0), xyz(r1), xyz(r3));
719 let rhs = [-r0[3], -r1[3], -r3[3]];
720 let det = dot(a, cross(b, c));
721 if det.abs() < 1e-12 {
722 return [0.0, 0.0, view.distance];
723 }
724 let solve = |i: usize| {
726 let swap = |row: [f32; 3], k: usize| {
727 let mut r = row;
728 if let Some(slot) = r.get_mut(i) {
729 *slot = rhs.get(k).copied().unwrap_or(0.0);
730 }
731 r
732 };
733 dot(swap(a, 0), cross(swap(b, 1), swap(c, 2))) / det
734 };
735 [solve(0), solve(1), solve(2)]
736}
737
738pub(crate) fn light_dir(view: &camera::CameraView, light_yaw: f32, light_pitch: f32) -> [f32; 3] {
744 let [r0, _, r3] = rows(view);
745 let normalize = |v: [f32; 3]| {
746 let len = dot(v, v).sqrt();
747 if len > 1e-12 {
748 [v[0] / len, v[1] / len, v[2] / len]
749 } else {
750 v
751 }
752 };
753 let forward = normalize(xyz(r3));
754 let r = xyz(r0);
756 let along = dot(r, forward);
757 let right = normalize([
758 r[0] - along * forward[0],
759 r[1] - along * forward[1],
760 r[2] - along * forward[2],
761 ]);
762 let up = cross(right, forward);
763 let (sy, cy) = finite_or(light_yaw, DEFAULT_LIGHT_YAW).sin_cos();
764 let (sp, cp) = finite_or(light_pitch, DEFAULT_LIGHT_PITCH)
765 .clamp(-FRAC_PI_2, FRAC_PI_2)
766 .sin_cos();
767 let (x, y, z) = (sy * cp, sp, cy * cp);
768 normalize([
770 right[0] * x + up[0] * y - forward[0] * z,
771 right[1] * x + up[1] * y - forward[1] * z,
772 right[2] * x + up[2] * y - forward[2] * z,
773 ])
774}
775
776pub(crate) fn cut_plane(cut: f32, cut_yaw: f32, cut_pitch: f32) -> Option<[f32; 4]> {
781 let cut = finite_or(cut, DEFAULT_CUT).min(1.0);
782 if cut <= 0.0 {
783 return None;
784 }
785 let (sy, cy) = finite_or(cut_yaw, DEFAULT_CUT_YAW).sin_cos();
786 let (sp, cp) = finite_or(cut_pitch, DEFAULT_CUT_PITCH)
787 .clamp(-FRAC_PI_2, FRAC_PI_2)
788 .sin_cos();
789 Some([cp * sy, sp, cp * cy, CUBE_RADIUS * (1.0 - 2.0 * cut)])
790}
791
792fn xyz(v: [f32; 4]) -> [f32; 3] {
793 [v[0], v[1], v[2]]
794}
795
796fn dot(a: [f32; 3], b: [f32; 3]) -> f32 {
797 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
798}
799
800fn cross(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
801 [
802 a[1] * b[2] - a[2] * b[1],
803 a[2] * b[0] - a[0] * b[2],
804 a[0] * b[1] - a[1] * b[0],
805 ]
806}
807
808#[repr(C)]
815#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
816struct StepParams {
817 a: [u32; 4],
819 b: [u32; 4],
821 c: [u32; 4],
824 d: [u32; 4],
826}
827
828#[repr(C)]
830#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
831struct MarchParams {
832 r0: [f32; 4],
833 r1: [f32; 4],
834 r3: [f32; 4],
835 eye: [f32; 4],
837 a: [f32; 4],
839 b: [f32; 4],
841 c: [f32; 4],
843 d: [f32; 4],
846 light: [f32; 4],
849 lit: [f32; 4],
851 cut: [f32; 4],
854 bounce: [f32; 4],
857 shells: [[f32; 4]; (MAX_SHELLS / 4) as usize],
859}
860
861pub(crate) fn march_size(target: (u32, u32), scale: f32, cap: (u32, u32)) -> (u32, u32) {
866 let scale =
867 GridScale::new(scale.clamp(GridScale::MIN, GridScale::MAX)).unwrap_or(GridScale::FULL);
868 crate::render::grid::grid_size(target, scale, cap, MARCH_STEP)
869}
870
871#[derive(Clone, Copy)]
873enum StepPass {
874 Step,
876 Seed,
878 Stamp,
880}
881
882impl StepPass {
883 fn mode(self) -> u32 {
884 match self {
885 StepPass::Step => 0,
886 StepPass::Seed => 1,
887 StepPass::Stamp => 2,
888 }
889 }
890
891 fn label(self) -> &'static str {
892 match self {
893 StepPass::Step => "voxel-step",
894 StepPass::Seed => "voxel-seed",
895 StepPass::Stamp => "voxel-stamp",
896 }
897 }
898
899 fn source(self) -> String {
902 format!(
903 "const MODE: u32 = {}u;\nconst AGE_CAP: u32 = {}u;\n{}{}{}",
904 self.mode(),
905 AGE_CAP,
906 gpu::HASH_WGSL,
907 common::CELL_HASH_WGSL,
908 shader::STEP_SHADER
909 )
910 }
911}
912
913struct StateTexture {
915 #[cfg_attr(not(test), allow(dead_code))]
917 texture: wgpu::Texture,
918 read: wgpu::TextureView,
920 write: wgpu::TextureView,
922}
923
924impl StateTexture {
925 fn new(device: &wgpu::Device, grid: u32, label: &str) -> Self {
926 let texture = device.create_texture(&wgpu::TextureDescriptor {
927 label: Some(label),
928 size: wgpu::Extent3d {
929 width: grid,
930 height: grid,
931 depth_or_array_layers: grid,
932 },
933 mip_level_count: 1,
934 sample_count: 1,
935 dimension: wgpu::TextureDimension::D3,
936 format: STATE_FORMAT,
937 usage: wgpu::TextureUsages::TEXTURE_BINDING
940 | wgpu::TextureUsages::STORAGE_BINDING
941 | wgpu::TextureUsages::COPY_SRC
942 | wgpu::TextureUsages::COPY_DST,
943 view_formats: &[],
944 });
945 let read = texture.create_view(&wgpu::TextureViewDescriptor::default());
946 let write = texture.create_view(&wgpu::TextureViewDescriptor::default());
947 Self {
948 texture,
949 read,
950 write,
951 }
952 }
953}
954
955struct LightField {
959 _splat: wgpu::Texture,
961 _blurred: wgpu::Texture,
962 blurred_view: wgpu::TextureView,
964 splat_pipeline: wgpu::ComputePipeline,
965 blur_pipeline: wgpu::ComputePipeline,
966 splat_from_a: wgpu::BindGroup,
968 splat_from_b: wgpu::BindGroup,
969 blur_bg: wgpu::BindGroup,
970 side: u32,
972}
973
974impl LightField {
975 fn build(device: &wgpu::Device, grid: u32, a: &StateTexture, b: &StateTexture) -> Self {
976 let side = grid.div_ceil(LIGHT_CELL);
977 let field = |label: &str| {
978 device.create_texture(&wgpu::TextureDescriptor {
979 label: Some(label),
980 size: wgpu::Extent3d {
981 width: side,
982 height: side,
983 depth_or_array_layers: side,
984 },
985 mip_level_count: 1,
986 sample_count: 1,
987 dimension: wgpu::TextureDimension::D3,
988 format: LIGHT_FORMAT,
989 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::STORAGE_BINDING,
990 view_formats: &[],
991 })
992 };
993 let splat = field("voxel-light-splat");
994 let blurred = field("voxel-light-blurred");
995 let splat_read = splat.create_view(&wgpu::TextureViewDescriptor::default());
996 let splat_write = splat.create_view(&wgpu::TextureViewDescriptor::default());
997 let blurred_write = blurred.create_view(&wgpu::TextureViewDescriptor::default());
998 let blurred_view = blurred.create_view(&wgpu::TextureViewDescriptor::default());
999
1000 let splat_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1008 label: Some("voxel-light-splat-layout"),
1009 entries: &[
1010 wgpu::BindGroupLayoutEntry {
1011 binding: 0,
1012 visibility: wgpu::ShaderStages::COMPUTE,
1013 ty: wgpu::BindingType::StorageTexture {
1014 access: wgpu::StorageTextureAccess::WriteOnly,
1015 format: LIGHT_FORMAT,
1016 view_dimension: wgpu::TextureViewDimension::D3,
1017 },
1018 count: None,
1019 },
1020 wgpu::BindGroupLayoutEntry {
1021 binding: 1,
1022 visibility: wgpu::ShaderStages::COMPUTE,
1023 ty: wgpu::BindingType::Texture {
1024 sample_type: wgpu::TextureSampleType::Uint,
1025 view_dimension: wgpu::TextureViewDimension::D3,
1026 multisampled: false,
1027 },
1028 count: None,
1029 },
1030 ],
1031 });
1032 let blur_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1035 label: Some("voxel-light-blur-layout"),
1036 entries: &[
1037 wgpu::BindGroupLayoutEntry {
1038 binding: 0,
1039 visibility: wgpu::ShaderStages::COMPUTE,
1040 ty: wgpu::BindingType::Texture {
1041 sample_type: wgpu::TextureSampleType::Float { filterable: false },
1042 view_dimension: wgpu::TextureViewDimension::D3,
1043 multisampled: false,
1044 },
1045 count: None,
1046 },
1047 wgpu::BindGroupLayoutEntry {
1048 binding: 1,
1049 visibility: wgpu::ShaderStages::COMPUTE,
1050 ty: wgpu::BindingType::StorageTexture {
1051 access: wgpu::StorageTextureAccess::WriteOnly,
1052 format: LIGHT_FORMAT,
1053 view_dimension: wgpu::TextureViewDimension::D3,
1054 },
1055 count: None,
1056 },
1057 wgpu::BindGroupLayoutEntry {
1058 binding: 2,
1059 visibility: wgpu::ShaderStages::COMPUTE,
1060 ty: wgpu::BindingType::Texture {
1061 sample_type: wgpu::TextureSampleType::Float { filterable: false },
1062 view_dimension: wgpu::TextureViewDimension::D3,
1063 multisampled: false,
1064 },
1065 count: None,
1066 },
1067 ],
1068 });
1069 let bind = |layout: &wgpu::BindGroupLayout, views: &[&wgpu::TextureView]| {
1071 let entries: Vec<wgpu::BindGroupEntry<'_>> = (0u32..)
1072 .zip(views)
1073 .map(|(binding, view)| wgpu::BindGroupEntry {
1074 binding,
1075 resource: wgpu::BindingResource::TextureView(view),
1076 })
1077 .collect();
1078 device.create_bind_group(&wgpu::BindGroupDescriptor {
1079 label: Some("voxel-light-bg"),
1080 layout,
1081 entries: &entries,
1082 })
1083 };
1084 let splat_from_a = bind(&splat_layout, &[&splat_write, &a.read]);
1085 let splat_from_b = bind(&splat_layout, &[&splat_write, &b.read]);
1086 let blur_bg = bind(&blur_layout, &[&splat_read, &blurred_write, &splat_read]);
1087 let pipeline = |label: &str, layout: &wgpu::BindGroupLayout, source: String| {
1088 let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1089 label: Some(label),
1090 source: wgpu::ShaderSource::Wgsl(source.into()),
1091 });
1092 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1093 label: Some(label),
1094 bind_group_layouts: &[Some(layout)],
1095 immediate_size: 0,
1096 });
1097 device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
1098 label: Some(label),
1099 layout: Some(&pipeline_layout),
1100 module: &module,
1101 entry_point: Some("main"),
1102 compilation_options: Default::default(),
1103 cache: None,
1104 })
1105 };
1106 let splat_pipeline = pipeline(
1107 "voxel-light-splat",
1108 &splat_layout,
1109 format!(
1110 "const LIGHT_CELL: u32 = {LIGHT_CELL}u;\nconst AGE_SPAN: f32 = {AGE_SPAN:?};\n{}",
1111 shader::LIGHT_SPLAT_SHADER
1112 ),
1113 );
1114 let blur_pipeline = pipeline(
1115 "voxel-light-blur",
1116 &blur_layout,
1117 shader::LIGHT_BLUR_SHADER.to_string(),
1118 );
1119 Self {
1120 _splat: splat,
1121 _blurred: blurred,
1122 blurred_view,
1123 splat_pipeline,
1124 blur_pipeline,
1125 splat_from_a,
1126 splat_from_b,
1127 blur_bg,
1128 side,
1129 }
1130 }
1131
1132 fn encode(&self, encoder: &mut wgpu::CommandEncoder, reading_a: bool) {
1135 let groups = self.side.div_ceil(LIGHT_WORKGROUP);
1136 {
1137 let mut cpass = gpu::compute_pass(encoder, "voxel-light-splat");
1138 cpass.set_pipeline(&self.splat_pipeline);
1139 cpass.set_bind_group(
1140 0,
1141 if reading_a {
1142 &self.splat_from_a
1143 } else {
1144 &self.splat_from_b
1145 },
1146 &[],
1147 );
1148 cpass.dispatch_workgroups(groups, groups, groups);
1149 }
1150 let mut cpass = gpu::compute_pass(encoder, "voxel-light-blur");
1151 cpass.set_pipeline(&self.blur_pipeline);
1152 cpass.set_bind_group(0, &self.blur_bg, &[]);
1153 cpass.dispatch_workgroups(groups, groups, groups);
1154 }
1155}
1156
1157struct MarchTarget {
1160 size: (u32, u32),
1161 _texture: wgpu::Texture,
1163 view: wgpu::TextureView,
1164 present_bg: wgpu::BindGroup,
1165}
1166
1167struct Resources {
1170 grid: u32,
1172 #[cfg_attr(not(test), allow(dead_code))]
1174 a: StateTexture,
1175 #[cfg_attr(not(test), allow(dead_code))]
1176 b: StateTexture,
1177 reading_a: bool,
1179 seed_pipeline: wgpu::ComputePipeline,
1180 stamp_pipeline: wgpu::ComputePipeline,
1181 step_pipeline: wgpu::ComputePipeline,
1182 step_uniform: wgpu::Buffer,
1183 step_from_a: wgpu::BindGroup,
1185 step_from_b: wgpu::BindGroup,
1186 _bricks: wgpu::Texture,
1189 bricks_read: wgpu::TextureView,
1192 brick_pipeline: wgpu::ComputePipeline,
1193 brick_from_a: wgpu::BindGroup,
1195 brick_from_b: wgpu::BindGroup,
1196 march_layout: wgpu::BindGroupLayout,
1197 march_pipeline: wgpu::RenderPipeline,
1198 march_uniform: wgpu::Buffer,
1199 march_from_a: wgpu::BindGroup,
1202 march_from_b: wgpu::BindGroup,
1203 _light_stand_in: wgpu::Texture,
1206 light_sampler: wgpu::Sampler,
1208 light: Option<LightField>,
1210 light_fresh: bool,
1213 present_layout: wgpu::BindGroupLayout,
1214 present_pipeline: wgpu::RenderPipeline,
1215 present_sampler: wgpu::Sampler,
1216 target: Option<MarchTarget>,
1218 luts: palette::LutPair,
1221}
1222
1223impl Resources {
1224 fn build(device: &wgpu::Device, surface_format: wgpu::TextureFormat, grid: u32) -> Self {
1225 let a = StateTexture::new(device, grid, "voxel-state-a");
1226 let b = StateTexture::new(device, grid, "voxel-state-b");
1227 let side = grid.div_ceil(BRICK);
1228 let bricks = device.create_texture(&wgpu::TextureDescriptor {
1229 label: Some("voxel-bricks"),
1230 size: wgpu::Extent3d {
1231 width: side,
1232 height: side,
1233 depth_or_array_layers: side,
1234 },
1235 mip_level_count: 1,
1236 sample_count: 1,
1237 dimension: wgpu::TextureDimension::D3,
1238 format: STATE_FORMAT,
1239 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::STORAGE_BINDING,
1240 view_formats: &[],
1241 });
1242 let bricks_read = bricks.create_view(&wgpu::TextureViewDescriptor::default());
1243 let bricks_write = bricks.create_view(&wgpu::TextureViewDescriptor::default());
1244 let step_uniform = gpu::uniform_buffer(
1245 device,
1246 "voxel-step-params",
1247 std::mem::size_of::<StepParams>(),
1248 );
1249 let march_uniform = gpu::uniform_buffer(
1250 device,
1251 "voxel-march-params",
1252 std::mem::size_of::<MarchParams>(),
1253 );
1254
1255 let step_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1258 label: Some("voxel-step-layout"),
1259 entries: &[
1260 wgpu::BindGroupLayoutEntry {
1261 binding: 0,
1262 visibility: wgpu::ShaderStages::COMPUTE,
1263 ty: wgpu::BindingType::Texture {
1264 sample_type: wgpu::TextureSampleType::Uint,
1265 view_dimension: wgpu::TextureViewDimension::D3,
1266 multisampled: false,
1267 },
1268 count: None,
1269 },
1270 wgpu::BindGroupLayoutEntry {
1271 binding: 1,
1272 visibility: wgpu::ShaderStages::COMPUTE,
1273 ty: wgpu::BindingType::StorageTexture {
1274 access: wgpu::StorageTextureAccess::WriteOnly,
1275 format: STATE_FORMAT,
1276 view_dimension: wgpu::TextureViewDimension::D3,
1277 },
1278 count: None,
1279 },
1280 gpu::uniform(2, wgpu::ShaderStages::COMPUTE),
1281 ],
1282 });
1283 let step_bind = |src: &StateTexture, dst: &StateTexture| {
1284 device.create_bind_group(&wgpu::BindGroupDescriptor {
1285 label: Some("voxel-step-bg"),
1286 layout: &step_layout,
1287 entries: &[
1288 wgpu::BindGroupEntry {
1289 binding: 0,
1290 resource: wgpu::BindingResource::TextureView(&src.read),
1291 },
1292 wgpu::BindGroupEntry {
1293 binding: 1,
1294 resource: wgpu::BindingResource::TextureView(&dst.write),
1295 },
1296 wgpu::BindGroupEntry {
1297 binding: 2,
1298 resource: step_uniform.as_entire_binding(),
1299 },
1300 ],
1301 })
1302 };
1303 let step_from_a = step_bind(&a, &b);
1304 let step_from_b = step_bind(&b, &a);
1305 let step_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1306 label: Some("voxel-step-pipeline-layout"),
1307 bind_group_layouts: &[Some(&step_layout)],
1308 immediate_size: 0,
1309 });
1310 let compute = |pass: StepPass| {
1311 let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1312 label: Some(pass.label()),
1313 source: wgpu::ShaderSource::Wgsl(pass.source().into()),
1314 });
1315 device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
1316 label: Some(pass.label()),
1317 layout: Some(&step_pipeline_layout),
1318 module: &module,
1319 entry_point: Some("main"),
1320 compilation_options: Default::default(),
1321 cache: None,
1322 })
1323 };
1324 let seed_pipeline = compute(StepPass::Seed);
1325 let stamp_pipeline = compute(StepPass::Stamp);
1326 let step_pipeline = compute(StepPass::Step);
1327
1328 let brick_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1332 label: Some("voxel-brick-layout"),
1333 entries: &[
1334 wgpu::BindGroupLayoutEntry {
1335 binding: 0,
1336 visibility: wgpu::ShaderStages::COMPUTE,
1337 ty: wgpu::BindingType::Texture {
1338 sample_type: wgpu::TextureSampleType::Uint,
1339 view_dimension: wgpu::TextureViewDimension::D3,
1340 multisampled: false,
1341 },
1342 count: None,
1343 },
1344 wgpu::BindGroupLayoutEntry {
1345 binding: 1,
1346 visibility: wgpu::ShaderStages::COMPUTE,
1347 ty: wgpu::BindingType::StorageTexture {
1348 access: wgpu::StorageTextureAccess::WriteOnly,
1349 format: STATE_FORMAT,
1350 view_dimension: wgpu::TextureViewDimension::D3,
1351 },
1352 count: None,
1353 },
1354 ],
1355 });
1356 let brick_bind = |src: &StateTexture| {
1357 device.create_bind_group(&wgpu::BindGroupDescriptor {
1358 label: Some("voxel-brick-bg"),
1359 layout: &brick_layout,
1360 entries: &[
1361 wgpu::BindGroupEntry {
1362 binding: 0,
1363 resource: wgpu::BindingResource::TextureView(&src.read),
1364 },
1365 wgpu::BindGroupEntry {
1366 binding: 1,
1367 resource: wgpu::BindingResource::TextureView(&bricks_write),
1368 },
1369 ],
1370 })
1371 };
1372 let brick_from_a = brick_bind(&a);
1373 let brick_from_b = brick_bind(&b);
1374 let brick_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1375 label: Some("voxel-bricks"),
1376 source: wgpu::ShaderSource::Wgsl(
1377 format!("const BRICK: u32 = {BRICK}u;\n{}", shader::BRICK_SHADER).into(),
1378 ),
1379 });
1380 let brick_pipeline_layout =
1381 device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1382 label: Some("voxel-brick-pipeline-layout"),
1383 bind_group_layouts: &[Some(&brick_layout)],
1384 immediate_size: 0,
1385 });
1386 let brick_pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
1387 label: Some("voxel-bricks"),
1388 layout: Some(&brick_pipeline_layout),
1389 module: &brick_module,
1390 entry_point: Some("main"),
1391 compilation_options: Default::default(),
1392 cache: None,
1393 });
1394
1395 let luts = palette::LutPair::new(device, "voxel");
1396 let march_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1400 label: Some("voxel-march-layout"),
1401 entries: &[
1402 wgpu::BindGroupLayoutEntry {
1403 binding: 0,
1404 visibility: wgpu::ShaderStages::FRAGMENT,
1405 ty: wgpu::BindingType::Buffer {
1406 ty: wgpu::BufferBindingType::Uniform,
1407 has_dynamic_offset: false,
1408 min_binding_size: wgpu::BufferSize::new(
1409 std::mem::size_of::<MarchParams>() as u64
1410 ),
1411 },
1412 count: None,
1413 },
1414 wgpu::BindGroupLayoutEntry {
1415 binding: 1,
1416 visibility: wgpu::ShaderStages::FRAGMENT,
1417 ty: wgpu::BindingType::Texture {
1418 sample_type: wgpu::TextureSampleType::Uint,
1419 view_dimension: wgpu::TextureViewDimension::D3,
1420 multisampled: false,
1421 },
1422 count: None,
1423 },
1424 wgpu::BindGroupLayoutEntry {
1425 binding: 2,
1426 visibility: wgpu::ShaderStages::FRAGMENT,
1427 ty: wgpu::BindingType::Texture {
1428 sample_type: wgpu::TextureSampleType::Uint,
1429 view_dimension: wgpu::TextureViewDimension::D3,
1430 multisampled: false,
1431 },
1432 count: None,
1433 },
1434 gpu::texture(3, true),
1435 gpu::texture(4, true),
1436 gpu::sampler(5),
1437 wgpu::BindGroupLayoutEntry {
1438 binding: 6,
1439 visibility: wgpu::ShaderStages::FRAGMENT,
1440 ty: wgpu::BindingType::Texture {
1441 sample_type: wgpu::TextureSampleType::Float { filterable: true },
1442 view_dimension: wgpu::TextureViewDimension::D3,
1443 multisampled: false,
1444 },
1445 count: None,
1446 },
1447 gpu::sampler(7),
1448 ],
1449 });
1450 let light_stand_in = device.create_texture(&wgpu::TextureDescriptor {
1451 label: Some("voxel-light-stand-in"),
1452 size: wgpu::Extent3d {
1453 width: 1,
1454 height: 1,
1455 depth_or_array_layers: 1,
1456 },
1457 mip_level_count: 1,
1458 sample_count: 1,
1459 dimension: wgpu::TextureDimension::D3,
1460 format: LIGHT_FORMAT,
1461 usage: wgpu::TextureUsages::TEXTURE_BINDING,
1462 view_formats: &[],
1463 });
1464 let light_stand_in_view =
1465 light_stand_in.create_view(&wgpu::TextureViewDescriptor::default());
1466 let light_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
1467 label: Some("voxel-light-sampler"),
1468 address_mode_u: wgpu::AddressMode::ClampToEdge,
1469 address_mode_v: wgpu::AddressMode::ClampToEdge,
1470 address_mode_w: wgpu::AddressMode::ClampToEdge,
1471 mag_filter: wgpu::FilterMode::Linear,
1472 min_filter: wgpu::FilterMode::Linear,
1473 ..Default::default()
1474 });
1475 let march_bind = |field: &StateTexture| {
1476 march_bind_group(
1477 device,
1478 &march_layout,
1479 &march_uniform,
1480 &field.read,
1481 &bricks_read,
1482 &luts,
1483 &light_stand_in_view,
1484 &light_sampler,
1485 )
1486 };
1487 let march_from_a = march_bind(&a);
1488 let march_from_b = march_bind(&b);
1489 let march_shader =
1490 gpu::fullscreen_shader(device, "voxel-march", gpu::FULLSCREEN_VS_NDC, &march_body());
1491 let march_pipeline = gpu::fullscreen_pipeline(
1494 device,
1495 &march_shader,
1496 &[&march_layout],
1497 surface_format,
1498 wgpu::BlendState::REPLACE,
1499 "voxel-march",
1500 );
1501
1502 let present_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1506 label: Some("voxel-present-layout"),
1507 entries: &[
1508 wgpu::BindGroupLayoutEntry {
1509 binding: 0,
1510 visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
1511 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
1512 count: None,
1513 },
1514 gpu::texture(1, true),
1515 ],
1516 });
1517 let present_shader = gpu::fullscreen_shader(
1518 device,
1519 "voxel-present",
1520 gpu::FULLSCREEN_VS_UV_FLIPPED,
1521 shader::PRESENT_SHADER,
1522 );
1523 let present_pipeline = gpu::fullscreen_pipeline(
1524 device,
1525 &present_shader,
1526 &[&present_layout],
1527 surface_format,
1528 wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING,
1531 "voxel-present",
1532 );
1533 let present_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
1534 label: Some("voxel-present-sampler"),
1535 mag_filter: wgpu::FilterMode::Linear,
1536 min_filter: wgpu::FilterMode::Linear,
1537 ..Default::default()
1538 });
1539
1540 Self {
1541 grid,
1542 a,
1543 b,
1544 reading_a: true,
1545 seed_pipeline,
1546 stamp_pipeline,
1547 step_pipeline,
1548 step_uniform,
1549 step_from_a,
1550 step_from_b,
1551 _bricks: bricks,
1552 bricks_read,
1553 brick_pipeline,
1554 brick_from_a,
1555 brick_from_b,
1556 march_layout,
1557 march_pipeline,
1558 march_uniform,
1559 march_from_a,
1560 march_from_b,
1561 _light_stand_in: light_stand_in,
1562 light_sampler,
1563 light: None,
1564 light_fresh: false,
1565 present_layout,
1566 present_pipeline,
1567 present_sampler,
1568 target: None,
1569 luts,
1570 }
1571 }
1572
1573 fn ensure_light(&mut self, device: &wgpu::Device) {
1575 if self.light.is_some() {
1576 return;
1577 }
1578 let light = LightField::build(device, self.grid, &self.a, &self.b);
1579 let bind = |field: &StateTexture| {
1580 march_bind_group(
1581 device,
1582 &self.march_layout,
1583 &self.march_uniform,
1584 &field.read,
1585 &self.bricks_read,
1586 &self.luts,
1587 &light.blurred_view,
1588 &self.light_sampler,
1589 )
1590 };
1591 self.march_from_a = bind(&self.a);
1592 self.march_from_b = bind(&self.b);
1593 self.light = Some(light);
1594 self.light_fresh = false;
1595 }
1596
1597 fn encode_light(&mut self, encoder: &mut wgpu::CommandEncoder) {
1599 if let Some(light) = self.light.as_ref() {
1600 light.encode(encoder, self.reading_a);
1601 self.light_fresh = true;
1602 }
1603 }
1604
1605 fn encode_bricks(&self, encoder: &mut wgpu::CommandEncoder) {
1607 let side = self.grid.div_ceil(BRICK);
1608 let mut cpass = gpu::compute_pass(encoder, "voxel-bricks");
1609 cpass.set_pipeline(&self.brick_pipeline);
1610 cpass.set_bind_group(
1611 0,
1612 if self.reading_a {
1613 &self.brick_from_a
1614 } else {
1615 &self.brick_from_b
1616 },
1617 &[],
1618 );
1619 cpass.dispatch_workgroups(side, side, side);
1620 }
1621
1622 fn ensure_target(
1625 &mut self,
1626 device: &wgpu::Device,
1627 format: wgpu::TextureFormat,
1628 size: (u32, u32),
1629 ) {
1630 if self.target.as_ref().is_none_or(|t| t.size != size) {
1631 let texture = device.create_texture(&wgpu::TextureDescriptor {
1632 label: Some("voxel-march-target"),
1633 size: wgpu::Extent3d {
1634 width: size.0,
1635 height: size.1,
1636 depth_or_array_layers: 1,
1637 },
1638 mip_level_count: 1,
1639 sample_count: 1,
1640 dimension: wgpu::TextureDimension::D2,
1641 format,
1642 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
1643 | wgpu::TextureUsages::TEXTURE_BINDING,
1644 view_formats: &[],
1645 });
1646 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
1647 let present_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
1648 label: Some("voxel-present-bg"),
1649 layout: &self.present_layout,
1650 entries: &[
1651 wgpu::BindGroupEntry {
1652 binding: 0,
1653 resource: wgpu::BindingResource::Sampler(&self.present_sampler),
1654 },
1655 wgpu::BindGroupEntry {
1656 binding: 1,
1657 resource: wgpu::BindingResource::TextureView(&view),
1658 },
1659 ],
1660 });
1661 self.target = Some(MarchTarget {
1662 size,
1663 _texture: texture,
1664 view,
1665 present_bg,
1666 });
1667 }
1668 }
1669
1670 #[cfg(test)]
1672 fn current(&self) -> &wgpu::Texture {
1673 if self.reading_a {
1674 &self.a.texture
1675 } else {
1676 &self.b.texture
1677 }
1678 }
1679
1680 fn encode_step(&mut self, encoder: &mut wgpu::CommandEncoder, pass: StepPass) {
1684 let pipeline = match pass {
1685 StepPass::Step => &self.step_pipeline,
1686 StepPass::Seed => &self.seed_pipeline,
1687 StepPass::Stamp => &self.stamp_pipeline,
1688 };
1689 let groups = self.grid.div_ceil(WORKGROUP);
1690 {
1691 let mut cpass = gpu::compute_pass(encoder, pass.label());
1692 cpass.set_pipeline(pipeline);
1693 cpass.set_bind_group(
1694 0,
1695 if self.reading_a {
1696 &self.step_from_a
1697 } else {
1698 &self.step_from_b
1699 },
1700 &[],
1701 );
1702 cpass.dispatch_workgroups(groups, groups, groups);
1703 }
1704 self.reading_a = !self.reading_a;
1705 }
1706}
1707
1708#[allow(clippy::too_many_arguments)]
1711fn march_bind_group(
1712 device: &wgpu::Device,
1713 layout: &wgpu::BindGroupLayout,
1714 uniform: &wgpu::Buffer,
1715 field: &wgpu::TextureView,
1716 bricks: &wgpu::TextureView,
1717 luts: &palette::LutPair,
1718 light: &wgpu::TextureView,
1719 light_sampler: &wgpu::Sampler,
1720) -> wgpu::BindGroup {
1721 let [lut_a, lut_b, lut_sampler] = luts.bind_entries(3, 4, 5);
1722 device.create_bind_group(&wgpu::BindGroupDescriptor {
1723 label: Some("voxel-march-bg"),
1724 layout,
1725 entries: &[
1726 wgpu::BindGroupEntry {
1727 binding: 0,
1728 resource: uniform.as_entire_binding(),
1729 },
1730 wgpu::BindGroupEntry {
1731 binding: 1,
1732 resource: wgpu::BindingResource::TextureView(field),
1733 },
1734 wgpu::BindGroupEntry {
1735 binding: 2,
1736 resource: wgpu::BindingResource::TextureView(bricks),
1737 },
1738 lut_a,
1739 lut_b,
1740 lut_sampler,
1741 wgpu::BindGroupEntry {
1742 binding: 6,
1743 resource: wgpu::BindingResource::TextureView(light),
1744 },
1745 wgpu::BindGroupEntry {
1746 binding: 7,
1747 resource: wgpu::BindingResource::Sampler(light_sampler),
1748 },
1749 ],
1750 })
1751}
1752
1753pub struct VoxelScene {
1756 device: wgpu::Device,
1758 surface_format: wgpu::TextureFormat,
1759 res: Option<Resources>,
1760 config: VoxelConfig,
1762 grid_cap: u32,
1765 march_scale: GridScale,
1767 march_cap: (u32, u32),
1769 target: (u32, u32),
1771 field_scale: GridScale,
1774 skip_bricks: bool,
1777 needs_seed: bool,
1780 clock: common::GenerationClock,
1781 dt: f32,
1784 pending_generations: u32,
1786 rises: u32,
1789 pending_stamp: Option<Stamp>,
1791 prev_reseed: f32,
1793 levels: [f32; MAX_SHELLS as usize],
1795 rule: f32,
1796 step_rate: f32,
1797 reseed: f32,
1798 reseed_radius: f32,
1799 shell_gain: f32,
1800 density: f32,
1801 trail: f32,
1802 age_tint: f32,
1803 hue_spread: f32,
1804 key: f32,
1805 ambient: f32,
1806 ao: f32,
1807 bounce: f32,
1808 light_builds: u32,
1811 light_yaw: f32,
1812 light_pitch: f32,
1813 cut: f32,
1814 cut_yaw: f32,
1815 cut_pitch: f32,
1816 zoom: f32,
1817 colour: PaletteParams,
1818 pan: PanParams,
1819 camera: CameraParams,
1820 occlude: f32,
1823 palette: Palette,
1826}
1827
1828impl VoxelScene {
1829 pub fn new(
1833 device: &wgpu::Device,
1834 surface_format: wgpu::TextureFormat,
1835 grid_cap: u32,
1836 march_scale: GridScale,
1837 march_cap: (u32, u32),
1838 ) -> Self {
1839 Self {
1840 device: device.clone(),
1841 surface_format,
1842 res: None,
1843 config: VoxelConfig::default(),
1844 grid_cap: grid_cap.clamp(MIN_GRID, MAX_GRID),
1845 march_scale,
1846 march_cap,
1847 target: (1, 1),
1848 field_scale: GridScale::FULL,
1849 skip_bricks: true,
1850 needs_seed: true,
1851 clock: common::GenerationClock::default(),
1852 dt: 0.0,
1853 pending_generations: 0,
1854 rises: 0,
1855 pending_stamp: None,
1856 prev_reseed: 0.0,
1857 levels: [0.0; MAX_SHELLS as usize],
1858 rule: 0.0,
1859 step_rate: DEFAULT_STEP_RATE,
1860 reseed: 0.0,
1861 reseed_radius: DEFAULT_RESEED_RADIUS,
1862 shell_gain: DEFAULT_SHELL_GAIN,
1863 density: DEFAULT_DENSITY,
1864 trail: DEFAULT_TRAIL,
1865 age_tint: DEFAULT_AGE_TINT,
1866 hue_spread: DEFAULT_HUE_SPREAD,
1867 key: DEFAULT_KEY,
1868 ambient: DEFAULT_AMBIENT,
1869 ao: DEFAULT_AO,
1870 bounce: DEFAULT_BOUNCE,
1871 light_builds: 0,
1872 light_yaw: DEFAULT_LIGHT_YAW,
1873 light_pitch: DEFAULT_LIGHT_PITCH,
1874 cut: DEFAULT_CUT,
1875 cut_yaw: DEFAULT_CUT_YAW,
1876 cut_pitch: DEFAULT_CUT_PITCH,
1877 zoom: DEFAULT_ZOOM,
1878 colour: PaletteParams::new(DEFAULT_HUE, DEFAULT_BRIGHTNESS),
1879 pan: PanParams::default(),
1880 camera: CameraParams::default(),
1881 occlude: crate::render::post::DEFAULT_OCCLUDE,
1882 palette: Palette::default_spectrum(),
1883 }
1884 }
1885
1886 fn step_params(&self, stamp: Option<Stamp>) -> StepParams {
1889 let rule = self.config.rules.pick(self.rule);
1890 let stamp = stamp.unwrap_or(Stamp {
1891 centre: [0, 0, 0],
1892 radius_sq: 0,
1893 seed: 0,
1894 });
1895 StepParams {
1896 a: [
1897 self.config.grid,
1898 u32::from(self.config.wrap),
1899 field_seed(self.config.salt),
1900 live_threshold(self.config.seed_fill),
1901 ],
1902 b: [
1903 rule.birth,
1904 rule.survive,
1905 rule.states,
1906 rule.neighbourhood.index(),
1907 ],
1908 c: [
1909 seed_radius_sq(self.config.seed_radius, self.config.grid),
1910 stamp.seed,
1911 0,
1912 0,
1913 ],
1914 d: [
1915 stamp.centre[0],
1916 stamp.centre[1],
1917 stamp.centre[2],
1918 stamp.radius_sq,
1919 ],
1920 }
1921 }
1922
1923 #[cfg(test)]
1926 pub(crate) fn light_build_count(&self) -> u32 {
1927 self.light_builds
1928 }
1929
1930 fn applied_bounce(&self) -> f32 {
1933 match self.config.present {
1934 Present::Glow => 0.0,
1935 Present::Solid => finite_or(self.bounce, DEFAULT_BOUNCE).clamp(0.0, MAX_BOUNCE),
1936 }
1937 }
1938
1939 fn march_params(&self, aspect: f32) -> MarchParams {
1941 let zoom = if self.zoom.is_finite() && self.zoom > 1e-3 {
1942 self.zoom
1943 } else {
1944 DEFAULT_ZOOM
1945 };
1946 let view = self.camera.camera().view(
1947 aspect,
1948 zoom,
1949 [finite_or(self.pan.x, 0.0), finite_or(self.pan.y, 0.0)],
1950 );
1951 let [r0, r1, r3] = rows(&view);
1952 let [ex, ey, ez] = eye(&view);
1953 let cut = cut_plane(self.cut, self.cut_yaw, self.cut_pitch);
1954 MarchParams {
1955 r0,
1956 r1,
1957 r3,
1958 eye: [ex, ey, ez, self.config.grid as f32],
1959 a: [
1960 self.colour.hue,
1961 self.colour.brightness,
1962 self.colour.saturation,
1963 self.colour.mix,
1964 ],
1965 b: [
1966 palette::band_steps(self.colour.steps),
1967 self.occlude,
1968 finite_or(self.density, DEFAULT_DENSITY).max(0.0),
1969 finite_or(self.trail, DEFAULT_TRAIL).clamp(0.0, 1.0),
1970 ],
1971 c: [
1972 finite_or(self.age_tint, DEFAULT_AGE_TINT),
1973 finite_or(self.hue_spread, DEFAULT_HUE_SPREAD),
1974 finite_or(self.camera.fog, 0.0).clamp(0.0, 1.0),
1975 finite_or(self.shell_gain, DEFAULT_SHELL_GAIN).clamp(0.0, MAX_SHELL_GAIN),
1976 ],
1977 d: [
1978 view.distance - CUBE_RADIUS,
1979 2.0 * CUBE_RADIUS,
1980 self.config.shells.min(MAX_SHELLS) as f32,
1981 if self.skip_bricks { 1.0 } else { 0.0 },
1982 ],
1983 light: {
1984 let [lx, ly, lz] = light_dir(&view, self.light_yaw, self.light_pitch);
1985 let solid = match self.config.present {
1986 Present::Glow => 0.0,
1987 Present::Solid => 1.0,
1988 };
1989 [lx, ly, lz, solid]
1990 },
1991 lit: [
1992 finite_or(self.key, DEFAULT_KEY).max(0.0),
1993 finite_or(self.ambient, DEFAULT_AMBIENT).max(0.0),
1994 finite_or(self.ao, DEFAULT_AO).clamp(0.0, 1.0),
1995 if cut.is_some() { 1.0 } else { 0.0 },
1996 ],
1997 cut: cut.unwrap_or([0.0; 4]),
1998 bounce: [self.applied_bounce(), 0.0, 0.0, 0.0],
1999 shells: {
2000 let mut out = [[0.0; 4]; (MAX_SHELLS / 4) as usize];
2001 for (slot, level) in out.iter_mut().flatten().zip(self.levels) {
2002 *slot = level;
2003 }
2004 out
2005 },
2006 }
2007 }
2008}
2009
2010impl Scene for VoxelScene {
2011 fn name(&self) -> &'static str {
2012 "voxel"
2013 }
2014
2015 fn advance(&mut self, dt: f32) {
2016 self.dt = dt;
2017 }
2018
2019 fn set_target_size(&mut self, width: u32, height: u32) {
2020 self.target = (width.max(1), height.max(1));
2023 }
2024
2025 fn set_grid_scale(&mut self, scale: GridScale) {
2026 self.field_scale = scale;
2027 }
2028
2029 fn set_occlude(&mut self, occlude: f32) {
2030 self.occlude = occlude;
2031 }
2032
2033 fn set_palette(&mut self, palette: &Palette) {
2034 self.palette = palette.clone();
2035 if let Some(res) = self.res.as_mut() {
2036 res.luts.set(palette);
2037 }
2038 }
2039
2040 fn configure(&mut self, cfg: &GeneratorConfig) -> Option<super::CapOverflow> {
2041 let GeneratorConfig::Voxel(config) = cfg else {
2042 return None;
2043 };
2044 let (config, overflow) = grid_clamp(*config, self.grid_cap);
2049 self.config = config;
2050 if self.res.as_ref().is_none_or(|res| res.grid != config.grid) {
2051 let mut built = Resources::build(&self.device, self.surface_format, config.grid);
2052 built.luts.set(&self.palette);
2053 self.res = Some(built);
2054 }
2055 self.needs_seed = true;
2056 self.clock = common::GenerationClock::default();
2057 self.pending_generations = 0;
2058 self.rises = 0;
2060 self.pending_stamp = None;
2061 self.prev_reseed = 0.0;
2062 overflow
2063 }
2064
2065 fn reset_params(&mut self) {
2066 self.rule = 0.0;
2067 self.step_rate = DEFAULT_STEP_RATE;
2068 self.reseed = 0.0;
2069 self.reseed_radius = DEFAULT_RESEED_RADIUS;
2070 self.shell_gain = DEFAULT_SHELL_GAIN;
2071 self.density = DEFAULT_DENSITY;
2072 self.trail = DEFAULT_TRAIL;
2073 self.age_tint = DEFAULT_AGE_TINT;
2074 self.hue_spread = DEFAULT_HUE_SPREAD;
2075 self.key = DEFAULT_KEY;
2076 self.ambient = DEFAULT_AMBIENT;
2077 self.ao = DEFAULT_AO;
2078 self.bounce = DEFAULT_BOUNCE;
2079 self.light_yaw = DEFAULT_LIGHT_YAW;
2080 self.light_pitch = DEFAULT_LIGHT_PITCH;
2081 self.cut = DEFAULT_CUT;
2082 self.cut_yaw = DEFAULT_CUT_YAW;
2083 self.cut_pitch = DEFAULT_CUT_PITCH;
2084 self.zoom = DEFAULT_ZOOM;
2085 self.colour.reset();
2086 self.pan.reset();
2087 self.camera.reset();
2088 }
2089
2090 fn set_param(&mut self, name: &str, value: f32) {
2091 if self.colour.set(name, value) || self.pan.set(name, value) {
2094 return;
2095 }
2096 match name {
2097 "yaw" => self.camera.yaw = value,
2103 "pitch" => self.camera.pitch = value,
2104 "distance" => self.camera.distance = value,
2105 "fov" => self.camera.fov = value,
2106 "focus" => self.camera.focus = value,
2107 "aperture" => self.camera.aperture = value,
2108 "fog" => self.camera.fog = value,
2109 "rule" => self.rule = value,
2110 "step_rate" => self.step_rate = value,
2111 "reseed" => self.reseed = value,
2112 "reseed_radius" => self.reseed_radius = value,
2113 "shell_gain" => self.shell_gain = value,
2114 "density" => self.density = value,
2115 "trail" => self.trail = value,
2116 "age_tint" => self.age_tint = value,
2117 "hue_spread" => self.hue_spread = value,
2118 "key" => self.key = value,
2119 "ambient" => self.ambient = value,
2120 "ao" => self.ao = value,
2121 "bounce" => self.bounce = value,
2122 "light_yaw" => self.light_yaw = value,
2123 "light_pitch" => self.light_pitch = value,
2124 "cut" => self.cut = value,
2125 "cut_yaw" => self.cut_yaw = value,
2126 "cut_pitch" => self.cut_pitch = value,
2127 "zoom" => self.zoom = value,
2128 _ => {}
2129 }
2130 }
2131
2132 fn update(&mut self, frame: &AnalysisFrame) {
2133 self.pending_generations = self
2134 .clock
2135 .advance(applied_step_rate(self.step_rate), self.dt);
2136 self.levels = shell_levels(&frame.spectrum, self.config.shells);
2137 let reseed = finite_or(self.reseed, 0.0);
2142 if reseed >= RESEED_THRESHOLD && self.prev_reseed < RESEED_THRESHOLD {
2143 self.pending_stamp = Some(stamp(
2144 self.config.salt,
2145 self.rises,
2146 self.config.grid,
2147 finite_or(self.reseed_radius, DEFAULT_RESEED_RADIUS),
2148 ));
2149 self.rises = self.rises.wrapping_add(1);
2150 }
2151 self.prev_reseed = reseed;
2152 }
2153
2154 fn render(
2155 &mut self,
2156 queue: &wgpu::Queue,
2157 encoder: &mut wgpu::CommandEncoder,
2158 view: &wgpu::TextureView,
2159 aspect: f32,
2160 ) {
2161 let march = self.march_params(aspect);
2163 let seed = std::mem::replace(&mut self.needs_seed, false);
2164 let stamp = self.pending_stamp.take();
2165 let generations = std::mem::take(&mut self.pending_generations);
2166 let step = self.step_params(stamp);
2169 let size = march_size(
2170 self.target,
2171 self.march_scale.get() * self.field_scale.get(),
2172 self.march_cap,
2173 );
2174 let Some(res) = self.res.as_mut() else {
2175 return;
2176 };
2177 res.luts.flush(queue);
2178 queue.write_buffer(&res.march_uniform, 0, bytemuck::bytes_of(&march));
2179 let changed = seed || stamp.is_some() || generations > 0;
2183 if changed {
2184 queue.write_buffer(&res.step_uniform, 0, bytemuck::bytes_of(&step));
2185 }
2186 if seed {
2187 res.encode_step(encoder, StepPass::Seed);
2188 }
2189 if stamp.is_some() {
2190 res.encode_step(encoder, StepPass::Stamp);
2191 }
2192 for _ in 0..generations {
2193 res.encode_step(encoder, StepPass::Step);
2194 }
2195 if changed {
2198 res.encode_bricks(encoder);
2199 res.light_fresh = false;
2200 }
2201 if march.bounce[0] > 0.0 {
2204 res.ensure_light(&self.device);
2205 if !res.light_fresh {
2206 res.encode_light(encoder);
2207 self.light_builds = self.light_builds.wrapping_add(1);
2208 }
2209 }
2210
2211 res.ensure_target(&self.device, self.surface_format, size);
2212 let Some(target) = res.target.as_ref() else {
2213 return;
2214 };
2215 {
2216 let mut pass = gpu::color_pass(
2219 encoder,
2220 "voxel-march-pass",
2221 &target.view,
2222 wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
2223 );
2224 pass.set_pipeline(&res.march_pipeline);
2225 pass.set_bind_group(
2226 0,
2227 if res.reading_a {
2228 &res.march_from_a
2229 } else {
2230 &res.march_from_b
2231 },
2232 &[],
2233 );
2234 pass.draw(0..3, 0..1);
2235 }
2236 let mut pass = gpu::color_pass(encoder, "voxel-present-pass", view, wgpu::LoadOp::Load);
2239 pass.set_pipeline(&res.present_pipeline);
2240 pass.set_bind_group(0, &target.present_bg, &[]);
2241 pass.draw(0..3, 0..1);
2242 }
2243}
2244
2245#[cfg(test)]
2246mod tests;