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