1#![deny(
29 clippy::unwrap_used,
30 clippy::expect_used,
31 clippy::indexing_slicing,
32 clippy::panic,
33 clippy::unreachable
34)]
35
36use crate::render::gpu;
37
38use super::common;
39use super::{Scene, SeededRng};
40use crate::dsp::AnalysisFrame;
41use crate::render::feedback::PingPongField;
42use crate::render::palette::{self, Palette};
43use crate::render::scenes::{ParamKind, ParamSpec, default_of};
44
45const GRID: u32 = 256;
51
52const FIXED_STEP: f32 = 1.0 / 720.0;
57
58const MAX_SUBSTEPS: u32 = 40;
63
64const SEED_BLOBS: usize = 30;
66const MAX_BLOBS: usize = 32;
67const SEED: u64 = 0x4C4D_565F_5244_5F31;
71
72const DEFAULT_FEED: f32 = default_of(PARAMS, "feed");
76const DEFAULT_KILL: f32 = default_of(PARAMS, "kill");
77const DIFFUSE_U: f32 = 0.16;
82const DIFFUSE_V: f32 = 0.08;
83const DEFAULT_FLOW: f32 = default_of(PARAMS, "flow");
85
86const DEFAULT_HUE: f32 = 0.0;
89const DEFAULT_CONTOUR: f32 = default_of(PARAMS, "contour");
90const DEFAULT_HATCH: f32 = default_of(PARAMS, "hatch");
91const DEFAULT_GLOW: f32 = default_of(PARAMS, "glow");
92const DEFAULT_COLOR_SPAN: f32 = default_of(PARAMS, "color_span");
97const DEFAULT_COLOR_CENTER: f32 = default_of(PARAMS, "color_center");
98const DEFAULT_ZOOM: f32 = 1.0;
101
102const INJECT_RADIUS: f32 = 0.045;
106const INJECT_AMOUNT: f32 = 0.85;
107const INJECT_SEED: u64 = 0x4C4D_5244_494E_4A31; const INJECT_THRESHOLD: f32 = 0.5;
110
111const INIT_SHADER: &str = r#"
113struct Init {
114 blobs: array<vec4<f32>, 32>, // xy: center (uv), z: radius, w: unused
115 count: vec4<u32>, // x: active blob count
116}
117@group(0) @binding(0) var<uniform> init: Init;
118
119@fragment
120fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
121 var v = 0.0;
122 let n = init.count.x;
123 for (var i = 0u; i < n; i = i + 1u) {
124 let b = init.blobs[i];
125 if (distance(in.uv, b.xy) < b.z) {
126 v = 1.0;
127 }
128 }
129 return vec4<f32>(1.0 - v, v, 0.0, 1.0);
130}
131"#;
132
133const SIM_SHADER: &str = r#"
135struct Sim {
136 p: vec4<f32>, // x: feed, y: kill, z: diffuse_u, w: diffuse_v
137 inj: vec4<f32>, // xy: stamp center (uv), z: radius, w: amount (0 = no stamp)
138}
139@group(0) @binding(0) var<uniform> sim: Sim;
140@group(0) @binding(1) var field: texture_2d<f32>;
141
142// Toroidal texel fetch (wrap at the edges) of the (U, V) pair.
143fn ld(c: vec2<i32>, size: vec2<i32>) -> vec2<f32> {
144 let w = ((c % size) + size) % size;
145 return textureLoad(field, w, 0).xy;
146}
147
148@fragment
149fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
150 let size = vec2<i32>(textureDimensions(field));
151 let c = vec2<i32>(i32(in.pos.x), i32(in.pos.y));
152 let m = ld(c, size);
153 let u = m.x;
154 let v = m.y;
155
156 // 3×3 Laplacian: orthogonal 0.2, diagonal 0.05, center -1.
157 var lap = ld(c + vec2<i32>(-1, 0), size) * 0.2
158 + ld(c + vec2<i32>(1, 0), size) * 0.2
159 + ld(c + vec2<i32>(0, -1), size) * 0.2
160 + ld(c + vec2<i32>(0, 1), size) * 0.2
161 + ld(c + vec2<i32>(-1, -1), size) * 0.05
162 + ld(c + vec2<i32>(1, -1), size) * 0.05
163 + ld(c + vec2<i32>(-1, 1), size) * 0.05
164 + ld(c + vec2<i32>(1, 1), size) * 0.05;
165 lap = lap - m;
166
167 let feed = sim.p.x;
168 let kill = sim.p.y;
169 let du = sim.p.z;
170 let dv = sim.p.w;
171 let reaction = u * v * v;
172 let nu = u + du * lap.x - reaction + feed * (1.0 - u);
173 var nv = v + dv * lap.y + reaction - (kill + feed) * v;
174
175 // Beat-stamped seed injection (Phase 3), folded into the sim so no extra
176 // pipeline is needed. `inj.w` is non-zero only on the stamp frame; it is
177 // applied on every sub-step of that frame, so V saturates at the stamp.
178 let stamp = sim.inj.w * (1.0 - smoothstep(sim.inj.z * 0.4, sim.inj.z, distance(in.uv, sim.inj.xy)));
179 nv = nv + stamp;
180
181 return vec4<f32>(clamp(nu, 0.0, 1.0), clamp(nv, 0.0, 1.0), 0.0, 1.0);
182}
183"#;
184
185const PRESENT_SHADER: &str = r#"
189struct Present {
190 // x: hue, y: contour density, z: hatch frequency (texels), w: glow
191 a: vec4<f32>,
192 // x: color_span, y: color_center, z: saturation, w: palette_mix
193 b: vec4<f32>,
194 // x: zoom, yz: pan (field-space view transform, ADR-0018), w: occlude (ADR-0085)
195 c: vec4<f32>,
196 // x: palette_steps (integral, quantized CPU-side), y: palette_contour
197 // (ADR-0078), zw: unused
198 d: vec4<f32>,
199}
200@group(0) @binding(0) var present_field: texture_2d<f32>;
201@group(0) @binding(1) var present_samp: sampler;
202@group(0) @binding(2) var<uniform> pp: Present;
203// Shared gradient LUTs (ADR-0021): A/B for the `palette_mix` crossfade, one
204// repeat sampler. Kept in this present bind group (a unique 6-entry layout) so it
205// never matches another pipeline's layout on the DX12 WARP software adapter.
206@group(0) @binding(3) var lut_a: texture_2d<f32>;
207@group(0) @binding(4) var lut_b: texture_2d<f32>;
208@group(0) @binding(5) var lut_samp: sampler;
209
210// Shared `saturation` (mirrors core/src/render/palette.rs::desaturate verbatim).
211fn apply_saturation(c: vec3<f32>, s: f32) -> vec3<f32> {
212 let luma = dot(c, vec3<f32>(0.299, 0.587, 0.114));
213 return vec3<f32>(luma) + (c - vec3<f32>(luma)) * s;
214}
215
216// Shared `palette_steps` (mirrors core/src/render/palette.rs::band_coord
217// verbatim, ADR-0078): snap the palette coordinate to a band centre before the
218// LUT read. Below 1.5 steps it is the exact identity, not a one-band degenerate.
219fn band_coord(t: f32, steps: f32) -> f32 {
220 if (steps < 1.5) {
221 return t;
222 }
223 return (floor(t * steps) + 0.5) / steps;
224}
225
226// Shared `palette_contour` (ADR-0078 / ADR-0133; the WGSL is the implementation,
227// copied verbatim at each fragment-stage site — palette.rs has no CPU
228// counterpart to be canonical, since `fwidth` exists only here).
229//
230// Darkens within one PIXEL of a band edge, so the line has the same weight where
231// the field is shallow and where it is steep — AND ONLY WHERE THE INK ACTUALLY
232// CHANGES (ADR-0133). It samples the two band centres either side of the nearest
233// edge and returns unchanged when they resolve to the same colour within half a
234// code value, which is below the LUT's own 8-bit quantization. On a smooth
235// palette two distinct centres always differ by at least one code value, so
236// every edge draws exactly as it did at any `palette_steps`; inside a plateau
237// the LUT is literally constant and the samples are bit-equal, so the line
238// vanishes there and survives at the run boundaries. One rule, both behaviours,
239// no new parameter.
240//
241// The two LUTs, the sampler and `palette_mix` are EXPLICIT parameters rather
242// than module-scope globals this happens to find: all four sites name them the
243// same today, so implicit capture would compile — and would silently bind the
244// shared function to whatever a future site called its textures.
245//
246// `textureSampleLevel`, not `textureSample`: the LUT has one mip, and an
247// explicit LOD keeps these reads free of the uniformity requirement that a
248// sample after a conditional return would otherwise carry.
249fn band_contour(
250 t: f32,
251 steps: f32,
252 amount: f32,
253 lut_a: texture_2d<f32>,
254 lut_b: texture_2d<f32>,
255 lut_samp: sampler,
256 mix_ab: f32,
257) -> f32 {
258 let f = t * steps;
259 let w = max(fwidth(f), 1e-5);
260 if (steps < 1.5 || amount <= 0.0) {
261 return 1.0;
262 }
263 let n = round(f);
264 let m = clamp(mix_ab, 0.0, 1.0);
265 let lo = mix(
266 textureSampleLevel(lut_a, lut_samp, vec2<f32>((n - 0.5) / steps, 0.5), 0.0).rgb,
267 textureSampleLevel(lut_b, lut_samp, vec2<f32>((n - 0.5) / steps, 0.5), 0.0).rgb,
268 m
269 );
270 let hi = mix(
271 textureSampleLevel(lut_a, lut_samp, vec2<f32>((n + 0.5) / steps, 0.5), 0.0).rgb,
272 textureSampleLevel(lut_b, lut_samp, vec2<f32>((n + 0.5) / steps, 0.5), 0.0).rgb,
273 m
274 );
275 if (all(abs(hi - lo) < vec3<f32>(0.5 / 255.0))) {
276 return 1.0;
277 }
278 let d = min(fract(f), 1.0 - fract(f));
279 return 1.0 - clamp(amount, 0.0, 1.0) * (1.0 - smoothstep(0.0, w, d));
280}
281
282fn tap_v(uv: vec2<f32>) -> f32 {
283 return textureSampleLevel(present_field, present_samp, uv, 0.0).y;
284}
285
286// C1 reconstruction of the finite field (Plan 0033 Phase 3, ADR-0034).
287//
288// Hardware bilinear is C0: the value is continuous, its gradient is not. This
289// pass runs analytic iso-contours and a central-difference gradient over exactly
290// that field, and `line_d` divides by `fwidth`, so a slope discontinuity far
291// below the 8-bit output quantum is amplified into a visible tangent kink —
292// which is why an 8x upscale of a smooth field read as angular facets.
293//
294// Catmull-Rom, not a smoothed texel coordinate. Warping the fractional
295// coordinate by a quintic is the cheap trick and it is *wrong here*: a
296// smoothstep-family warp has zero derivative at both ends, so it pins the
297// reconstruction's gradient to zero at every texel centre. That is C1, but the
298// derivative then oscillates once per cell, and a pass with this much gradient
299// gain renders it as one scalloped step per texel — measurably worse than the
300// faceting it replaces. Only a genuine higher-order filter has a smooth,
301// non-degenerate derivative.
302//
303// Nine taps rather than sixteen: each pair of neighbouring weights is folded
304// into one hardware-bilinear fetch at the weighted midpoint (`offset12`), which
305// is exact for a separable cubic. `w1 + w2 >= 1` over the whole cell, so the
306// division is safe.
307//
308// **Every** read of the field goes through here — value, gradient, contour and
309// hatch — because fixing only the value tap fixes nothing: the gradient is where
310// the discontinuity becomes visible.
311fn sample_v(uv: vec2<f32>) -> f32 {
312 let dims = vec2<f32>(textureDimensions(present_field));
313 let sample_pos = uv * dims;
314 let pos1 = floor(sample_pos - 0.5) + 0.5;
315 let f = sample_pos - pos1;
316
317 let w0 = f * (-0.5 + f * (1.0 - 0.5 * f));
318 let w1 = 1.0 + f * f * (-2.5 + 1.5 * f);
319 let w2 = f * (0.5 + f * (2.0 - 1.5 * f));
320 let w3 = f * f * (-0.5 + 0.5 * f);
321 let w12 = w1 + w2;
322
323 let p0 = (pos1 - 1.0) / dims;
324 let p3 = (pos1 + 2.0) / dims;
325 let p12 = (pos1 + w2 / w12) / dims;
326
327 var acc = 0.0;
328 acc = acc + tap_v(vec2<f32>(p0.x, p0.y)) * w0.x * w0.y;
329 acc = acc + tap_v(vec2<f32>(p12.x, p0.y)) * w12.x * w0.y;
330 acc = acc + tap_v(vec2<f32>(p3.x, p0.y)) * w3.x * w0.y;
331
332 acc = acc + tap_v(vec2<f32>(p0.x, p12.y)) * w0.x * w12.y;
333 acc = acc + tap_v(vec2<f32>(p12.x, p12.y)) * w12.x * w12.y;
334 acc = acc + tap_v(vec2<f32>(p3.x, p12.y)) * w3.x * w12.y;
335
336 acc = acc + tap_v(vec2<f32>(p0.x, p3.y)) * w0.x * w3.y;
337 acc = acc + tap_v(vec2<f32>(p12.x, p3.y)) * w12.x * w3.y;
338 acc = acc + tap_v(vec2<f32>(p3.x, p3.y)) * w3.x * w3.y;
339 return acc;
340}
341
342@fragment
343fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
344 let dims = vec2<f32>(textureDimensions(present_field));
345 let texel = 1.0 / dims;
346
347 // View transform (ADR-0018): scale the sampled window about its centre by
348 // `zoom`, then offset by `pan`. Default zoom = 1, pan = 0 leaves `in.uv`
349 // untouched, so an unbound preset renders identically. Same shape fragment_field
350 // uses for its field-space zoom/pan.
351 let zoom = pp.c.x;
352 // `pan.y` is negated because `in.uv` is now Y-flipped (ADR-0070): this pass
353 // moved from the retired unflipped prelude, which reversed the direction a
354 // positive `pan_y` scrolls the field. Every other scene applies pan in clip
355 // space, where +y is up, and all four RD presets were authored against that
356 // agreement — so the sign restores the shipped behaviour rather than changing
357 // it. Measured both ways: pan_y = +0.12 moves the field 86 px, up before this
358 // and down without this negation.
359 let pan = vec2<f32>(pp.c.y, -pp.c.z);
360 let uv = (in.uv - vec2<f32>(0.5, 0.5)) * zoom + vec2<f32>(0.5, 0.5) + pan;
361
362 let v = sample_v(uv);
363
364 // Central-difference gradient of the field (for hatch orientation + edges).
365 let gx = sample_v(uv + vec2<f32>(texel.x, 0.0)) - sample_v(uv - vec2<f32>(texel.x, 0.0));
366 let gy = sample_v(uv + vec2<f32>(0.0, texel.y)) - sample_v(uv - vec2<f32>(0.0, texel.y));
367 let grad = vec2<f32>(gx, gy);
368 let gmag = length(grad);
369
370 let hue = pp.a.x;
371 let density = pp.a.y;
372 let hatch_freq = pp.a.z;
373 let glow = pp.a.w;
374 let color_span = pp.b.x;
375 let color_center = pp.b.y;
376 let saturation = pp.b.z;
377 let palette_mix = pp.b.w;
378
379 // Slope mask: contours and hatch only appear where the field actually
380 // slopes, so the flat V=0 background stays dark (V=0 is itself an iso-level,
381 // which would otherwise flood the flats).
382 let slope = smoothstep(0.0008, 0.004, gmag);
383
384 // Iso-contour lines: distance (in pixels) to the nearest V = k/density level,
385 // anti-aliased by fwidth. `contour` is ~1 on a line, 0 between them.
386 let f = v * density;
387 let line_d = abs(fract(f - 0.5) - 0.5) / max(fwidth(f), 1e-4);
388 let contour = (1.0 - clamp(line_d, 0.0, 1.0)) * slope;
389
390 // Palette by field level so the nested loops read as coloured bands. The
391 // field level `v` is the gradient coordinate: `color_span` (was a fixed 0.85)
392 // sets the spanned range, `color_center`/`hue` slide the window, and the A/B
393 // LUTs crossfade by `palette_mix` before the shared `saturation`.
394 let coord = v * color_span + color_center + hue;
395 // Hard bands, then the contour from the SAME coordinate (ADR-0078), so the
396 // dark line follows the palette's iso-lines through the field.
397 let banded = band_coord(coord, pp.d.x);
398 let ca = textureSample(lut_a, lut_samp, vec2<f32>(banded, 0.5)).rgb;
399 let cb = textureSample(lut_b, lut_samp, vec2<f32>(banded, 0.5)).rgb;
400 let mixed = mix(ca, cb, clamp(palette_mix, 0.0, 1.0))
401 * band_contour(coord, pp.d.x, pp.d.y, lut_a, lut_b, lut_samp, palette_mix);
402 let col = apply_saturation(mixed, saturation);
403
404 // Hatch/comb: stripes along the contour tangent (perpendicular to grad),
405 // gated to the slopes so flats stay clean.
406 let tang = normalize(vec2<f32>(-grad.y, grad.x) + vec2<f32>(1e-5, 1e-5));
407 let s = dot(uv * dims, tang) / max(hatch_freq, 1.0);
408 let hatch = smoothstep(0.30, 0.5, abs(fract(s) - 0.5));
409 let hatch_amt = hatch * slope;
410
411 // Compose: dark bed, a coloured fill only where the field lives, bright
412 // contour loops, hatch ticks that darken along the slopes, and a soft glow.
413 let structure = smoothstep(0.04, 0.45, v);
414 var out_col = col * structure * 0.5;
415 out_col = out_col + col * contour * 0.9;
416 out_col = out_col * (1.0 - hatch_amt * 0.4);
417 out_col = out_col + col * v * glow * 0.22;
418
419 // Alpha carries scene presence (the V-field `structure` term) so V=0 voids are
420 // transparent and the `bg_*` backdrop shows through (ADR-0026). The present
421 // pipeline blends premultiplied-OVER: `out_col` is emitted as-is (added over the
422 // backdrop, so bright contours keep full brightness), and alpha only gates how
423 // much backdrop reveals. Over the default black backdrop this is byte-identical
424 // to the prior opaque present.
425 //
426 // `occlude` (pp.c.w) scales that coverage: how much of the backdrop the field
427 // holds out where it does have presence (ADR-0085). Reached only when no post
428 // stage is active — the chain's last stage owns the seam otherwise, and the
429 // renderer hands a literal 1.0 here.
430 return vec4<f32>(out_col, structure * pp.c.w);
431}
432"#;
433
434#[repr(C)]
435#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
436struct InitParams {
437 blobs: [[f32; 4]; MAX_BLOBS],
438 count: [u32; 4],
439}
440
441#[repr(C)]
442#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
443struct SimParams {
444 p: [f32; 4],
446 inj: [f32; 4],
448}
449
450#[repr(C)]
451#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
452struct PresentParams {
453 a: [f32; 4],
455 b: [f32; 4],
457 c: [f32; 4],
459 d: [f32; 4],
461}
462
463struct Resources {
465 field: PingPongField,
466 sim_pipeline: wgpu::RenderPipeline,
467 init_pipeline: wgpu::RenderPipeline,
468 present_pipeline: wgpu::RenderPipeline,
469 sim_uniform: wgpu::Buffer,
470 init_uniform: wgpu::Buffer,
471 present_uniform: wgpu::Buffer,
472 sim_bg_a: wgpu::BindGroup,
475 sim_bg_b: wgpu::BindGroup,
476 init_bg: wgpu::BindGroup,
477 present_bg_a: wgpu::BindGroup,
478 present_bg_b: wgpu::BindGroup,
479 luts: palette::LutPair,
483}
484
485impl Resources {
486 fn build(device: &wgpu::Device, surface_format: wgpu::TextureFormat) -> Self {
488 let init_shader = gpu::fullscreen_shader(
489 device,
490 "rd-init-shader",
491 gpu::FULLSCREEN_VS_UV_FLIPPED,
492 INIT_SHADER,
493 );
494 let sim_shader = gpu::fullscreen_shader(
495 device,
496 "rd-sim-shader",
497 gpu::FULLSCREEN_VS_UV_FLIPPED,
498 SIM_SHADER,
499 );
500 let present_shader = gpu::fullscreen_shader(
501 device,
502 "rd-present-shader",
503 gpu::FULLSCREEN_VS_UV_FLIPPED,
504 PRESENT_SHADER,
505 );
506
507 let field = PingPongField::new(device, GRID, GRID);
508
509 let sim_uniform =
510 gpu::uniform_buffer(device, "rd-sim-params", std::mem::size_of::<SimParams>());
511 let init_uniform =
512 gpu::uniform_buffer(device, "rd-init-params", std::mem::size_of::<InitParams>());
513 let present_uniform = gpu::uniform_buffer(
514 device,
515 "rd-present-params",
516 std::mem::size_of::<PresentParams>(),
517 );
518 let init_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
520 label: Some("rd-init-layout"),
521 entries: &[gpu::uniform(0, wgpu::ShaderStages::FRAGMENT)],
522 });
523 let init_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
524 label: Some("rd-init-bg"),
525 layout: &init_layout,
526 entries: &[wgpu::BindGroupEntry {
527 binding: 0,
528 resource: init_uniform.as_entire_binding(),
529 }],
530 });
531 let init_pipeline = gpu::fullscreen_pipeline(
532 device,
533 &init_shader,
534 &[&init_layout],
535 PingPongField::FORMAT,
536 wgpu::BlendState::REPLACE,
537 "rd-init",
538 );
539
540 let sim_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
542 label: Some("rd-sim-layout"),
543 entries: &[
544 gpu::uniform(0, wgpu::ShaderStages::FRAGMENT),
545 gpu::texture(1, false),
546 ],
547 });
548 let sim_bg_a = sim_bind_group(device, &sim_layout, &sim_uniform, field.view_a());
549 let sim_bg_b = sim_bind_group(device, &sim_layout, &sim_uniform, field.view_b());
550 let sim_pipeline = gpu::fullscreen_pipeline(
551 device,
552 &sim_shader,
553 &[&sim_layout],
554 PingPongField::FORMAT,
555 wgpu::BlendState::REPLACE,
556 "rd-sim",
557 );
558
559 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
575 label: Some("rd-present-sampler"),
576 address_mode_u: wgpu::AddressMode::Repeat,
577 address_mode_v: wgpu::AddressMode::Repeat,
578 address_mode_w: wgpu::AddressMode::Repeat,
579 mag_filter: wgpu::FilterMode::Linear,
580 min_filter: wgpu::FilterMode::Linear,
581 ..Default::default()
582 });
583 let luts = palette::LutPair::new(device, "rd");
586 let present_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
587 label: Some("rd-present-layout"),
588 entries: &[
589 gpu::texture(0, true),
590 gpu::sampler(1),
591 gpu::uniform(2, wgpu::ShaderStages::FRAGMENT),
592 gpu::texture(3, true),
593 gpu::texture(4, true),
594 gpu::sampler(5),
595 ],
596 });
597 let present_bg_a = present_bind_group(
598 device,
599 &present_layout,
600 field.view_a(),
601 &sampler,
602 &present_uniform,
603 &luts,
604 );
605 let present_bg_b = present_bind_group(
606 device,
607 &present_layout,
608 field.view_b(),
609 &sampler,
610 &present_uniform,
611 &luts,
612 );
613 let present_pipeline = gpu::fullscreen_pipeline(
614 device,
615 &present_shader,
616 &[&present_layout],
617 surface_format,
618 wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING,
623 "rd-present",
624 );
625
626 Self {
627 field,
628 sim_pipeline,
629 init_pipeline,
630 present_pipeline,
631 sim_uniform,
632 init_uniform,
633 present_uniform,
634 sim_bg_a,
635 sim_bg_b,
636 init_bg,
637 present_bg_a,
638 present_bg_b,
639 luts,
640 }
641 }
642
643 fn encode_seed(&self, encoder: &mut wgpu::CommandEncoder) {
646 let mut pass = gpu::color_pass(
647 encoder,
648 "rd-seed-pass",
649 self.field.read_view(),
650 wgpu::LoadOp::Clear(wgpu::Color::BLACK),
651 );
652 pass.set_pipeline(&self.init_pipeline);
653 pass.set_bind_group(0, &self.init_bg, &[]);
654 pass.draw(0..3, 0..1);
655 }
656}
657
658pub struct ReactionDiffusionScene {
662 device: wgpu::Device,
666 surface_format: wgpu::TextureFormat,
667 res: Option<Resources>,
668 init_params: InitParams,
671 needs_seed: bool,
672 fixed_step: gpu::FixedStep,
675 pending_substeps: u32,
677 stamp_rng: SeededRng,
680 pending_stamp: Option<[f32; 4]>,
683 prev_inject: f32,
685 time: f32,
687 feed: f32,
688 kill: f32,
689 flow: f32,
691 inject: f32,
693 colour: common::PaletteParams,
695 pan: common::PanParams,
697 contour: f32,
698 hatch: f32,
699 glow: f32,
700 color_span: f32,
702 color_center: f32,
703 zoom: f32,
706 occlude: f32,
711 palette: Palette,
716}
717
718impl ReactionDiffusionScene {
719 pub fn new(device: &wgpu::Device, surface_format: wgpu::TextureFormat) -> Self {
722 let mut init_params = InitParams {
723 blobs: [[0.0; 4]; MAX_BLOBS],
724 count: [0; 4],
725 };
726 let mut rng = SeededRng::new(SEED);
727 let n = SEED_BLOBS.min(MAX_BLOBS);
728 for slot in init_params.blobs.iter_mut().take(n) {
729 let x = rng.next_f32();
730 let y = rng.next_f32();
731 let r = rng.range(0.02, 0.045);
732 *slot = [x, y, r, 0.0];
733 }
734 init_params.count = [n as u32, 0, 0, 0];
735
736 Self {
737 device: device.clone(),
738 surface_format,
739 res: None,
740 init_params,
741 needs_seed: true,
742 fixed_step: gpu::FixedStep::new(FIXED_STEP, MAX_SUBSTEPS),
743 pending_substeps: 0,
744 stamp_rng: SeededRng::new(INJECT_SEED),
745 pending_stamp: None,
746 prev_inject: 0.0,
747 time: 0.0,
748 feed: DEFAULT_FEED,
749 kill: DEFAULT_KILL,
750 flow: DEFAULT_FLOW,
751 inject: 0.0,
752 colour: common::PaletteParams::new(DEFAULT_HUE, common::DEFAULT_BRIGHTNESS),
753 pan: common::PanParams::default(),
754 contour: DEFAULT_CONTOUR,
755 hatch: DEFAULT_HATCH,
756 glow: DEFAULT_GLOW,
757 color_span: DEFAULT_COLOR_SPAN,
758 color_center: DEFAULT_COLOR_CENTER,
759 zoom: DEFAULT_ZOOM,
760 occlude: crate::render::post::DEFAULT_OCCLUDE,
761 palette: Palette::default_spectrum(),
762 }
763 }
764}
765
766fn sim_bind_group(
767 device: &wgpu::Device,
768 layout: &wgpu::BindGroupLayout,
769 uniform: &wgpu::Buffer,
770 input: &wgpu::TextureView,
771) -> wgpu::BindGroup {
772 device.create_bind_group(&wgpu::BindGroupDescriptor {
773 label: Some("rd-sim-bg"),
774 layout,
775 entries: &[
776 wgpu::BindGroupEntry {
777 binding: 0,
778 resource: uniform.as_entire_binding(),
779 },
780 wgpu::BindGroupEntry {
781 binding: 1,
782 resource: wgpu::BindingResource::TextureView(input),
783 },
784 ],
785 })
786}
787
788fn present_bind_group(
789 device: &wgpu::Device,
790 layout: &wgpu::BindGroupLayout,
791 input: &wgpu::TextureView,
792 sampler: &wgpu::Sampler,
793 uniform: &wgpu::Buffer,
794 luts: &palette::LutPair,
795) -> wgpu::BindGroup {
796 let [lut_a, lut_b, lut_sampler] = luts.bind_entries(3, 4, 5);
797 device.create_bind_group(&wgpu::BindGroupDescriptor {
798 label: Some("rd-present-bg"),
799 layout,
800 entries: &[
801 wgpu::BindGroupEntry {
802 binding: 0,
803 resource: wgpu::BindingResource::TextureView(input),
804 },
805 wgpu::BindGroupEntry {
806 binding: 1,
807 resource: wgpu::BindingResource::Sampler(sampler),
808 },
809 wgpu::BindGroupEntry {
810 binding: 2,
811 resource: uniform.as_entire_binding(),
812 },
813 lut_a,
814 lut_b,
815 lut_sampler,
816 ],
817 })
818}
819
820pub const PARAMS: &[ParamSpec] = &[
823 ParamSpec {
824 name: "feed",
825 default: 0.0367,
826 range: Some([0.01, 0.09]),
827 doc: "Feed rate of the reaction - with `kill`, it is what decides whether you get spots, stripes or mitosis.",
828 kind: ParamKind::Modal,
829 },
830 ParamSpec {
831 name: "kill",
832 default: 0.0649,
833 range: Some([0.03, 0.07]),
834 doc: "Kill rate of the reaction; small moves here change the pattern's whole character.",
835 kind: ParamKind::Modal,
836 },
837 ParamSpec {
838 name: "flow",
839 default: 1.0,
840 range: Some([0.0, 4.0]),
841 doc: "How fast the simulation advances per second.",
842 kind: ParamKind::Modal,
843 },
844 ParamSpec {
845 name: "inject",
846 default: 0.0,
847 range: Some([0.0, 1.0]),
848 doc: "Drops fresh reagent into the field, which is how a beat seeds new growth.",
849 kind: ParamKind::Modal,
850 },
851 crate::render::scenes::common::hue(DEFAULT_HUE),
852 ParamSpec {
853 name: "contour",
854 default: 6.0,
855 range: Some([0.0, 24.0]),
856 doc: "How many bands the concentration is drawn as, as a real density: a fraction \
857 slides the whole set of iso-lines. 0 is a smooth gradient.",
858 kind: ParamKind::Modal,
859 },
860 ParamSpec {
861 name: "hatch",
862 default: 5.0,
863 range: Some([0.0, 24.0]),
864 doc: "Density of the hatching drawn along the concentration gradient.",
865 kind: ParamKind::Modal,
866 },
867 ParamSpec {
868 name: "glow",
869 default: 1.0,
870 range: Some([0.0, 2.0]),
871 doc: "Overall light the field emits.",
872 kind: ParamKind::Modal,
873 },
874 ParamSpec {
875 name: "color_span",
876 default: 0.85,
877 range: Some([0.0, 1.0]),
878 doc: "How much of the palette the concentration range covers.",
879 kind: ParamKind::Modal,
880 },
881 ParamSpec {
882 name: "color_center",
883 default: 0.0,
884 range: Some([-1.0, 1.0]),
885 doc: "Shifts which concentration lands in the middle of the palette.",
886 kind: ParamKind::Modal,
887 },
888 crate::render::scenes::common::SATURATION,
889 crate::render::scenes::common::PALETTE_MIX,
890 crate::render::scenes::common::PALETTE_STEPS,
891 crate::render::scenes::common::PALETTE_CONTOUR,
892 crate::render::scenes::common::zoom(DEFAULT_ZOOM),
893 crate::render::scenes::common::PAN_X,
894 crate::render::scenes::common::PAN_Y,
895];
896
897impl Scene for ReactionDiffusionScene {
898 fn name(&self) -> &'static str {
899 "reaction diffusion"
900 }
901
902 fn advance(&mut self, dt: f32) {
903 self.pending_substeps = self.fixed_step.advance(dt);
909 }
910
911 fn set_time(&mut self, time: f32) {
912 self.time = time;
913 }
914
915 fn set_occlude(&mut self, occlude: f32) {
916 self.occlude = occlude;
917 }
918
919 fn set_palette(&mut self, palette: &Palette) {
920 self.palette = palette.clone();
923 if let Some(res) = self.res.as_mut() {
924 res.luts.set(palette);
925 }
926 }
927
928 fn reset_params(&mut self) {
929 self.feed = DEFAULT_FEED;
930 self.kill = DEFAULT_KILL;
931 self.flow = DEFAULT_FLOW;
932 self.inject = 0.0;
933 self.colour.reset();
934 self.pan.reset();
935 self.contour = DEFAULT_CONTOUR;
936 self.hatch = DEFAULT_HATCH;
937 self.glow = DEFAULT_GLOW;
938 self.color_span = DEFAULT_COLOR_SPAN;
939 self.color_center = DEFAULT_COLOR_CENTER;
940 self.zoom = DEFAULT_ZOOM;
941 }
942
943 fn set_param(&mut self, name: &str, value: f32) {
944 if self.colour.set(name, value) || self.pan.set(name, value) {
947 return;
948 }
949 match name {
954 "feed" => self.feed = value,
955 "kill" => self.kill = value,
956 "flow" => self.flow = value,
957 "inject" => self.inject = value,
958 "contour" => self.contour = value,
959 "hatch" => self.hatch = value,
960 "glow" => self.glow = value,
961 "color_span" => self.color_span = value,
962 "color_center" => self.color_center = value,
963 "zoom" => self.zoom = value,
964 _ => {}
965 }
966 }
967
968 fn update(&mut self, _frame: &AnalysisFrame) {
969 if self.inject >= INJECT_THRESHOLD && self.prev_inject < INJECT_THRESHOLD {
974 let cx = self.stamp_rng.next_f32();
975 let cy = self.stamp_rng.next_f32();
976 self.pending_stamp = Some([cx, cy, INJECT_RADIUS, INJECT_AMOUNT]);
977 }
978 self.prev_inject = self.inject;
979 }
980
981 fn render(
982 &mut self,
983 queue: &wgpu::Queue,
984 encoder: &mut wgpu::CommandEncoder,
985 view: &wgpu::TextureView,
986 _aspect: f32,
987 ) {
988 if self.res.is_none() {
992 let mut built = Resources::build(&self.device, self.surface_format);
993 built.luts.set(&self.palette);
994 self.res = Some(built);
995 }
996 let Self {
997 res,
998 init_params,
999 needs_seed,
1000 pending_substeps,
1001 pending_stamp,
1002 feed,
1003 kill,
1004 flow,
1005 contour,
1006 hatch,
1007 glow,
1008 color_span,
1009 color_center,
1010 colour,
1011 zoom,
1012 pan,
1013 occlude,
1014 ..
1015 } = self;
1016 let Some(res) = res.as_mut() else {
1017 return;
1018 };
1019
1020 res.luts.flush(queue);
1023
1024 queue.write_buffer(
1025 &res.present_uniform,
1026 0,
1027 bytemuck::bytes_of(&PresentParams {
1028 a: [colour.hue, *contour, *hatch, *glow],
1029 b: [*color_span, *color_center, colour.saturation, colour.mix],
1030 c: [*zoom, pan.x, pan.y, *occlude],
1031 d: [
1032 palette::band_steps(colour.steps),
1033 palette::band_contour(colour.contour),
1034 0.0,
1035 0.0,
1036 ],
1037 }),
1038 );
1039
1040 let inj = pending_stamp.take().unwrap_or([0.0; 4]);
1044 queue.write_buffer(
1045 &res.sim_uniform,
1046 0,
1047 bytemuck::bytes_of(&SimParams {
1048 p: [*feed, *kill, DIFFUSE_U * *flow, DIFFUSE_V * *flow],
1051 inj,
1052 }),
1053 );
1054
1055 if *needs_seed {
1057 queue.write_buffer(&res.init_uniform, 0, bytemuck::bytes_of(init_params));
1058 res.encode_seed(encoder);
1059 *needs_seed = false;
1060 }
1061
1062 for _ in 0..*pending_substeps {
1065 let sim_bg = if res.field.reading_a() {
1066 &res.sim_bg_a
1067 } else {
1068 &res.sim_bg_b
1069 };
1070 {
1071 let mut pass = gpu::color_pass(
1073 encoder,
1074 "rd-sim-pass",
1075 res.field.write_view(),
1076 wgpu::LoadOp::Clear(wgpu::Color::BLACK),
1077 );
1078 pass.set_pipeline(&res.sim_pipeline);
1079 pass.set_bind_group(0, sim_bg, &[]);
1080 pass.draw(0..3, 0..1);
1081 }
1082 res.field.swap();
1083 }
1084
1085 let present_bg = if res.field.reading_a() {
1087 &res.present_bg_a
1088 } else {
1089 &res.present_bg_b
1090 };
1091 let mut pass = gpu::color_pass(encoder, "rd-present-pass", view, wgpu::LoadOp::Load);
1098 pass.set_pipeline(&res.present_pipeline);
1099 pass.set_bind_group(0, present_bg, &[]);
1100 pass.draw(0..3, 0..1);
1101 }
1102}