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::{ParamGroup, 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), z: palette_contour_style (integral, rounded CPU-side),
198 // w: palette_contour_ink (ADR-0197)
199 d: vec4<f32>,
200}
201@group(0) @binding(0) var present_field: texture_2d<f32>;
202@group(0) @binding(1) var present_samp: sampler;
203@group(0) @binding(2) var<uniform> pp: Present;
204// Shared gradient LUTs (ADR-0021): A/B for the `palette_mix` crossfade, one
205// repeat sampler. Kept in this present bind group (a unique 6-entry layout) so it
206// never matches another pipeline's layout on the DX12 WARP software adapter.
207@group(0) @binding(3) var lut_a: texture_2d<f32>;
208@group(0) @binding(4) var lut_b: texture_2d<f32>;
209@group(0) @binding(5) var lut_samp: sampler;
210
211// Shared `saturation` (mirrors core/src/render/palette.rs::desaturate verbatim).
212fn apply_saturation(c: vec3<f32>, s: f32) -> vec3<f32> {
213 let luma = dot(c, vec3<f32>(0.299, 0.587, 0.114));
214 return vec3<f32>(luma) + (c - vec3<f32>(luma)) * s;
215}
216
217// Shared `palette_steps` (mirrors core/src/render/palette.rs::band_coord
218// verbatim, ADR-0078): snap the palette coordinate to a band centre before the
219// LUT read. Below 1.5 steps it is the exact identity, not a one-band degenerate.
220fn band_coord(t: f32, steps: f32) -> f32 {
221 if (steps < 1.5) {
222 return t;
223 }
224 return (floor(t * steps) + 0.5) / steps;
225}
226
227// Shared `palette_contour` (ADR-0078 / ADR-0133; the WGSL is the implementation,
228// copied verbatim at each fragment-stage site — palette.rs has no CPU
229// counterpart to be canonical, since `fwidth` exists only here).
230//
231// Darkens within one PIXEL of a band edge, so the line has the same weight where
232// the field is shallow and where it is steep — AND ONLY WHERE THE INK ACTUALLY
233// CHANGES (ADR-0133). It samples the two band centres either side of the nearest
234// edge and returns unchanged when they resolve to the same colour within half a
235// code value, which is below the LUT's own 8-bit quantization. On a smooth
236// palette two distinct centres always differ by at least one code value, so
237// every edge draws exactly as it did at any `palette_steps`; inside a plateau
238// the LUT is literally constant and the samples are bit-equal, so the line
239// vanishes there and survives at the run boundaries. One rule, both behaviours,
240// no new parameter.
241//
242// The two LUTs, the sampler and `palette_mix` are EXPLICIT parameters rather
243// than module-scope globals this happens to find: all six sites name them the
244// same today, so implicit capture would compile — and would silently bind the
245// shared function to whatever a future site called its textures.
246//
247// `textureSampleLevel`, not `textureSample`: the LUT has one mip, and an
248// explicit LOD keeps these reads free of the uniformity requirement that a
249// sample after a conditional return would otherwise carry.
250//
251// **What the line is drawn in is `style`** (ADR-0197): `0` the soft darkening
252// above, `1` a hard darkening over the same footprint, `2` a soft line in the
253// palette's own colour at `ink_t` and `3` a hard one. `style` arrives rounded to
254// a whole number from the CPU (`palette::band_contour_style`), so the equality
255// comparisons below are exact. The `style < 0.5` arm is the expression that
256// shipped before the other three existed, which is what keeps every golden still.
257fn band_contour_ink(
258 col: vec3<f32>,
259 t: f32,
260 steps: f32,
261 amount: f32,
262 style: f32,
263 ink_t: f32,
264 lut_a: texture_2d<f32>,
265 lut_b: texture_2d<f32>,
266 lut_samp: sampler,
267 mix_ab: f32,
268) -> vec3<f32> {
269 let f = t * steps;
270 let w = max(fwidth(f), 1e-5);
271 if (steps < 1.5 || amount <= 0.0) {
272 return col;
273 }
274 let n = round(f);
275 let m = clamp(mix_ab, 0.0, 1.0);
276 let lo = mix(
277 textureSampleLevel(lut_a, lut_samp, vec2<f32>((n - 0.5) / steps, 0.5), 0.0).rgb,
278 textureSampleLevel(lut_b, lut_samp, vec2<f32>((n - 0.5) / steps, 0.5), 0.0).rgb,
279 m
280 );
281 let hi = mix(
282 textureSampleLevel(lut_a, lut_samp, vec2<f32>((n + 0.5) / steps, 0.5), 0.0).rgb,
283 textureSampleLevel(lut_b, lut_samp, vec2<f32>((n + 0.5) / steps, 0.5), 0.0).rgb,
284 m
285 );
286 if (all(abs(hi - lo) < vec3<f32>(0.5 / 255.0))) {
287 return col;
288 }
289 let d = min(fract(f), 1.0 - fract(f));
290 if (style < 0.5) {
291 return col * (1.0 - clamp(amount, 0.0, 1.0) * (1.0 - smoothstep(0.0, w, d)));
292 }
293 let hard = style == 1.0 || style == 3.0;
294 let cover = select(1.0 - smoothstep(0.0, w, d), f32(d < w), hard);
295 let ink_lut = mix(
296 textureSampleLevel(lut_a, lut_samp, vec2<f32>(ink_t, 0.5), 0.0).rgb,
297 textureSampleLevel(lut_b, lut_samp, vec2<f32>(ink_t, 0.5), 0.0).rgb,
298 m
299 );
300 let ink = select(vec3<f32>(0.0), ink_lut, style >= 2.0);
301 return mix(col, ink, clamp(amount, 0.0, 1.0) * cover);
302}
303
304fn tap_v(uv: vec2<f32>) -> f32 {
305 return textureSampleLevel(present_field, present_samp, uv, 0.0).y;
306}
307
308// C1 reconstruction of the finite field (Plan 0033 Phase 3, ADR-0034).
309//
310// Hardware bilinear is C0: the value is continuous, its gradient is not. This
311// pass runs analytic iso-contours and a central-difference gradient over exactly
312// that field, and `line_d` divides by `fwidth`, so a slope discontinuity far
313// below the 8-bit output quantum is amplified into a visible tangent kink —
314// which is why an 8x upscale of a smooth field read as angular facets.
315//
316// Catmull-Rom, not a smoothed texel coordinate. Warping the fractional
317// coordinate by a quintic is the cheap trick and it is *wrong here*: a
318// smoothstep-family warp has zero derivative at both ends, so it pins the
319// reconstruction's gradient to zero at every texel centre. That is C1, but the
320// derivative then oscillates once per cell, and a pass with this much gradient
321// gain renders it as one scalloped step per texel — measurably worse than the
322// faceting it replaces. Only a genuine higher-order filter has a smooth,
323// non-degenerate derivative.
324//
325// Nine taps rather than sixteen: each pair of neighbouring weights is folded
326// into one hardware-bilinear fetch at the weighted midpoint (`offset12`), which
327// is exact for a separable cubic. `w1 + w2 >= 1` over the whole cell, so the
328// division is safe.
329//
330// **Every** read of the field goes through here — value, gradient, contour and
331// hatch — because fixing only the value tap fixes nothing: the gradient is where
332// the discontinuity becomes visible.
333fn sample_v(uv: vec2<f32>) -> f32 {
334 let dims = vec2<f32>(textureDimensions(present_field));
335 let sample_pos = uv * dims;
336 let pos1 = floor(sample_pos - 0.5) + 0.5;
337 let f = sample_pos - pos1;
338
339 let w0 = f * (-0.5 + f * (1.0 - 0.5 * f));
340 let w1 = 1.0 + f * f * (-2.5 + 1.5 * f);
341 let w2 = f * (0.5 + f * (2.0 - 1.5 * f));
342 let w3 = f * f * (-0.5 + 0.5 * f);
343 let w12 = w1 + w2;
344
345 let p0 = (pos1 - 1.0) / dims;
346 let p3 = (pos1 + 2.0) / dims;
347 let p12 = (pos1 + w2 / w12) / dims;
348
349 var acc = 0.0;
350 acc = acc + tap_v(vec2<f32>(p0.x, p0.y)) * w0.x * w0.y;
351 acc = acc + tap_v(vec2<f32>(p12.x, p0.y)) * w12.x * w0.y;
352 acc = acc + tap_v(vec2<f32>(p3.x, p0.y)) * w3.x * w0.y;
353
354 acc = acc + tap_v(vec2<f32>(p0.x, p12.y)) * w0.x * w12.y;
355 acc = acc + tap_v(vec2<f32>(p12.x, p12.y)) * w12.x * w12.y;
356 acc = acc + tap_v(vec2<f32>(p3.x, p12.y)) * w3.x * w12.y;
357
358 acc = acc + tap_v(vec2<f32>(p0.x, p3.y)) * w0.x * w3.y;
359 acc = acc + tap_v(vec2<f32>(p12.x, p3.y)) * w12.x * w3.y;
360 acc = acc + tap_v(vec2<f32>(p3.x, p3.y)) * w3.x * w3.y;
361 return acc;
362}
363
364@fragment
365fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
366 let dims = vec2<f32>(textureDimensions(present_field));
367 let texel = 1.0 / dims;
368
369 // View transform (ADR-0018): scale the sampled window about its centre by
370 // `zoom`, then offset by `pan`. Default zoom = 1, pan = 0 leaves `in.uv`
371 // untouched, so an unbound preset renders identically. Same shape fragment_field
372 // uses for its field-space zoom/pan.
373 let zoom = pp.c.x;
374 // `pan.y` is negated because `in.uv` is now Y-flipped (ADR-0070): this pass
375 // moved from the retired unflipped prelude, which reversed the direction a
376 // positive `pan_y` scrolls the field. Every other scene applies pan in clip
377 // space, where +y is up, and all four RD presets were authored against that
378 // agreement — so the sign restores the shipped behaviour rather than changing
379 // it. Measured both ways: pan_y = +0.12 moves the field 86 px, up before this
380 // and down without this negation.
381 let pan = vec2<f32>(pp.c.y, -pp.c.z);
382 let uv = (in.uv - vec2<f32>(0.5, 0.5)) * zoom + vec2<f32>(0.5, 0.5) + pan;
383
384 let v = sample_v(uv);
385
386 // Central-difference gradient of the field (for hatch orientation + edges).
387 let gx = sample_v(uv + vec2<f32>(texel.x, 0.0)) - sample_v(uv - vec2<f32>(texel.x, 0.0));
388 let gy = sample_v(uv + vec2<f32>(0.0, texel.y)) - sample_v(uv - vec2<f32>(0.0, texel.y));
389 let grad = vec2<f32>(gx, gy);
390 let gmag = length(grad);
391
392 let hue = pp.a.x;
393 let density = pp.a.y;
394 let hatch_freq = pp.a.z;
395 let glow = pp.a.w;
396 let color_span = pp.b.x;
397 let color_center = pp.b.y;
398 let saturation = pp.b.z;
399 let palette_mix = pp.b.w;
400
401 // Slope mask: contours and hatch only appear where the field actually
402 // slopes, so the flat V=0 background stays dark (V=0 is itself an iso-level,
403 // which would otherwise flood the flats).
404 let slope = smoothstep(0.0008, 0.004, gmag);
405
406 // Iso-contour lines: distance (in pixels) to the nearest V = k/density level,
407 // anti-aliased by fwidth. `contour` is ~1 on a line, 0 between them.
408 let f = v * density;
409 let line_d = abs(fract(f - 0.5) - 0.5) / max(fwidth(f), 1e-4);
410 let contour = (1.0 - clamp(line_d, 0.0, 1.0)) * slope;
411
412 // Palette by field level so the nested loops read as coloured bands. The
413 // field level `v` is the gradient coordinate: `color_span` (was a fixed 0.85)
414 // sets the spanned range, `color_center`/`hue` slide the window, and the A/B
415 // LUTs crossfade by `palette_mix` before the shared `saturation`.
416 let coord = v * color_span + color_center + hue;
417 // Hard bands, then the contour from the SAME coordinate (ADR-0078), so the
418 // dark line follows the palette's iso-lines through the field.
419 let banded = band_coord(coord, pp.d.x);
420 let ca = textureSample(lut_a, lut_samp, vec2<f32>(banded, 0.5)).rgb;
421 let cb = textureSample(lut_b, lut_samp, vec2<f32>(banded, 0.5)).rgb;
422 let mixed = band_contour_ink(
423 mix(ca, cb, clamp(palette_mix, 0.0, 1.0)),
424 coord, pp.d.x, pp.d.y, pp.d.z, pp.d.w, lut_a, lut_b, lut_samp, palette_mix
425 );
426 let col = apply_saturation(mixed, saturation);
427
428 // Hatch/comb: stripes along the contour tangent (perpendicular to grad),
429 // gated to the slopes so flats stay clean.
430 let tang = normalize(vec2<f32>(-grad.y, grad.x) + vec2<f32>(1e-5, 1e-5));
431 let s = dot(uv * dims, tang) / max(hatch_freq, 1.0);
432 let hatch = smoothstep(0.30, 0.5, abs(fract(s) - 0.5));
433 let hatch_amt = hatch * slope;
434
435 // Compose: dark bed, a coloured fill only where the field lives, bright
436 // contour loops, hatch ticks that darken along the slopes, and a soft glow.
437 let structure = smoothstep(0.04, 0.45, v);
438 var out_col = col * structure * 0.5;
439 out_col = out_col + col * contour * 0.9;
440 out_col = out_col * (1.0 - hatch_amt * 0.4);
441 out_col = out_col + col * v * glow * 0.22;
442
443 // Alpha carries scene presence (the V-field `structure` term) so V=0 voids are
444 // transparent and the `bg_*` backdrop shows through (ADR-0026). The present
445 // pipeline blends premultiplied-OVER: `out_col` is emitted as-is (added over the
446 // backdrop, so bright contours keep full brightness), and alpha only gates how
447 // much backdrop reveals. Over the default black backdrop this is byte-identical
448 // to the prior opaque present.
449 //
450 // `occlude` (pp.c.w) scales that coverage: how much of the backdrop the field
451 // holds out where it does have presence (ADR-0085). Reached only when no post
452 // stage is active — the chain's last stage owns the seam otherwise, and the
453 // renderer hands a literal 1.0 here.
454 return vec4<f32>(out_col, structure * pp.c.w);
455}
456"#;
457
458#[repr(C)]
459#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
460struct InitParams {
461 blobs: [[f32; 4]; MAX_BLOBS],
462 count: [u32; 4],
463}
464
465#[repr(C)]
466#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
467struct SimParams {
468 p: [f32; 4],
470 inj: [f32; 4],
472}
473
474#[repr(C)]
475#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
476struct PresentParams {
477 a: [f32; 4],
479 b: [f32; 4],
481 c: [f32; 4],
483 d: [f32; 4],
486}
487
488struct Resources {
490 field: PingPongField,
491 sim_pipeline: wgpu::RenderPipeline,
492 init_pipeline: wgpu::RenderPipeline,
493 present_pipeline: wgpu::RenderPipeline,
494 sim_uniform: wgpu::Buffer,
495 init_uniform: wgpu::Buffer,
496 present_uniform: wgpu::Buffer,
497 sim_bg_a: wgpu::BindGroup,
500 sim_bg_b: wgpu::BindGroup,
501 init_bg: wgpu::BindGroup,
502 present_bg_a: wgpu::BindGroup,
503 present_bg_b: wgpu::BindGroup,
504 luts: palette::LutPair,
508}
509
510impl Resources {
511 fn build(device: &wgpu::Device, surface_format: wgpu::TextureFormat) -> Self {
513 let init_shader = gpu::fullscreen_shader(
514 device,
515 "rd-init-shader",
516 gpu::FULLSCREEN_VS_UV_FLIPPED,
517 INIT_SHADER,
518 );
519 let sim_shader = gpu::fullscreen_shader(
520 device,
521 "rd-sim-shader",
522 gpu::FULLSCREEN_VS_UV_FLIPPED,
523 SIM_SHADER,
524 );
525 let present_shader = gpu::fullscreen_shader(
526 device,
527 "rd-present-shader",
528 gpu::FULLSCREEN_VS_UV_FLIPPED,
529 PRESENT_SHADER,
530 );
531
532 let field = PingPongField::new(device, GRID, GRID);
533
534 let sim_uniform =
535 gpu::uniform_buffer(device, "rd-sim-params", std::mem::size_of::<SimParams>());
536 let init_uniform =
537 gpu::uniform_buffer(device, "rd-init-params", std::mem::size_of::<InitParams>());
538 let present_uniform = gpu::uniform_buffer(
539 device,
540 "rd-present-params",
541 std::mem::size_of::<PresentParams>(),
542 );
543 let init_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
545 label: Some("rd-init-layout"),
546 entries: &[gpu::uniform(0, wgpu::ShaderStages::FRAGMENT)],
547 });
548 let init_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
549 label: Some("rd-init-bg"),
550 layout: &init_layout,
551 entries: &[wgpu::BindGroupEntry {
552 binding: 0,
553 resource: init_uniform.as_entire_binding(),
554 }],
555 });
556 let init_pipeline = gpu::fullscreen_pipeline(
557 device,
558 &init_shader,
559 &[&init_layout],
560 PingPongField::FORMAT,
561 wgpu::BlendState::REPLACE,
562 "rd-init",
563 );
564
565 let sim_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
567 label: Some("rd-sim-layout"),
568 entries: &[
569 gpu::uniform(0, wgpu::ShaderStages::FRAGMENT),
570 gpu::texture(1, false),
571 ],
572 });
573 let sim_bg_a = sim_bind_group(device, &sim_layout, &sim_uniform, field.view_a());
574 let sim_bg_b = sim_bind_group(device, &sim_layout, &sim_uniform, field.view_b());
575 let sim_pipeline = gpu::fullscreen_pipeline(
576 device,
577 &sim_shader,
578 &[&sim_layout],
579 PingPongField::FORMAT,
580 wgpu::BlendState::REPLACE,
581 "rd-sim",
582 );
583
584 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
600 label: Some("rd-present-sampler"),
601 address_mode_u: wgpu::AddressMode::Repeat,
602 address_mode_v: wgpu::AddressMode::Repeat,
603 address_mode_w: wgpu::AddressMode::Repeat,
604 mag_filter: wgpu::FilterMode::Linear,
605 min_filter: wgpu::FilterMode::Linear,
606 ..Default::default()
607 });
608 let luts = palette::LutPair::new(device, "rd");
611 let present_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
612 label: Some("rd-present-layout"),
613 entries: &[
614 gpu::texture(0, true),
615 gpu::sampler(1),
616 gpu::uniform(2, wgpu::ShaderStages::FRAGMENT),
617 gpu::texture(3, true),
618 gpu::texture(4, true),
619 gpu::sampler(5),
620 ],
621 });
622 let present_bg_a = present_bind_group(
623 device,
624 &present_layout,
625 field.view_a(),
626 &sampler,
627 &present_uniform,
628 &luts,
629 );
630 let present_bg_b = present_bind_group(
631 device,
632 &present_layout,
633 field.view_b(),
634 &sampler,
635 &present_uniform,
636 &luts,
637 );
638 let present_pipeline = gpu::fullscreen_pipeline(
639 device,
640 &present_shader,
641 &[&present_layout],
642 surface_format,
643 wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING,
648 "rd-present",
649 );
650
651 Self {
652 field,
653 sim_pipeline,
654 init_pipeline,
655 present_pipeline,
656 sim_uniform,
657 init_uniform,
658 present_uniform,
659 sim_bg_a,
660 sim_bg_b,
661 init_bg,
662 present_bg_a,
663 present_bg_b,
664 luts,
665 }
666 }
667
668 fn encode_seed(&self, encoder: &mut wgpu::CommandEncoder) {
671 let mut pass = gpu::color_pass(
672 encoder,
673 "rd-seed-pass",
674 self.field.read_view(),
675 wgpu::LoadOp::Clear(wgpu::Color::BLACK),
676 );
677 pass.set_pipeline(&self.init_pipeline);
678 pass.set_bind_group(0, &self.init_bg, &[]);
679 pass.draw(0..3, 0..1);
680 }
681}
682
683pub struct ReactionDiffusionScene {
687 device: wgpu::Device,
691 surface_format: wgpu::TextureFormat,
692 res: Option<Resources>,
693 init_params: InitParams,
696 needs_seed: bool,
697 fixed_step: gpu::FixedStep,
700 pending_substeps: u32,
702 stamp_rng: SeededRng,
705 pending_stamp: Option<[f32; 4]>,
708 prev_inject: f32,
710 time: f32,
712 feed: f32,
713 kill: f32,
714 flow: f32,
716 inject: f32,
718 colour: common::PaletteParams,
720 pan: common::PanParams,
722 contour: f32,
723 hatch: f32,
724 glow: f32,
725 color_span: f32,
727 color_center: f32,
728 zoom: f32,
731 occlude: f32,
736 palette: Palette,
741}
742
743impl ReactionDiffusionScene {
744 pub fn new(device: &wgpu::Device, surface_format: wgpu::TextureFormat) -> Self {
747 let mut init_params = InitParams {
748 blobs: [[0.0; 4]; MAX_BLOBS],
749 count: [0; 4],
750 };
751 let mut rng = SeededRng::new(SEED);
752 let n = SEED_BLOBS.min(MAX_BLOBS);
753 for slot in init_params.blobs.iter_mut().take(n) {
754 let x = rng.next_f32();
755 let y = rng.next_f32();
756 let r = rng.range(0.02, 0.045);
757 *slot = [x, y, r, 0.0];
758 }
759 init_params.count = [n as u32, 0, 0, 0];
760
761 Self {
762 device: device.clone(),
763 surface_format,
764 res: None,
765 init_params,
766 needs_seed: true,
767 fixed_step: gpu::FixedStep::new(FIXED_STEP, MAX_SUBSTEPS),
768 pending_substeps: 0,
769 stamp_rng: SeededRng::new(INJECT_SEED),
770 pending_stamp: None,
771 prev_inject: 0.0,
772 time: 0.0,
773 feed: DEFAULT_FEED,
774 kill: DEFAULT_KILL,
775 flow: DEFAULT_FLOW,
776 inject: 0.0,
777 colour: common::PaletteParams::new(DEFAULT_HUE, common::DEFAULT_BRIGHTNESS),
778 pan: common::PanParams::default(),
779 contour: DEFAULT_CONTOUR,
780 hatch: DEFAULT_HATCH,
781 glow: DEFAULT_GLOW,
782 color_span: DEFAULT_COLOR_SPAN,
783 color_center: DEFAULT_COLOR_CENTER,
784 zoom: DEFAULT_ZOOM,
785 occlude: crate::render::post::DEFAULT_OCCLUDE,
786 palette: Palette::default_spectrum(),
787 }
788 }
789}
790
791fn sim_bind_group(
792 device: &wgpu::Device,
793 layout: &wgpu::BindGroupLayout,
794 uniform: &wgpu::Buffer,
795 input: &wgpu::TextureView,
796) -> wgpu::BindGroup {
797 device.create_bind_group(&wgpu::BindGroupDescriptor {
798 label: Some("rd-sim-bg"),
799 layout,
800 entries: &[
801 wgpu::BindGroupEntry {
802 binding: 0,
803 resource: uniform.as_entire_binding(),
804 },
805 wgpu::BindGroupEntry {
806 binding: 1,
807 resource: wgpu::BindingResource::TextureView(input),
808 },
809 ],
810 })
811}
812
813fn present_bind_group(
814 device: &wgpu::Device,
815 layout: &wgpu::BindGroupLayout,
816 input: &wgpu::TextureView,
817 sampler: &wgpu::Sampler,
818 uniform: &wgpu::Buffer,
819 luts: &palette::LutPair,
820) -> wgpu::BindGroup {
821 let [lut_a, lut_b, lut_sampler] = luts.bind_entries(3, 4, 5);
822 device.create_bind_group(&wgpu::BindGroupDescriptor {
823 label: Some("rd-present-bg"),
824 layout,
825 entries: &[
826 wgpu::BindGroupEntry {
827 binding: 0,
828 resource: wgpu::BindingResource::TextureView(input),
829 },
830 wgpu::BindGroupEntry {
831 binding: 1,
832 resource: wgpu::BindingResource::Sampler(sampler),
833 },
834 wgpu::BindGroupEntry {
835 binding: 2,
836 resource: uniform.as_entire_binding(),
837 },
838 lut_a,
839 lut_b,
840 lut_sampler,
841 ],
842 })
843}
844
845pub const PARAMS: &[ParamSpec] = &[
848 ParamSpec {
849 name: "feed",
850 default: 0.0367,
851 range: Some([0.01, 0.09]),
852 doc: "Feed rate of the reaction - with `kill`, it is what decides whether you get spots, stripes or mitosis.",
853 kind: ParamKind::Modal,
854 group: ParamGroup::Shape,
855 main: true,
856 },
857 ParamSpec {
858 name: "kill",
859 default: 0.0649,
860 range: Some([0.03, 0.07]),
861 doc: "Kill rate of the reaction; small moves here change the pattern's whole character.",
862 kind: ParamKind::Modal,
863 group: ParamGroup::Shape,
864 main: true,
865 },
866 ParamSpec {
867 name: "flow",
868 default: 1.0,
869 range: Some([0.0, 4.0]),
870 doc: "How fast the simulation advances per second.",
871 kind: ParamKind::Modal,
872 group: ParamGroup::Motion,
873 main: true,
874 },
875 ParamSpec {
876 name: "inject",
877 default: 0.0,
878 range: Some([0.0, 1.0]),
879 doc: "Drops fresh reagent into the field, which is how a beat seeds new growth.",
880 kind: ParamKind::Modal,
881 group: ParamGroup::Motion,
882 main: false,
883 },
884 crate::render::scenes::common::hue(DEFAULT_HUE),
885 ParamSpec {
886 name: "contour",
887 default: 6.0,
888 range: Some([0.0, 24.0]),
889 doc: "How many bands the concentration is drawn as, as a real density: a fraction \
890 slides the whole set of iso-lines. 0 is a smooth gradient.",
891 kind: ParamKind::Modal,
892 group: ParamGroup::Shape,
893 main: false,
894 },
895 ParamSpec {
896 name: "hatch",
897 default: 5.0,
898 range: Some([0.0, 24.0]),
899 doc: "Density of the hatching drawn along the concentration gradient.",
900 kind: ParamKind::Modal,
901 group: ParamGroup::Shape,
902 main: false,
903 },
904 ParamSpec {
905 name: "glow",
906 default: 1.0,
907 range: Some([0.0, 2.0]),
908 doc: "Overall light the field emits.",
909 kind: ParamKind::Modal,
910 group: ParamGroup::Light,
911 main: false,
912 },
913 ParamSpec {
914 name: "color_span",
915 default: 0.85,
916 range: Some([0.0, 1.0]),
917 doc: "How much of the palette the concentration range covers.",
918 kind: ParamKind::Modal,
919 group: ParamGroup::Colour,
920 main: false,
921 },
922 ParamSpec {
923 name: "color_center",
924 default: 0.0,
925 range: Some([-1.0, 1.0]),
926 doc: "Shifts which concentration lands in the middle of the palette.",
927 kind: ParamKind::Modal,
928 group: ParamGroup::Colour,
929 main: false,
930 },
931 crate::render::scenes::common::SATURATION,
932 crate::render::scenes::common::PALETTE_MIX,
933 crate::render::scenes::common::PALETTE_STEPS,
934 crate::render::scenes::common::PALETTE_CONTOUR,
935 crate::render::scenes::common::PALETTE_CONTOUR_STYLE,
936 crate::render::scenes::common::PALETTE_CONTOUR_INK,
937 crate::render::scenes::common::zoom(DEFAULT_ZOOM),
938 crate::render::scenes::common::PAN_X,
939 crate::render::scenes::common::PAN_Y,
940];
941
942impl Scene for ReactionDiffusionScene {
943 fn name(&self) -> &'static str {
944 "reaction diffusion"
945 }
946
947 fn advance(&mut self, dt: f32) {
948 self.pending_substeps = self.fixed_step.advance(dt);
954 }
955
956 fn set_time(&mut self, time: f32) {
957 self.time = time;
958 }
959
960 fn set_occlude(&mut self, occlude: f32) {
961 self.occlude = occlude;
962 }
963
964 fn set_palette(&mut self, palette: &Palette) {
965 self.palette = palette.clone();
968 if let Some(res) = self.res.as_mut() {
969 res.luts.set(palette);
970 }
971 }
972
973 fn reset_params(&mut self) {
974 self.feed = DEFAULT_FEED;
975 self.kill = DEFAULT_KILL;
976 self.flow = DEFAULT_FLOW;
977 self.inject = 0.0;
978 self.colour.reset();
979 self.pan.reset();
980 self.contour = DEFAULT_CONTOUR;
981 self.hatch = DEFAULT_HATCH;
982 self.glow = DEFAULT_GLOW;
983 self.color_span = DEFAULT_COLOR_SPAN;
984 self.color_center = DEFAULT_COLOR_CENTER;
985 self.zoom = DEFAULT_ZOOM;
986 }
987
988 fn set_param(&mut self, name: &str, value: f32) {
989 if self.colour.set(name, value) || self.pan.set(name, value) {
992 return;
993 }
994 match name {
999 "feed" => self.feed = value,
1000 "kill" => self.kill = value,
1001 "flow" => self.flow = value,
1002 "inject" => self.inject = value,
1003 "contour" => self.contour = value,
1004 "hatch" => self.hatch = value,
1005 "glow" => self.glow = value,
1006 "color_span" => self.color_span = value,
1007 "color_center" => self.color_center = value,
1008 "zoom" => self.zoom = value,
1009 _ => {}
1010 }
1011 }
1012
1013 fn update(&mut self, _frame: &AnalysisFrame) {
1014 if self.inject >= INJECT_THRESHOLD && self.prev_inject < INJECT_THRESHOLD {
1019 let cx = self.stamp_rng.next_f32();
1020 let cy = self.stamp_rng.next_f32();
1021 self.pending_stamp = Some([cx, cy, INJECT_RADIUS, INJECT_AMOUNT]);
1022 }
1023 self.prev_inject = self.inject;
1024 }
1025
1026 fn render(
1027 &mut self,
1028 queue: &wgpu::Queue,
1029 encoder: &mut wgpu::CommandEncoder,
1030 view: &wgpu::TextureView,
1031 _aspect: f32,
1032 ) {
1033 if self.res.is_none() {
1037 let mut built = Resources::build(&self.device, self.surface_format);
1038 built.luts.set(&self.palette);
1039 self.res = Some(built);
1040 }
1041 let Self {
1042 res,
1043 init_params,
1044 needs_seed,
1045 pending_substeps,
1046 pending_stamp,
1047 feed,
1048 kill,
1049 flow,
1050 contour,
1051 hatch,
1052 glow,
1053 color_span,
1054 color_center,
1055 colour,
1056 zoom,
1057 pan,
1058 occlude,
1059 ..
1060 } = self;
1061 let Some(res) = res.as_mut() else {
1062 return;
1063 };
1064
1065 res.luts.flush(queue);
1068
1069 queue.write_buffer(
1070 &res.present_uniform,
1071 0,
1072 bytemuck::bytes_of(&PresentParams {
1073 a: [colour.hue, *contour, *hatch, *glow],
1074 b: [*color_span, *color_center, colour.saturation, colour.mix],
1075 c: [*zoom, pan.x, pan.y, *occlude],
1076 d: [
1077 palette::band_steps(colour.steps),
1078 palette::band_contour(colour.contour),
1079 palette::band_contour_style(colour.contour_style),
1080 colour.contour_ink,
1081 ],
1082 }),
1083 );
1084
1085 let inj = pending_stamp.take().unwrap_or([0.0; 4]);
1089 queue.write_buffer(
1090 &res.sim_uniform,
1091 0,
1092 bytemuck::bytes_of(&SimParams {
1093 p: [*feed, *kill, DIFFUSE_U * *flow, DIFFUSE_V * *flow],
1096 inj,
1097 }),
1098 );
1099
1100 if *needs_seed {
1102 queue.write_buffer(&res.init_uniform, 0, bytemuck::bytes_of(init_params));
1103 res.encode_seed(encoder);
1104 *needs_seed = false;
1105 }
1106
1107 for _ in 0..*pending_substeps {
1110 let sim_bg = if res.field.reading_a() {
1111 &res.sim_bg_a
1112 } else {
1113 &res.sim_bg_b
1114 };
1115 {
1116 let mut pass = gpu::color_pass(
1118 encoder,
1119 "rd-sim-pass",
1120 res.field.write_view(),
1121 wgpu::LoadOp::Clear(wgpu::Color::BLACK),
1122 );
1123 pass.set_pipeline(&res.sim_pipeline);
1124 pass.set_bind_group(0, sim_bg, &[]);
1125 pass.draw(0..3, 0..1);
1126 }
1127 res.field.swap();
1128 }
1129
1130 let present_bg = if res.field.reading_a() {
1132 &res.present_bg_a
1133 } else {
1134 &res.present_bg_b
1135 };
1136 let mut pass = gpu::color_pass(encoder, "rd-present-pass", view, wgpu::LoadOp::Load);
1143 pass.set_pipeline(&res.present_pipeline);
1144 pass.set_bind_group(0, present_bg, &[]);
1145 pass.draw(0..3, 0..1);
1146 }
1147}