1#![deny(
57 clippy::unwrap_used,
58 clippy::expect_used,
59 clippy::indexing_slicing,
60 clippy::panic,
61 clippy::unreachable
62)]
63
64use crate::dsp::AnalysisFrame;
65use crate::render::feedback::PingPongField;
66use crate::render::gpu;
67use crate::render::palette::{self, Palette};
68
69use super::common;
70use super::{PerVertexBound, Phase, Scene, lines};
71
72mod encode;
78mod mesh;
79mod resources;
80mod shaders;
81
82pub use mesh::{DEFAULT_MESH, MAX_MESH, MIN_MESH, clamp_grid, vertex_count, vertex_position};
88
89use crate::render::scenes::{ParamGroup, ParamKind, ParamSpec, default_of};
90use mesh::*;
91use resources::*;
92use shaders::*;
93
94pub const MAX_SHAPE_VERTICES: usize = 96 * 1024;
102
103pub const PER_VERTEX_PARAMS: &[ParamSpec] = &[
113 ParamSpec {
114 name: "zoom",
115 default: 1.0,
116 range: Some([0.5, 2.0]),
117 doc: "Scale the previous frame is resampled at, per vertex; above 1 the past is magnified and the image travels outward.",
118 kind: ParamKind::Modal,
119 group: ParamGroup::Motion,
120 main: true,
121 },
122 ParamSpec {
123 name: "rot",
124 default: 0.0,
125 range: Some([-1.0, 1.0]),
126 doc: "Turns per second the resample is rotated by, per vertex.",
127 kind: ParamKind::Modal,
128 group: ParamGroup::Motion,
129 main: true,
130 },
131 ParamSpec {
132 name: "cx",
133 default: 0.5,
134 range: Some([0.0, 1.0]),
135 doc: "Horizontal point the per-vertex zoom and rotation pivot about, in uv.",
136 kind: ParamKind::Modal,
137 group: ParamGroup::Shape,
138 main: false,
139 },
140 ParamSpec {
141 name: "cy",
142 default: 0.5,
143 range: Some([0.0, 1.0]),
144 doc: "Vertical point the per-vertex zoom and rotation pivot about, in uv.",
145 kind: ParamKind::Modal,
146 group: ParamGroup::Shape,
147 main: false,
148 },
149 ParamSpec {
150 name: "dx",
151 default: 0.0,
152 range: Some([-1.0, 1.0]),
153 doc: "Sideways offset of the resample, in frame widths.",
154 kind: ParamKind::Modal,
155 group: ParamGroup::Motion,
156 main: false,
157 },
158 ParamSpec {
159 name: "dy",
160 default: 0.0,
161 range: Some([-1.0, 1.0]),
162 doc: "Vertical offset of the resample, in frame heights.",
163 kind: ParamKind::Modal,
164 group: ParamGroup::Motion,
165 main: false,
166 },
167 ParamSpec {
168 name: "sx",
169 default: 1.0,
170 range: Some([0.5, 2.0]),
171 doc: "Horizontal stretch of the resample, independently of `zoom`.",
172 kind: ParamKind::Modal,
173 group: ParamGroup::Shape,
174 main: false,
175 },
176 ParamSpec {
177 name: "sy",
178 default: 1.0,
179 range: Some([0.5, 2.0]),
180 doc: "Vertical stretch of the resample, independently of `zoom`.",
181 kind: ParamKind::Modal,
182 group: ParamGroup::Shape,
183 main: false,
184 },
185 ParamSpec {
186 name: "warp",
187 default: 0.0,
188 range: Some([0.0, 2.0]),
189 doc: "Amplitude of the travelling ripple added to the resample.",
190 kind: ParamKind::Modal,
191 group: ParamGroup::Shape,
192 main: true,
193 },
194];
195
196const PER_VERTEX_DEFAULTS: [f32; 9] = [1.0, 0.0, 0.5, 0.5, 0.0, 0.0, 1.0, 1.0, 0.0];
203
204const OUTPUTS: usize = PER_VERTEX_DEFAULTS.len();
207const _: () = assert!(
208 OUTPUTS == PER_VERTEX_PARAMS.len(),
209 "the per-vertex roster and its defaults must be the same length"
210);
211
212const DEFAULT_DECAY: f32 = default_of(PARAMS, "decay");
215const MAX_DECAY: f32 = 0.995;
220
221pub const MILKDROP_SOFTNESS: f32 = 1.0;
247
248const DEFAULT_WARP_SCALE: f32 = default_of(PARAMS, "warp_scale");
251const DEFAULT_WARP_SPEED: f32 = default_of(PARAMS, "warp_speed");
252
253const DEFAULT_DEPOSIT: f32 = default_of(PARAMS, "deposit");
256const DEFAULT_DEPOSIT_CENTRE: f32 = default_of(PARAMS, "deposit_x");
257const DEFAULT_DEPOSIT_RADIUS: f32 = default_of(PARAMS, "deposit_radius");
258const DEFAULT_DEPOSIT_WIDTH: f32 = default_of(PARAMS, "deposit_width");
259const DEFAULT_DEPOSIT_ARMS: f32 = default_of(PARAMS, "deposit_arms");
260const DEFAULT_DEPOSIT_TWIST: f32 = default_of(PARAMS, "deposit_twist");
261const DEFAULT_DEPOSIT_SPIN: f32 = default_of(PARAMS, "deposit_spin");
262
263pub const COMPOSITE_PARAMS: &[ParamSpec] = &[
289 ParamSpec {
290 name: "gamma",
291 default: 1.0,
292 range: Some([0.25, 4.0]),
293 doc: "Shapes the field's tone curve on its way out; below 1 lifts the mid tones.",
294 kind: ParamKind::Modal,
295 group: ParamGroup::Light,
296 main: false,
297 },
298 ParamSpec {
299 name: "wrap",
300 default: DEFAULT_COMPOSITE_FLAG,
301 range: Some([0.0, 1.0]),
302 doc: "Wraps a sample that leaves the frame back in at the opposite edge, instead of clamping.",
303 kind: ParamKind::Modal,
304 group: ParamGroup::Shape,
305 main: false,
306 },
307 ParamSpec {
308 name: "darken_center",
309 default: DEFAULT_COMPOSITE_FLAG,
310 range: Some([0.0, 1.0]),
311 doc: "Pulls brightness down toward the middle of the frame.",
312 kind: ParamKind::Modal,
313 group: ParamGroup::Light,
314 main: false,
315 },
316 ParamSpec {
317 name: "brighten",
318 default: DEFAULT_COMPOSITE_FLAG,
319 range: Some([0.0, 1.0]),
320 doc: "Lifts the field's bright end, MilkDrop's own brighten switch.",
321 kind: ParamKind::Modal,
322 group: ParamGroup::Light,
323 main: false,
324 },
325 ParamSpec {
326 name: "darken",
327 default: DEFAULT_COMPOSITE_FLAG,
328 range: Some([0.0, 1.0]),
329 doc: "Pushes the field's dark end down, MilkDrop's own darken switch.",
330 kind: ParamKind::Modal,
331 group: ParamGroup::Light,
332 main: false,
333 },
334 ParamSpec {
335 name: "solarize",
336 default: DEFAULT_COMPOSITE_FLAG,
337 range: Some([0.0, 1.0]),
338 doc: "Inverts the field above its midpoint, so highlights fold back into shadow.",
339 kind: ParamKind::Modal,
340 group: ParamGroup::Colour,
341 main: false,
342 },
343 ParamSpec {
344 name: "invert",
345 default: DEFAULT_COMPOSITE_FLAG,
346 range: Some([0.0, 1.0]),
347 doc: "Inverts the whole field.",
348 kind: ParamKind::Modal,
349 group: ParamGroup::Colour,
350 main: false,
351 },
352 ParamSpec {
353 name: "echo_alpha",
354 default: 0.0,
355 range: Some([0.0, 1.0]),
356 doc: "How strongly a second, scaled copy of the field is blended over the first.",
357 kind: ParamKind::Modal,
358 group: ParamGroup::Post,
359 main: false,
360 },
361 ParamSpec {
362 name: "echo_zoom",
363 default: 1.0,
364 range: Some([0.25, 4.0]),
365 doc: "How much larger or smaller that echoed copy is.",
366 kind: ParamKind::Modal,
367 group: ParamGroup::Post,
368 main: false,
369 },
370 ParamSpec {
371 name: "echo_orient",
372 default: 0.0,
373 range: Some([0.0, 3.0]),
374 doc: "Which way the echoed copy is flipped before it is blended.",
375 kind: ParamKind::Structural,
376 group: ParamGroup::Post,
377 main: false,
378 },
379];
380
381const DEFAULT_GAMMA: f32 = default_of(PARAMS, "gamma");
383const DEFAULT_COMPOSITE_FLAG: f32 = 0.0;
385const DEFAULT_ECHO_ALPHA: f32 = default_of(PARAMS, "echo_alpha");
388const DEFAULT_ECHO_ZOOM: f32 = default_of(PARAMS, "echo_zoom");
389const DEFAULT_ECHO_ORIENT: f32 = default_of(PARAMS, "echo_orient");
390
391fn echo_orientation(v: f32) -> u8 {
413 if !v.is_finite() {
414 return 0;
415 }
416 match (v.trunc() % 4.0) as i32 {
417 1 | -1 | -3 => 1,
418 2 => 2,
419 3 => 3,
420 _ => 0,
421 }
422}
423
424const DARKEN_CENTER_STRENGTH: f32 = 0.22;
431
432const DEFAULT_HUE: f32 = 0.0;
435const DEFAULT_COLOR_SPAN: f32 = default_of(PARAMS, "color_span");
436const DEFAULT_COLOR_CENTER: f32 = default_of(PARAMS, "color_center");
437const DEFAULT_BRIGHTNESS: f32 = 1.0;
438
439pub const PARAMS: &[ParamSpec] = &[
442 ParamSpec {
443 name: "zoom",
444 default: 1.0,
445 range: Some([0.5, 2.0]),
446 doc: "Scale the previous frame is resampled at, per vertex; above 1 the past is magnified and the image travels outward.",
447 kind: ParamKind::Modal,
448 group: ParamGroup::Motion,
449 main: true,
450 },
451 ParamSpec {
452 name: "rot",
453 default: 0.0,
454 range: Some([-1.0, 1.0]),
455 doc: "Turns per second the resample is rotated by, per vertex.",
456 kind: ParamKind::Modal,
457 group: ParamGroup::Motion,
458 main: true,
459 },
460 ParamSpec {
461 name: "cx",
462 default: 0.5,
463 range: Some([0.0, 1.0]),
464 doc: "Horizontal point the per-vertex zoom and rotation pivot about, in uv.",
465 kind: ParamKind::Modal,
466 group: ParamGroup::Shape,
467 main: false,
468 },
469 ParamSpec {
470 name: "cy",
471 default: 0.5,
472 range: Some([0.0, 1.0]),
473 doc: "Vertical point the per-vertex zoom and rotation pivot about, in uv.",
474 kind: ParamKind::Modal,
475 group: ParamGroup::Shape,
476 main: false,
477 },
478 ParamSpec {
479 name: "dx",
480 default: 0.0,
481 range: Some([-1.0, 1.0]),
482 doc: "Sideways offset of the resample, in frame widths.",
483 kind: ParamKind::Modal,
484 group: ParamGroup::Motion,
485 main: false,
486 },
487 ParamSpec {
488 name: "dy",
489 default: 0.0,
490 range: Some([-1.0, 1.0]),
491 doc: "Vertical offset of the resample, in frame heights.",
492 kind: ParamKind::Modal,
493 group: ParamGroup::Motion,
494 main: false,
495 },
496 ParamSpec {
497 name: "sx",
498 default: 1.0,
499 range: Some([0.5, 2.0]),
500 doc: "Horizontal stretch of the resample, independently of `zoom`.",
501 kind: ParamKind::Modal,
502 group: ParamGroup::Shape,
503 main: false,
504 },
505 ParamSpec {
506 name: "sy",
507 default: 1.0,
508 range: Some([0.5, 2.0]),
509 doc: "Vertical stretch of the resample, independently of `zoom`.",
510 kind: ParamKind::Modal,
511 group: ParamGroup::Shape,
512 main: false,
513 },
514 ParamSpec {
515 name: "warp",
516 default: 0.0,
517 range: Some([0.0, 2.0]),
518 doc: "Amplitude of the travelling ripple added to the resample.",
519 kind: ParamKind::Modal,
520 group: ParamGroup::Shape,
521 main: true,
522 },
523 ParamSpec {
524 name: "warp_scale",
525 default: 1.0,
526 range: Some([0.1, 4.0]),
527 doc: "Spatial frequency of the ripple; higher makes it finer.",
528 kind: ParamKind::Modal,
529 group: ParamGroup::Shape,
530 main: false,
531 },
532 ParamSpec {
533 name: "warp_speed",
534 default: 1.0,
535 range: Some([0.0, 4.0]),
536 doc: "How fast the ripple travels, as a multiple of its base rate.",
537 kind: ParamKind::Modal,
538 group: ParamGroup::Motion,
539 main: false,
540 },
541 ParamSpec {
542 name: "decay",
543 default: 0.72,
544 range: Some([0.0, 1.0]),
545 doc: "How much of the field survives each second, which is what sets the trail's length.",
546 kind: ParamKind::Modal,
547 group: ParamGroup::Light,
548 main: true,
549 },
550 ParamSpec {
551 name: "deposit",
552 default: 1.6,
553 range: Some([0.0, 8.0]),
554 doc: "How much light the source figure adds into the field each frame.",
555 kind: ParamKind::Modal,
556 group: ParamGroup::Shape,
557 main: true,
558 },
559 ParamSpec {
560 name: "deposit_x",
561 default: 0.5,
562 range: Some([0.0, 1.0]),
563 doc: "Horizontal position of the deposited figure, in uv.",
564 kind: ParamKind::Modal,
565 group: ParamGroup::Shape,
566 main: false,
567 },
568 ParamSpec {
569 name: "deposit_y",
570 default: 0.5,
571 range: Some([0.0, 1.0]),
572 doc: "Vertical position of the deposited figure, in uv.",
573 kind: ParamKind::Modal,
574 group: ParamGroup::Shape,
575 main: false,
576 },
577 ParamSpec {
578 name: "deposit_radius",
579 default: 0.45,
580 range: Some([0.0, 1.0]),
581 doc: "Radius of the deposited ring.",
582 kind: ParamKind::Modal,
583 group: ParamGroup::Shape,
584 main: false,
585 },
586 ParamSpec {
587 name: "deposit_width",
588 default: 0.11,
589 range: Some([0.0, 0.5]),
590 doc: "How thick that ring is; narrow reads as a wire, wide as a disc.",
591 kind: ParamKind::Modal,
592 group: ParamGroup::Shape,
593 main: false,
594 },
595 ParamSpec {
596 name: "deposit_arms",
597 default: 0.0,
598 range: Some([0.0, 16.0]),
599 doc: "How many arms the ring is broken into, as a whole number of arms; 0 leaves it \
600 whole.",
601 kind: ParamKind::Structural,
602 group: ParamGroup::Shape,
603 main: false,
604 },
605 ParamSpec {
606 name: "deposit_twist",
607 default: 0.0,
608 range: Some([-2.0, 2.0]),
609 doc: "Sweeps the arms into a spiral rather than leaving them radial.",
610 kind: ParamKind::Modal,
611 group: ParamGroup::Shape,
612 main: false,
613 },
614 ParamSpec {
615 name: "deposit_spin",
616 default: 0.0,
617 range: Some([-2.0, 2.0]),
618 doc: "Turns per second the deposited figure rotates by.",
619 kind: ParamKind::Modal,
620 group: ParamGroup::Motion,
621 main: false,
622 },
623 ParamSpec {
624 name: "gamma",
625 default: 1.0,
626 range: Some([0.25, 4.0]),
627 doc: "Shapes the field's tone curve on its way out; below 1 lifts the mid tones.",
628 kind: ParamKind::Modal,
629 group: ParamGroup::Light,
630 main: false,
631 },
632 ParamSpec {
633 name: "wrap",
634 default: DEFAULT_COMPOSITE_FLAG,
635 range: Some([0.0, 1.0]),
636 doc: "Wraps a sample that leaves the frame back in at the opposite edge, instead of clamping.",
637 kind: ParamKind::Modal,
638 group: ParamGroup::Shape,
639 main: false,
640 },
641 ParamSpec {
642 name: "darken_center",
643 default: DEFAULT_COMPOSITE_FLAG,
644 range: Some([0.0, 1.0]),
645 doc: "Pulls brightness down toward the middle of the frame.",
646 kind: ParamKind::Modal,
647 group: ParamGroup::Light,
648 main: false,
649 },
650 ParamSpec {
651 name: "brighten",
652 default: DEFAULT_COMPOSITE_FLAG,
653 range: Some([0.0, 1.0]),
654 doc: "Lifts the field's bright end, MilkDrop's own brighten switch.",
655 kind: ParamKind::Modal,
656 group: ParamGroup::Light,
657 main: false,
658 },
659 ParamSpec {
660 name: "darken",
661 default: DEFAULT_COMPOSITE_FLAG,
662 range: Some([0.0, 1.0]),
663 doc: "Pushes the field's dark end down, MilkDrop's own darken switch.",
664 kind: ParamKind::Modal,
665 group: ParamGroup::Light,
666 main: false,
667 },
668 ParamSpec {
669 name: "solarize",
670 default: DEFAULT_COMPOSITE_FLAG,
671 range: Some([0.0, 1.0]),
672 doc: "Inverts the field above its midpoint, so highlights fold back into shadow.",
673 kind: ParamKind::Modal,
674 group: ParamGroup::Colour,
675 main: false,
676 },
677 ParamSpec {
678 name: "invert",
679 default: DEFAULT_COMPOSITE_FLAG,
680 range: Some([0.0, 1.0]),
681 doc: "Inverts the whole field.",
682 kind: ParamKind::Modal,
683 group: ParamGroup::Colour,
684 main: false,
685 },
686 ParamSpec {
687 name: "echo_alpha",
688 default: 0.0,
689 range: Some([0.0, 1.0]),
690 doc: "How strongly a second, scaled copy of the field is blended over the first.",
691 kind: ParamKind::Modal,
692 group: ParamGroup::Post,
693 main: false,
694 },
695 ParamSpec {
696 name: "echo_zoom",
697 default: 1.0,
698 range: Some([0.25, 4.0]),
699 doc: "How much larger or smaller that echoed copy is.",
700 kind: ParamKind::Modal,
701 group: ParamGroup::Post,
702 main: false,
703 },
704 ParamSpec {
705 name: "echo_orient",
706 default: 0.0,
707 range: Some([0.0, 3.0]),
708 doc: "Which way the echoed copy is flipped before it is blended.",
709 kind: ParamKind::Structural,
710 group: ParamGroup::Post,
711 main: false,
712 },
713 crate::render::scenes::common::hue(DEFAULT_HUE),
714 ParamSpec {
715 name: "color_span",
716 default: 1.0,
717 range: Some([0.0, 1.0]),
718 doc: "How much of the palette the field's range covers.",
719 kind: ParamKind::Modal,
720 group: ParamGroup::Colour,
721 main: false,
722 },
723 ParamSpec {
724 name: "color_center",
725 default: 0.0,
726 range: Some([-1.0, 1.0]),
727 doc: "Shifts which part of that range lands in the middle of the palette.",
728 kind: ParamKind::Modal,
729 group: ParamGroup::Colour,
730 main: false,
731 },
732 ParamSpec {
733 name: "color_source",
734 default: 0.0,
735 range: Some([0.0, 1.0]),
736 doc: "Where the field takes its colour: 0 the deposit's own angle, 1 the light it has built up.",
737 kind: ParamKind::Structural,
738 group: ParamGroup::Colour,
739 main: false,
740 },
741 ParamSpec {
742 name: "coverage_threshold",
743 default: 0.0,
744 range: Some([0.0, 1.0]),
745 doc: "In level mode, the coverage a pixel needs to hold the ink: at or above it the palette's colour, below it the backdrop. 0 is off.",
746 kind: ParamKind::Modal,
747 group: ParamGroup::Light,
748 main: false,
749 },
750 crate::render::scenes::common::SATURATION,
751 crate::render::scenes::common::PALETTE_MIX,
752 crate::render::scenes::common::PALETTE_STEPS,
753 crate::render::scenes::common::PALETTE_CONTOUR,
754 crate::render::scenes::common::PALETTE_CONTOUR_STYLE,
755 crate::render::scenes::common::PALETTE_CONTOUR_INK,
756 crate::render::scenes::common::brightness(DEFAULT_BRIGHTNESS),
757];
758
759const DEFAULT_COLOR_SOURCE: f32 = default_of(PARAMS, "color_source");
762
763const DEFAULT_COVERAGE_THRESHOLD: f32 = default_of(PARAMS, "coverage_threshold");
770
771fn colour_source(v: f32) -> f32 {
780 if v.is_finite() {
781 v.clamp(0.0, 1.0).round()
782 } else {
783 DEFAULT_COLOR_SOURCE
784 }
785}
786
787pub struct WarpMeshScene {
789 device: wgpu::Device,
790 surface_format: wgpu::TextureFormat,
791 res: Option<Resources>,
792 tier_mesh: (u32, u32),
795 requested_mesh: (u32, u32),
797 state: MeshState,
798 target: (u32, u32),
801 last_aspect: f32,
805 time: f32,
806 dt: f32,
807 scalars: [f32; OUTPUTS],
810 warp_scale: f32,
811 warp_speed: f32,
812 warp_phase: Phase,
817 decay: f32,
818 deposit: f32,
819 deposit_x: f32,
820 deposit_y: f32,
821 deposit_radius: f32,
822 deposit_width: f32,
823 deposit_arms: f32,
824 deposit_twist: f32,
825 deposit_spin: f32,
826 deposit_phase: Phase,
830 gamma: f32,
831 wrap: f32,
832 darken_center: f32,
833 brighten: f32,
834 darken: f32,
835 solarize: f32,
836 invert: f32,
837 echo_alpha: f32,
838 echo_zoom: f32,
839 echo_orient: f32,
840 colour: common::PaletteParams,
842 color_span: f32,
843 color_center: f32,
844 color_source: f32,
847 coverage_threshold: f32,
851 occlude: f32,
852 palette: Palette,
856 milk: Option<crate::milk::MilkRuntime>,
868 shader_spec: Option<shader::ShaderSpec>,
872 quantize_steps: f32,
880 max_segments: usize,
883 draw: Option<crate::milk::outputs::FrameOutputs>,
886 geometry: draw::DrawGeometry,
889 frame: crate::dsp::AnalysisFrame,
892}
893
894impl WarpMeshScene {
895 #[cfg(test)]
906 pub(crate) fn field_texture(&self) -> Option<&wgpu::Texture> {
907 Some(self.res.as_ref()?.field.read_texture())
908 }
909
910 pub fn new(
914 device: &wgpu::Device,
915 surface_format: wgpu::TextureFormat,
916 tier_mesh: (u32, u32),
917 max_segments: usize,
918 ) -> Self {
919 let tier = crate::render::TierConfig {
920 mesh_grid: tier_mesh,
921 ..crate::render::TierConfig::FLOOR
922 };
923 let mesh = clamp_grid(DEFAULT_MESH, &tier);
924 Self {
925 device: device.clone(),
926 surface_format,
927 res: None,
928 tier_mesh,
929 requested_mesh: DEFAULT_MESH,
930 state: MeshState::new(mesh),
931 target: (0, 0),
932 last_aspect: 1.0,
933 time: 0.0,
934 dt: super::FALLBACK_DT,
935 scalars: PER_VERTEX_DEFAULTS,
936 warp_scale: DEFAULT_WARP_SCALE,
937 warp_speed: DEFAULT_WARP_SPEED,
938 warp_phase: Phase::default(),
939 decay: DEFAULT_DECAY,
940 deposit: DEFAULT_DEPOSIT,
941 deposit_x: DEFAULT_DEPOSIT_CENTRE,
942 deposit_y: DEFAULT_DEPOSIT_CENTRE,
943 deposit_radius: DEFAULT_DEPOSIT_RADIUS,
944 deposit_width: DEFAULT_DEPOSIT_WIDTH,
945 deposit_arms: DEFAULT_DEPOSIT_ARMS,
946 deposit_twist: DEFAULT_DEPOSIT_TWIST,
947 deposit_spin: DEFAULT_DEPOSIT_SPIN,
948 deposit_phase: Phase::default(),
949 gamma: DEFAULT_GAMMA,
950 wrap: DEFAULT_COMPOSITE_FLAG,
951 darken_center: DEFAULT_COMPOSITE_FLAG,
952 brighten: DEFAULT_COMPOSITE_FLAG,
953 darken: DEFAULT_COMPOSITE_FLAG,
954 solarize: DEFAULT_COMPOSITE_FLAG,
955 invert: DEFAULT_COMPOSITE_FLAG,
956 echo_alpha: DEFAULT_ECHO_ALPHA,
957 echo_zoom: DEFAULT_ECHO_ZOOM,
958 echo_orient: DEFAULT_ECHO_ORIENT,
959 colour: common::PaletteParams::new(DEFAULT_HUE, DEFAULT_BRIGHTNESS),
960 color_span: DEFAULT_COLOR_SPAN,
961 color_center: DEFAULT_COLOR_CENTER,
962 color_source: DEFAULT_COLOR_SOURCE,
963 coverage_threshold: DEFAULT_COVERAGE_THRESHOLD,
964 occlude: crate::render::post::DEFAULT_OCCLUDE,
965 palette: Palette::default_spectrum(),
966 milk: None,
967 shader_spec: None,
968 quantize_steps: 0.0,
969 max_segments,
970 draw: None,
971 geometry: draw::DrawGeometry::default(),
972 frame: crate::dsp::AnalysisFrame::default(),
973 }
974 }
975
976 pub fn mesh(&self) -> (u32, u32) {
980 self.state.mesh
981 }
982}
983
984impl PerVertexBound for WarpMeshScene {
985 fn set_per_vertex(&mut self, name: &str, values: &[f32]) {
986 let Some(index) = PER_VERTEX_PARAMS.iter().position(|spec| spec.name == name) else {
987 return;
988 };
989 let Some(slot) = self.state.values.get_mut(index) else {
990 return;
991 };
992 let n = slot.len().min(values.len());
997 if let (Some(dst), Some(src)) = (slot.get_mut(..n), values.get(..n)) {
998 dst.copy_from_slice(src);
999 }
1000 if let Some(flag) = self.state.bound.get_mut(index) {
1001 *flag = n > 0;
1002 }
1003 }
1004}
1005
1006#[cfg(test)]
1007impl super::FeedbackSource for WarpMeshScene {
1008 fn feedback_field(&self) -> Option<&wgpu::Texture> {
1009 self.field_texture()
1010 }
1011}
1012
1013impl Scene for WarpMeshScene {
1014 fn name(&self) -> &'static str {
1015 "warp mesh"
1016 }
1017
1018 fn as_per_vertex_bound(&mut self) -> Option<&mut dyn PerVertexBound> {
1019 Some(self)
1020 }
1021
1022 #[cfg(test)]
1023 fn as_feedback_source(&self) -> Option<&dyn super::FeedbackSource> {
1024 Some(self)
1025 }
1026
1027 fn set_time(&mut self, time: f32) {
1028 self.time = time;
1029 }
1030
1031 fn advance(&mut self, dt: f32) {
1032 self.dt = dt;
1035 }
1036
1037 fn set_occlude(&mut self, occlude: f32) {
1038 self.occlude = occlude;
1039 }
1040
1041 fn set_target_size(&mut self, width: u32, height: u32) {
1042 self.target = (width.max(1), height.max(1));
1044 }
1045
1046 fn set_palette(&mut self, palette: &Palette) {
1047 self.palette = palette.clone();
1048 if let Some(res) = self.res.as_mut() {
1049 res.luts.set(palette);
1050 }
1051 }
1052
1053 fn configure(&mut self, cfg: &super::GeneratorConfig) -> Option<super::CapOverflow> {
1054 if let super::GeneratorConfig::WarpMesh { mesh, milk, salt } = cfg {
1055 self.requested_mesh = *mesh;
1056 let tier = crate::render::TierConfig {
1057 mesh_grid: self.tier_mesh,
1058 ..crate::render::TierConfig::FLOOR
1059 };
1060 self.state.resize(clamp_grid(*mesh, &tier));
1061 self.milk = milk
1066 .as_ref()
1067 .map(|bundle| crate::milk::MilkRuntime::new((**bundle).clone(), *salt));
1068 self.quantize_steps = milk.as_ref().map_or(0.0, |bundle| bundle.quantize_steps);
1074 self.shader_spec = milk.as_ref().and_then(|bundle| {
1076 (bundle.warp_wgsl.is_some() || bundle.comp_wgsl.is_some()).then(|| {
1077 shader::ShaderSpec {
1078 warp: bundle.warp_wgsl.clone(),
1079 comp: bundle.comp_wgsl.clone(),
1080 blur: bundle.blur_level,
1081 }
1082 })
1083 });
1084 self.draw = None;
1087 self.geometry.clear();
1088 }
1089 None
1090 }
1091
1092 fn reset_params(&mut self) {
1093 self.scalars = PER_VERTEX_DEFAULTS;
1094 self.state.bound = [false; OUTPUTS];
1097 self.warp_scale = DEFAULT_WARP_SCALE;
1098 self.warp_speed = DEFAULT_WARP_SPEED;
1099 self.decay = DEFAULT_DECAY;
1100 self.deposit = DEFAULT_DEPOSIT;
1101 self.deposit_x = DEFAULT_DEPOSIT_CENTRE;
1102 self.deposit_y = DEFAULT_DEPOSIT_CENTRE;
1103 self.deposit_radius = DEFAULT_DEPOSIT_RADIUS;
1104 self.deposit_width = DEFAULT_DEPOSIT_WIDTH;
1105 self.deposit_arms = DEFAULT_DEPOSIT_ARMS;
1106 self.deposit_twist = DEFAULT_DEPOSIT_TWIST;
1107 self.deposit_spin = DEFAULT_DEPOSIT_SPIN;
1108 self.gamma = DEFAULT_GAMMA;
1109 self.wrap = DEFAULT_COMPOSITE_FLAG;
1110 self.darken_center = DEFAULT_COMPOSITE_FLAG;
1111 self.brighten = DEFAULT_COMPOSITE_FLAG;
1112 self.darken = DEFAULT_COMPOSITE_FLAG;
1113 self.solarize = DEFAULT_COMPOSITE_FLAG;
1114 self.invert = DEFAULT_COMPOSITE_FLAG;
1115 self.echo_alpha = DEFAULT_ECHO_ALPHA;
1116 self.echo_zoom = DEFAULT_ECHO_ZOOM;
1117 self.echo_orient = DEFAULT_ECHO_ORIENT;
1118 self.colour.reset();
1119 self.color_span = DEFAULT_COLOR_SPAN;
1120 self.color_center = DEFAULT_COLOR_CENTER;
1121 self.color_source = DEFAULT_COLOR_SOURCE;
1122 self.coverage_threshold = DEFAULT_COVERAGE_THRESHOLD;
1123 }
1124
1125 fn set_param(&mut self, name: &str, value: f32) {
1126 if self.colour.set(name, value) {
1129 return;
1130 }
1131 if let Some(index) = PER_VERTEX_PARAMS.iter().position(|spec| spec.name == name) {
1134 if let Some(slot) = self.scalars.get_mut(index) {
1135 *slot = value;
1136 }
1137 return;
1138 }
1139 match name {
1140 "warp_scale" => self.warp_scale = value,
1141 "warp_speed" => self.warp_speed = value,
1142 "decay" => self.decay = value,
1143 "deposit" => self.deposit = value,
1144 "deposit_x" => self.deposit_x = value,
1145 "deposit_y" => self.deposit_y = value,
1146 "deposit_radius" => self.deposit_radius = value,
1147 "deposit_width" => self.deposit_width = value,
1148 "deposit_arms" => self.deposit_arms = value,
1149 "deposit_twist" => self.deposit_twist = value,
1150 "deposit_spin" => self.deposit_spin = value,
1151 "gamma" => self.gamma = value,
1152 "wrap" => self.wrap = value,
1153 "darken_center" => self.darken_center = value,
1154 "brighten" => self.brighten = value,
1155 "darken" => self.darken = value,
1156 "solarize" => self.solarize = value,
1157 "invert" => self.invert = value,
1158 "echo_alpha" => self.echo_alpha = value,
1159 "echo_zoom" => self.echo_zoom = value,
1160 "echo_orient" => self.echo_orient = value,
1161 "color_span" => self.color_span = value,
1162 "color_center" => self.color_center = value,
1163 "color_source" => self.color_source = value,
1164 "coverage_threshold" => self.coverage_threshold = value,
1165 _ => {}
1166 }
1167 }
1168
1169 fn update(&mut self, frame: &AnalysisFrame) {
1170 self.warp_phase.step(self.warp_speed, self.dt);
1173 self.deposit_phase.step(self.deposit_spin, self.dt);
1174 self.frame = *frame;
1177 let aspect = self.last_aspect;
1186 let mesh = self.state.mesh;
1187 let (time, dt) = (self.time, self.dt);
1188 if let Some(runtime) = self.milk.as_mut() {
1189 let (transform, out) = runtime.run_frame(&self.frame, time, dt, mesh, aspect);
1190 for (index, value) in transform.iter().enumerate() {
1191 if let Some(slot) = self.scalars.get_mut(index) {
1192 *slot = *value;
1193 }
1194 }
1195 self.decay = out.decay;
1198 self.gamma = out.gamma;
1199 self.wrap = out.wrap;
1200 self.darken_center = out.darken_center;
1201 self.brighten = out.brighten;
1202 self.darken = out.darken;
1203 self.solarize = out.solarize;
1204 self.invert = out.invert;
1205 self.echo_alpha = out.echo_alpha;
1206 self.echo_zoom = out.echo_zoom;
1207 self.echo_orient = out.echo_orient;
1208 self.draw = Some(out);
1218 self.state.bound = [false; OUTPUTS];
1222 }
1223 }
1224
1225 fn render(
1226 &mut self,
1227 queue: &wgpu::Queue,
1228 encoder: &mut wgpu::CommandEncoder,
1229 view: &wgpu::TextureView,
1230 aspect: f32,
1231 ) {
1232 let size = if self.target == (0, 0) {
1233 (256, 256)
1237 } else {
1238 self.target
1239 };
1240 if !encode::ensure_resources(self, queue, encoder, size) {
1241 return;
1242 }
1243
1244 self.last_aspect = aspect;
1245 encode::prepare_mesh(self, aspect);
1246
1247 let dt = self.dt;
1248 if let Some(res) = self.res.as_ref() {
1249 encode::upload_uniforms(self, res, queue, aspect, size, dt);
1250 encode::encode_warp(res, encoder);
1251 encode::encode_deposit(res, encoder);
1252 }
1253 encode::encode_draw_layer(self, queue, encoder, aspect, dt);
1254 if let Some(res) = self.res.as_ref() {
1255 encode::encode_blur(res, encoder);
1256 encode::encode_present(res, encoder, view);
1257 }
1258 }
1259}
1260
1261pub mod draw;
1262mod shader;
1263
1264#[cfg(test)]
1265mod tests;