shader fix
This commit is contained in:
+43
-20
@@ -209,7 +209,10 @@ pub fn print_devices() -> anyhow::Result<()> {
|
||||
let host = cpal::default_host();
|
||||
println!("Input devices:");
|
||||
for (i, d) in host.input_devices()?.enumerate() {
|
||||
println!(" [{i}] {}", d.name().unwrap_or_else(|_| "<unknown>".into()));
|
||||
println!(
|
||||
" [{i}] {}",
|
||||
d.name().unwrap_or_else(|_| "<unknown>".into())
|
||||
);
|
||||
}
|
||||
println!(
|
||||
"Tips: pass an index, or `monitor`/`loopback`, or a file path.\n \
|
||||
@@ -311,7 +314,11 @@ pub fn start(src: Source) -> anyhow::Result<AudioHandle> {
|
||||
move |data: &[f32], _| {
|
||||
for f in data.chunks(channels) {
|
||||
let mid: f32 = f.iter().sum::<f32>() / f.len().max(1) as f32;
|
||||
let side = if f.len() >= 2 { (f[0] - f[1]) * 0.5 } else { 0.0 };
|
||||
let side = if f.len() >= 2 {
|
||||
(f[0] - f[1]) * 0.5
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
push_ms(mid, side);
|
||||
}
|
||||
},
|
||||
@@ -410,9 +417,7 @@ fn spawn_file_source(
|
||||
// back to the device's native rate and linear-resample.
|
||||
let supports_file_sr = dev
|
||||
.supported_output_configs()
|
||||
.map(|mut it| {
|
||||
it.any(|c| c.min_sample_rate() <= file_sr && file_sr <= c.max_sample_rate())
|
||||
})
|
||||
.map(|mut it| it.any(|c| c.min_sample_rate() <= file_sr && file_sr <= c.max_sample_rate()))
|
||||
.unwrap_or(false);
|
||||
let (scfg, out_sr) = if supports_file_sr {
|
||||
(
|
||||
@@ -466,7 +471,7 @@ fn spawn_file_source(
|
||||
loop {
|
||||
let packet = match reader.next_packet() {
|
||||
Ok(Some(p)) => p,
|
||||
Ok(None) => break, // EOF -> stop feeding; output silences
|
||||
Ok(None) => break, // EOF -> stop feeding; output silences
|
||||
Err(_) => break,
|
||||
};
|
||||
if packet.track_id != track_id {
|
||||
@@ -481,7 +486,11 @@ fn spawn_file_source(
|
||||
|
||||
for frame in ilv.chunks(ch) {
|
||||
let mid = frame.iter().sum::<f32>() / ch as f32;
|
||||
let side = if ch >= 2 { (frame[0] - frame[1]) * 0.5 } else { 0.0 };
|
||||
let side = if ch >= 2 {
|
||||
(frame[0] - frame[1]) * 0.5
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
// Emit `resample` output frames per input frame (linear).
|
||||
frac += resample;
|
||||
while frac >= 1.0 {
|
||||
@@ -536,9 +545,9 @@ pub struct Analyzer {
|
||||
agc_loud: f32,
|
||||
agc_broad: f32,
|
||||
agc_csd: f32,
|
||||
pop: [f32; 3], // low/mid/high onset envelopes
|
||||
broad_pop: f32, // broadband onset envelope
|
||||
csd_pop: f32, // complex-domain onset envelope
|
||||
pop: [f32; 3], // low/mid/high onset envelopes
|
||||
broad_pop: f32, // broadband onset envelope
|
||||
csd_pop: f32, // complex-domain onset envelope
|
||||
spec_edges: [(usize, usize); SPEC_N],
|
||||
// Predictive-IOI beat model (robust for breakcore's unstable tempo: it
|
||||
// tracks the running inter-onset interval, not a brittle global BPM).
|
||||
@@ -651,10 +660,8 @@ impl Analyzer {
|
||||
let mut dct = vec![0.0f32; MFCC_N * MEL_N];
|
||||
for k in 1..=MFCC_N {
|
||||
for j in 0..MEL_N {
|
||||
dct[(k - 1) * MEL_N + j] = (std::f32::consts::PI * k as f32
|
||||
* (j as f32 + 0.5)
|
||||
/ MEL_N as f32)
|
||||
.cos();
|
||||
dct[(k - 1) * MEL_N + j] =
|
||||
(std::f32::consts::PI * k as f32 * (j as f32 + 0.5) / MEL_N as f32).cos();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -825,7 +832,11 @@ impl Analyzer {
|
||||
let nbin = (half - 1).max(1) as f32;
|
||||
let gm = (log_sum / nbin).exp();
|
||||
let am = lin_sum / nbin;
|
||||
let flatness = if am > 1e-9 { (gm / am).clamp(0.0, 1.0) } else { 0.0 };
|
||||
let flatness = if am > 1e-9 {
|
||||
(gm / am).clamp(0.0, 1.0)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
// MFCC: mel-filterbank energies (magnitude) -> log -> DCT-II. c0
|
||||
// (overall energy) is dropped; c1.. = pitch-independent timbre.
|
||||
@@ -874,7 +885,11 @@ impl Analyzer {
|
||||
for i in 0..SPEC_N {
|
||||
spec[i] = norm(spec_raw[i], &mut self.agc_spec[i]);
|
||||
}
|
||||
let centroid_hz = if cen_den > 1e-6 { cen_num / cen_den } else { 0.0 };
|
||||
let centroid_hz = if cen_den > 1e-6 {
|
||||
cen_num / cen_den
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let centroid = norm(centroid_hz, &mut self.agc_centroid);
|
||||
let loud = norm(loud_sum / half as f32, &mut self.agc_loud);
|
||||
let broad = norm(broad_flux / half as f32, &mut self.agc_broad);
|
||||
@@ -941,7 +956,11 @@ impl Analyzer {
|
||||
if (0.18..1.20).contains(&obs) {
|
||||
let ratio = obs / self.beat_ioi.max(1e-3);
|
||||
// In-range: trust it. Way off (real tempo change): adopt slowly.
|
||||
let k = if (0.55..1.80).contains(&ratio) { 0.15 } else { 0.05 };
|
||||
let k = if (0.55..1.80).contains(&ratio) {
|
||||
0.15
|
||||
} else {
|
||||
0.05
|
||||
};
|
||||
self.beat_ioi = (self.beat_ioi + (obs - self.beat_ioi) * k).clamp(0.18, 1.0);
|
||||
}
|
||||
self.beat_clock = 0.0;
|
||||
@@ -1028,8 +1047,8 @@ impl Analyzer {
|
||||
if best > ACF_SNAP && !(0.75..1.34).contains(&ratio) {
|
||||
self.beat_ioi = ioi;
|
||||
} else {
|
||||
self.beat_ioi = (self.beat_ioi + (ioi - self.beat_ioi) * (best * 0.20))
|
||||
.clamp(0.18, 1.0);
|
||||
self.beat_ioi =
|
||||
(self.beat_ioi + (ioi - self.beat_ioi) * (best * 0.20)).clamp(0.18, 1.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1092,7 +1111,11 @@ pub fn analyze_file(path: &Path) -> anyhow::Result<Timeline> {
|
||||
decoded.copy_to_vec_interleaved::<f32>(&mut ilv);
|
||||
for frame in ilv.chunks(ch) {
|
||||
let mid = frame.iter().sum::<f32>() / ch as f32;
|
||||
let side = if ch >= 2 { (frame[0] - frame[1]) * 0.5 } else { 0.0 };
|
||||
let side = if ch >= 2 {
|
||||
(frame[0] - frame[1]) * 0.5
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
samples += 1;
|
||||
if let Some(b) = an.push(mid, side) {
|
||||
frames.push(b);
|
||||
|
||||
+23
-11
@@ -38,6 +38,7 @@ use audio_visualizer::viz::core::{RenderContext, Visualizer};
|
||||
use audio_visualizer::viz::fingerprint::{self, Accum as FpAccum};
|
||||
use audio_visualizer::viz::monolith::Monolith;
|
||||
use audio_visualizer::viz::palette::Palette;
|
||||
use audio_visualizer::viz::underground::Underground;
|
||||
use clap::{Parser, ValueEnum};
|
||||
use nannou::app::LoopMode;
|
||||
use nannou::prelude::*;
|
||||
@@ -113,6 +114,7 @@ impl Preset {
|
||||
enum VisMode {
|
||||
Breakcore,
|
||||
Monolith,
|
||||
Underground,
|
||||
}
|
||||
|
||||
impl VisMode {
|
||||
@@ -120,12 +122,14 @@ impl VisMode {
|
||||
match self {
|
||||
VisMode::Breakcore => "breakcore",
|
||||
VisMode::Monolith => "monolith",
|
||||
VisMode::Underground => "underground",
|
||||
}
|
||||
}
|
||||
fn parse(s: &str) -> Option<Self> {
|
||||
Some(match s {
|
||||
"breakcore" => VisMode::Breakcore,
|
||||
"monolith" => VisMode::Monolith,
|
||||
"underground" => VisMode::Underground,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
@@ -133,7 +137,8 @@ impl VisMode {
|
||||
fn next(self) -> Self {
|
||||
match self {
|
||||
VisMode::Breakcore => VisMode::Monolith,
|
||||
VisMode::Monolith => VisMode::Breakcore,
|
||||
VisMode::Monolith => VisMode::Underground,
|
||||
VisMode::Underground => VisMode::Breakcore,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -710,7 +715,11 @@ fn model(app: &App) -> Model {
|
||||
if scaled {
|
||||
cmd.args([
|
||||
"-vf",
|
||||
&format!("scale={w}:{h}:flags=lanczos", w = g.render_w, h = g.render_h),
|
||||
&format!(
|
||||
"scale={w}:{h}:flags=lanczos",
|
||||
w = g.render_w,
|
||||
h = g.render_h
|
||||
),
|
||||
]);
|
||||
}
|
||||
cmd.args([
|
||||
@@ -821,6 +830,7 @@ fn build_visualizer(
|
||||
match mode {
|
||||
VisMode::Breakcore => Box::new(Breakcore::new(seed, device, rw, rh)),
|
||||
VisMode::Monolith => Box::new(Monolith::new(seed, device, rw, rh)),
|
||||
VisMode::Underground => Box::new(Underground::new(device, rw, rh, seed)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -829,8 +839,7 @@ fn build_visualizer(
|
||||
/// the deterministic file-derived seed so the same file looks different per
|
||||
/// invocation but the chosen *form* still tracks the song fingerprint.
|
||||
fn wallclock_seed(app: &App) -> u64 {
|
||||
(app.duration.since_start.as_nanos() as u64)
|
||||
.wrapping_mul(0x9E37_79B9_7F4A_7C15)
|
||||
(app.duration.since_start.as_nanos() as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15)
|
||||
^ 0xD1B5_4A32_D192_ED03
|
||||
}
|
||||
|
||||
@@ -945,18 +954,19 @@ fn update(app: &App, m: &mut Model, upd: Update) {
|
||||
m.visual.install_fingerprint(fp);
|
||||
println!(
|
||||
"fingerprint: centroid={:.2} chroma={} tonal={:.2} dyn={:.2} bpm_cls={:.2}",
|
||||
fp.centroid_mean,
|
||||
fp.chroma_dom,
|
||||
fp.tonality,
|
||||
fp.dyn_range,
|
||||
fp.tempo_class,
|
||||
fp.centroid_mean, fp.chroma_dom, fp.tonality, fp.dyn_range, fp.tempo_class,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Audio-driven motion. The visual's own update grows/morphs/restructures.
|
||||
m.visual.update(&b, dt);
|
||||
m.ca_env += (b.flux - m.ca_env) * if b.flux > m.ca_env { 0.55 } else { 0.10 };
|
||||
let ca_a = if b.flux > m.ca_env {
|
||||
1.0 - (-dt / 0.02).exp()
|
||||
} else {
|
||||
1.0 - (-dt / 0.16).exp()
|
||||
};
|
||||
m.ca_env += (b.flux - m.ca_env) * ca_a;
|
||||
|
||||
let pal = Palette::from_audio(&b);
|
||||
let scale = 1.0 + (b.low * m.g.low).min(0.9) + b.low_on * 0.4;
|
||||
@@ -1024,7 +1034,9 @@ fn view(app: &App, m: &Model, frame: Frame) {
|
||||
if m.hud {
|
||||
let g = m.g;
|
||||
let extra = match &m.mode {
|
||||
Mode::Render { frame: f, total, .. } => format!("RENDER {}/{}", f, total),
|
||||
Mode::Render {
|
||||
frame: f, total, ..
|
||||
} => format!("RENDER {}/{}", f, total),
|
||||
Mode::Live(_) => format!("fps {:.0}", app.fps()),
|
||||
};
|
||||
let fp_state = if m.visual.fingerprint_ready() {
|
||||
|
||||
+28
-28
@@ -110,17 +110,17 @@ enum Kind {
|
||||
/// Audio still rides on top of these via the springs (premise §3).
|
||||
#[derive(Clone, Copy)]
|
||||
struct Regime {
|
||||
scale: f32, // backbone normalize-scale target
|
||||
melt: f32, // smooth-min fuse weight (0..1, scales melt_k)
|
||||
glow: f32, // filament brightness base
|
||||
speed: f32, // rotation + attractor integration multiplier
|
||||
tube: f32, // base capsule radius
|
||||
rib: f32, // spectral-shell presence 0..1
|
||||
deb: f32, // debris presence 0..1
|
||||
heat: f32, // colour-warmth / energy push base 0..1
|
||||
warpd: f32, // spine spectral-displacement amount
|
||||
ca: f32, // chromatic-aberration multiplier
|
||||
fade: f32, // feedback-fade multiplier (smaller ⇒ longer trail)
|
||||
scale: f32, // backbone normalize-scale target
|
||||
melt: f32, // smooth-min fuse weight (0..1, scales melt_k)
|
||||
glow: f32, // filament brightness base
|
||||
speed: f32, // rotation + attractor integration multiplier
|
||||
tube: f32, // base capsule radius
|
||||
rib: f32, // spectral-shell presence 0..1
|
||||
deb: f32, // debris presence 0..1
|
||||
heat: f32, // colour-warmth / energy push base 0..1
|
||||
warpd: f32, // spine spectral-displacement amount
|
||||
ca: f32, // chromatic-aberration multiplier
|
||||
fade: f32, // feedback-fade multiplier (smaller ⇒ longer trail)
|
||||
prefer_knot: bool, // bias the next restructure's backbone kind
|
||||
}
|
||||
|
||||
@@ -388,23 +388,19 @@ impl Breakcore {
|
||||
// §3 springs — regime sets the base, audio + tension ride on top.
|
||||
// Build-up contracts the backbone toward a dense core; release kicks
|
||||
// it back out (spring overshoot makes the drop *punch*).
|
||||
let scale_t = rg.scale * (1.0 - 0.42 * tn) + 0.30 * b.low + 0.25 * b.low_on
|
||||
+ 0.85 * rel;
|
||||
let scale_t = rg.scale * (1.0 - 0.42 * tn) + 0.30 * b.low + 0.25 * b.low_on + 0.85 * rel;
|
||||
self.sp_scale.step(scale_t, 14.0, dt);
|
||||
self.sp_tube
|
||||
.step(rg.tube + 0.05 * b.mid + 0.02 * b.mid_on, 11.0, dt);
|
||||
self.sp_dist.step(3.2 - 0.85 * b.low, 6.0, dt);
|
||||
self.sp_glow.step(
|
||||
rg.glow + 0.45 * b.loud + 0.4 * b.flux + 0.35 * tn,
|
||||
9.0,
|
||||
dt,
|
||||
);
|
||||
self.sp_glow
|
||||
.step(rg.glow + 0.45 * b.loud + 0.4 * b.flux + 0.35 * tn, 9.0, dt);
|
||||
|
||||
// Music-locked rotation. No constant baseline — true silence leaves
|
||||
// the field still; every term is audio · regime · tension. A snare/hat
|
||||
// onset adds a quick rotational *jolt* (decays, so it reads as a kick).
|
||||
let sp = rg.speed * (1.0 + 0.7 * tn);
|
||||
self.jolt = (self.jolt * 0.88).max(0.6 * b.high_on + 0.4 * b.mid_on);
|
||||
self.jolt = (self.jolt * (-7.8 * dt).exp()).max(0.6 * b.high_on + 0.4 * b.mid_on);
|
||||
self.yaw += (0.7 * b.mid + 0.9 * self.jolt) * sp * dt;
|
||||
self.pitch += 0.4 * b.low * sp * dt;
|
||||
self.roll += (0.35 * b.high + 1.4 * self.jolt) * sp * dt;
|
||||
@@ -414,8 +410,11 @@ impl Breakcore {
|
||||
// widens through a build then snaps back on the drop.
|
||||
self.sp_dolly
|
||||
.step(0.55 * b.low_on + 0.45 * b.beat, 20.0, dt);
|
||||
self.sp_focal
|
||||
.step((1.70 + 0.55 * tn - 0.15 * b.loud).clamp(1.25, 2.40), 6.0, dt);
|
||||
self.sp_focal.step(
|
||||
(1.70 + 0.55 * tn - 0.15 * b.loud).clamp(1.25, 2.40),
|
||||
6.0,
|
||||
dt,
|
||||
);
|
||||
|
||||
// Broadband-onset radial shock: the whole sigil pulses out per big
|
||||
// hit, distinct from the section restructure. Decays in ~0.4 s.
|
||||
@@ -550,8 +549,7 @@ impl Breakcore {
|
||||
let kw = (-(dw * dw) * 60.0).exp() * beat_amp;
|
||||
let disp = warpd * (0.05 + 0.55 * band + 0.4 * self.b.high_on) + 0.05 * kw;
|
||||
let p = (base[i] + nrm * disp) * scale;
|
||||
let r = (self.sp_tube.x * (0.4 + 0.7 * band) * (1.0 + 0.8 * kw))
|
||||
.clamp(0.0015, 0.0075);
|
||||
let r = (self.sp_tube.x * (0.4 + 0.7 * band) * (1.0 + 0.8 * kw)).clamp(0.0015, 0.0075);
|
||||
out[i] = [p.x, p.y, p.z, r];
|
||||
}
|
||||
}
|
||||
@@ -626,8 +624,7 @@ impl Breakcore {
|
||||
}
|
||||
let ang = pc as f32 / CHROMA_N as f32 * TAU;
|
||||
// A separate tilted plane from the ribs so spokes read distinctly.
|
||||
let dir = vec3(ang.cos(), 0.35 * (ang * 2.0).sin(), ang.sin())
|
||||
.normalize_or_zero();
|
||||
let dir = vec3(ang.cos(), 0.35 * (ang * 2.0).sin(), ang.sin()).normalize_or_zero();
|
||||
let inner = dir * (0.16 * scale);
|
||||
let outer = dir * ((0.16 + 0.62 * cv) * scale);
|
||||
let r = (self.sp_tube.x * (0.35 + 1.1 * cv)).clamp(0.0015, 0.0075);
|
||||
@@ -684,8 +681,7 @@ impl Breakcore {
|
||||
0.13 + 0.06 * lo,
|
||||
self.hue_b,
|
||||
);
|
||||
let sat = (0.17 + 0.12 * (lo * 0.5 + self.b.mid * 0.4))
|
||||
* (1.0 - 0.25 * self.b.flatness);
|
||||
let sat = (0.17 + 0.12 * (lo * 0.5 + self.b.mid * 0.4)) * (1.0 - 0.25 * self.b.flatness);
|
||||
let acc = oklch((0.62 + 0.28 * lo).min(0.97), sat, self.hue_a);
|
||||
|
||||
let mut u = [0.0f32; UBO_LEN];
|
||||
@@ -727,7 +723,11 @@ impl Breakcore {
|
||||
u[20] = (22.0 + 9.0 * warp.min(2.0)).clamp(16.0, march_cap.min(96) as f32);
|
||||
// Tension fuses the folds (higher melt_k) so a build melts to a core.
|
||||
u[21] = (0.004 + 0.008 * self.b.loud + 0.010 * tn * rg.melt).clamp(0.003, 0.018);
|
||||
u[22] = if feedback && self.gpu.primed() { 1.0 } else { 0.0 };
|
||||
u[22] = if feedback && self.gpu.primed() {
|
||||
1.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
// bounding-sphere radius: covers spine (0.92) + ribs/spokes/debris
|
||||
// (≤~1.05·scale) + tube, and grows with the shock so a pulse never
|
||||
// clips. Parked capsules sit far outside and contribute zero glow.
|
||||
|
||||
+20
-2
@@ -125,7 +125,16 @@ impl Visualizer for Breakcore {
|
||||
c: &RenderContext,
|
||||
) -> Option<&wgpu::Texture> {
|
||||
Some(Breakcore::render(
|
||||
self, device, queue, c.pal, c.scale, c.warp, c.feedback, c.fade, c.ca_px, c.drive,
|
||||
self,
|
||||
device,
|
||||
queue,
|
||||
c.pal,
|
||||
c.scale,
|
||||
c.warp,
|
||||
c.feedback,
|
||||
c.fade,
|
||||
c.ca_px,
|
||||
c.drive,
|
||||
c.march_cap,
|
||||
))
|
||||
}
|
||||
@@ -167,7 +176,16 @@ impl Visualizer for Monolith {
|
||||
c: &RenderContext,
|
||||
) -> Option<&wgpu::Texture> {
|
||||
Some(Monolith::render(
|
||||
self, device, queue, c.pal, c.scale, c.warp, c.feedback, c.fade, c.ca_px, c.drive,
|
||||
self,
|
||||
device,
|
||||
queue,
|
||||
c.pal,
|
||||
c.scale,
|
||||
c.warp,
|
||||
c.feedback,
|
||||
c.fade,
|
||||
c.ca_px,
|
||||
c.drive,
|
||||
c.march_cap,
|
||||
))
|
||||
}
|
||||
|
||||
+1
-2
@@ -83,8 +83,7 @@ fn hash2(mut x: u32) -> f32 {
|
||||
}
|
||||
|
||||
fn grad(ix: i32, iy: i32, seed: u32) -> Vec2 {
|
||||
let h = (ix as u32)
|
||||
.wrapping_mul(0x9E37_79B1)
|
||||
let h = (ix as u32).wrapping_mul(0x9E37_79B1)
|
||||
^ (iy as u32).wrapping_mul(0x85EB_CA77)
|
||||
^ seed.wrapping_mul(0xC2B2_AE3D);
|
||||
let a = hash2(h) * std::f32::consts::TAU;
|
||||
|
||||
+6
-4
@@ -44,9 +44,7 @@ impl Attr {
|
||||
p.x * (rho - p.z) - p.y,
|
||||
p.x * p.y - beta * p.z,
|
||||
),
|
||||
Attr::Rossler { a, b, c } => {
|
||||
vec3(-p.y - p.z, p.x + a * p.y, b + p.z * (p.x - c))
|
||||
}
|
||||
Attr::Rossler { a, b, c } => vec3(-p.y - p.z, p.x + a * p.y, b + p.z * (p.x - c)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,7 +170,11 @@ impl Figure {
|
||||
let t = tau * p[3].max(1.0) * u;
|
||||
let (pn, qn) = (p[0], p[1]);
|
||||
let r = 1.0 + p[2] * (qn * t).cos();
|
||||
vec3(r * (pn * t).cos(), r * (pn * t).sin(), p[2] * (qn * t).sin()) * 0.85
|
||||
vec3(
|
||||
r * (pn * t).cos(),
|
||||
r * (pn * t).sin(),
|
||||
p[2] * (qn * t).sin(),
|
||||
) * 0.85
|
||||
}
|
||||
1 => {
|
||||
let sf = |ang: f32, m: f32, n1: f32, n2: f32, n3: f32| -> f32 {
|
||||
|
||||
@@ -17,3 +17,4 @@ pub mod palette;
|
||||
pub mod post;
|
||||
pub mod shader;
|
||||
pub mod structure;
|
||||
pub mod underground;
|
||||
|
||||
+69
-68
@@ -38,6 +38,7 @@ use crate::viz::math::{Spring, angle_to};
|
||||
use crate::viz::palette::{Palette, oklch};
|
||||
use crate::viz::post::read_texture_rgba;
|
||||
use crate::viz::shader::ShaderPipeline;
|
||||
use crate::viz::structure::{Arch, Structure};
|
||||
use nannou::wgpu;
|
||||
|
||||
/// UBO length in f32: **10** std140 rows (40). Header layout is duplicated
|
||||
@@ -98,6 +99,8 @@ pub struct Monolith {
|
||||
fp_committed: bool,
|
||||
accent_class: Accent,
|
||||
|
||||
structure: Structure,
|
||||
|
||||
// springs
|
||||
sp_scale: Spring, // sub-bass breath (bulb world scale)
|
||||
sp_glow: Spring,
|
||||
@@ -152,6 +155,7 @@ impl Monolith {
|
||||
fp: Fingerprint::default(),
|
||||
fp_committed: false,
|
||||
accent_class: Accent::Cyan,
|
||||
structure: Structure::new(),
|
||||
sp_scale: Spring { x: 1.0, v: 0.0 },
|
||||
sp_glow: Spring { x: 0.55, v: 0.0 },
|
||||
sp_power: Spring { x: 8.0, v: 0.0 },
|
||||
@@ -217,6 +221,10 @@ impl Monolith {
|
||||
self.frame = self.frame.wrapping_add(1);
|
||||
self.b = *b;
|
||||
|
||||
self.structure.update(b, dt);
|
||||
let arch = self.structure.arch();
|
||||
let release = self.structure.release();
|
||||
|
||||
// Dynamic-range scaled motion (premise: quiet songs move slowly).
|
||||
let dyn_m = (0.40 + 0.85 * self.fp.dyn_range).clamp(0.40, 1.20);
|
||||
|
||||
@@ -230,15 +238,17 @@ impl Monolith {
|
||||
let flux_q = b.flux * b.flux;
|
||||
|
||||
// --- sub-bass breath: sp_scale grows on low²; the kick still reads,
|
||||
// a mid-range hum doesn't. Range now ≈ 1.0..1.20 instead of ≈1.0..1.55.
|
||||
let scale_t = 1.0 + 0.20 * low_q + 0.12 * b.low_on;
|
||||
self.sp_scale.step(scale_t, 4.0 * dyn_m, dt);
|
||||
let dist_t = 2.8 + 0.25 * low_q + 0.10 * b.low_on;
|
||||
// a mid-range hum doesn't.
|
||||
let scale_t = 1.0 + 0.40 * low_q + 0.22 * b.low_on;
|
||||
self.sp_scale.step(scale_t, 5.0 * dyn_m, dt);
|
||||
|
||||
// --- distance: kick pulls back into void; release kick-back shove.
|
||||
let dist_t = 2.8 + 0.35 * low_q + 0.15 * b.low_on;
|
||||
self.sp_dist.step(dist_t, 4.0 * dyn_m, dt);
|
||||
self.sp_dist.x += 1.2 * release * release; // Immediate shove on drop
|
||||
|
||||
// --- bulb power: small drift around 8 — mid² nudges it gently and
|
||||
// the fingerprint's tonality biases the resting point (tonal music
|
||||
// keeps a cleaner low-power bulb, noisy/atonal goes higher).
|
||||
// the fingerprint's tonality biases the resting point.
|
||||
let power_t = 7.8 + 0.5 * self.fp.tonality + 0.2 * (mid_q - 0.25);
|
||||
self.sp_power.step(power_t.clamp(6.5, 9.5), 2.0, dt);
|
||||
|
||||
@@ -250,31 +260,32 @@ impl Monolith {
|
||||
dt,
|
||||
);
|
||||
|
||||
// --- glitch envelope: smoothed hi-band onset / flux with a deadband
|
||||
// (subtract `min_trigger` so a steady noise floor produces nothing).
|
||||
// Slow attack so a single hi-hat tap doesn't ping the grid; slow
|
||||
// decay so a snare roll holds the effect through the burst.
|
||||
// --- glitch envelope: smoothed hi-band onset / flux with a deadband.
|
||||
let raw = 0.7 * b.high_on * b.high_on + 0.35 * flux_q;
|
||||
let glitch_target = (raw - 0.10).max(0.0).min(1.2);
|
||||
let a_g = if glitch_target > self.glitch_env {
|
||||
0.18
|
||||
let glitch_target = (raw - 0.10).clamp(0.0, 1.2);
|
||||
let g_tau = if glitch_target > self.glitch_env {
|
||||
0.08
|
||||
} else {
|
||||
0.04
|
||||
0.40
|
||||
};
|
||||
let a_g = 1.0 - (-dt / g_tau).exp();
|
||||
self.glitch_env += (glitch_target - self.glitch_env) * a_g;
|
||||
|
||||
// --- stutter FSM (the "drops to 12 fps" simulation). Triggered only
|
||||
// by real snare-flux bursts — both bands strong, or one very strong.
|
||||
// Gate is wide (0.45 s) so a busy fill can't latch repeatedly; the
|
||||
// hold itself is short (~80 ms) so the freeze reads as a tic, not a
|
||||
// dropped section.
|
||||
// by real snare-flux bursts.
|
||||
self.stutter_gate = (self.stutter_gate - dt).max(0.0);
|
||||
self.stutter_w = (self.stutter_w - dt / 0.08).max(0.0);
|
||||
let snare_burst =
|
||||
(b.high_on > 0.72 && b.flux > 0.65) || b.flux > 0.90 || b.high_on > 0.85;
|
||||
let snare_burst = (b.high_on > 0.75 && b.flux > 0.70) || b.flux > 0.94 || b.high_on > 0.88;
|
||||
|
||||
// Gate stutter more aggressively during drops/builds so it doesn't "stuck" the energy.
|
||||
let stutter_cooldown = match arch {
|
||||
Arch::Drop | Arch::Build => 0.65,
|
||||
_ => 0.45,
|
||||
};
|
||||
|
||||
if snare_burst && self.stutter_gate <= 0.0 {
|
||||
self.stutter_w = 1.0;
|
||||
self.stutter_gate = 0.45;
|
||||
self.stutter_gate = stutter_cooldown;
|
||||
self.held_yaw = self.yaw;
|
||||
self.held_pitch = self.pitch;
|
||||
self.held_roll = self.roll;
|
||||
@@ -283,24 +294,24 @@ impl Monolith {
|
||||
self.held_glow = self.sp_glow.x;
|
||||
}
|
||||
|
||||
// --- camera (BPM-paced + small audio jitter). No constant baseline
|
||||
// beyond tempo — true silence keeps it nearly still. All terms
|
||||
// gated quadratically so a quiet passage holds a steady frame.
|
||||
let base_rate = 0.05 + 0.10 * self.fp.tempo_class;
|
||||
let rate = base_rate * dyn_m;
|
||||
self.yaw += rate * dt + 0.10 * b.beat * b.beat * dt;
|
||||
self.pitch += (0.06 * low_q - 0.02) * rate * dt;
|
||||
self.roll += 0.02 * high_q * dt;
|
||||
// --- camera: Section-aware rotation rate.
|
||||
let energy_mult = match arch {
|
||||
Arch::Drop => 2.4,
|
||||
Arch::Build => 1.4 + self.structure.tension(),
|
||||
Arch::Ambient | Arch::Breakdown => 0.45,
|
||||
_ => 1.0,
|
||||
};
|
||||
|
||||
// --- swirl: mid² accumulates the feedback rotation. Sustained pad
|
||||
// = a slow drift; sparse mids = nearly stationary trail.
|
||||
let base_rate = 0.05 + 0.10 * self.fp.tempo_class;
|
||||
let rate = base_rate * dyn_m * energy_mult;
|
||||
self.yaw += rate * dt + 0.15 * b.beat * b.beat * dt;
|
||||
self.pitch += (0.12 * low_q - 0.04) * rate * dt;
|
||||
self.roll += 0.04 * high_q * dt;
|
||||
|
||||
// --- swirl: mid² accumulates the feedback rotation.
|
||||
self.swirl_phase += (0.15 + 0.6 * mid_q) * 0.30 * dt;
|
||||
|
||||
// --- colour inertia. Base hue drifts blue↔purple with centroid; the
|
||||
// two accents ease toward the neon class + its partner so contrast
|
||||
// stays alive. Each accent picks up a small audio nudge (mid drifts
|
||||
// the primary, high drifts the secondary) so the hues breathe with
|
||||
// the music instead of being statically committed.
|
||||
// --- colour inertia.
|
||||
let base_t = (5.0 - b.centroid * 1.4).rem_euclid(TAU);
|
||||
let acc_t = self.accent_class.hue()
|
||||
+ (self.fp.chroma_dom as f32) / 12.0 * 0.3
|
||||
@@ -312,10 +323,7 @@ impl Monolith {
|
||||
self.hue_a2 = angle_to(self.hue_a2, acc2_t, ha);
|
||||
}
|
||||
|
||||
/// Render this frame into the target and return it. Bin tunables match
|
||||
/// the other modes' contract so cfg keys stay shared (`fade`/`ca_px`/
|
||||
/// `drive`/`march_cap`); `warp` is unused — monolith has its own
|
||||
/// audio-driven swirl so the bin's noise-warp slot is a no-op here.
|
||||
/// Render this frame into the target and return it.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn render(
|
||||
&mut self,
|
||||
@@ -332,10 +340,9 @@ impl Monolith {
|
||||
) -> &wgpu::Texture {
|
||||
let dr = drive.clamp(0.0, 3.0);
|
||||
let dyn_m = (0.40 + 0.85 * self.fp.dyn_range).clamp(0.40, 1.20);
|
||||
let arch = self.structure.arch();
|
||||
|
||||
// Held-vs-live blending: while stutter is high, upload the held
|
||||
// values so the picture freezes; the shader also pins the feedback
|
||||
// floor near 1.0 from `stutter_w` so the trail survives unchanged.
|
||||
// Held-vs-live blending
|
||||
let s = self.stutter_w.clamp(0.0, 1.0);
|
||||
let yaw = self.yaw * (1.0 - s) + self.held_yaw * s;
|
||||
let pitch = self.pitch * (1.0 - s) + self.held_pitch * s;
|
||||
@@ -344,10 +351,7 @@ impl Monolith {
|
||||
let scl = self.sp_scale.x * (1.0 - s) + self.held_scale * s;
|
||||
let glow = self.sp_glow.x * (1.0 - s) + self.held_glow * s;
|
||||
|
||||
// Palette — base lands deep dark silver/blue; two accents on
|
||||
// contrasting neon classes so a frame never reads as one hue.
|
||||
// Lightness rides loud² so quiet stays dark; saturation rides
|
||||
// tonality. Capped low so neither accent can wash the frame.
|
||||
// Palette
|
||||
let lo = self.b.loud;
|
||||
let base = oklch(
|
||||
(0.14 + 0.14 * lo * lo).min(0.42),
|
||||
@@ -356,8 +360,6 @@ impl Monolith {
|
||||
);
|
||||
let acc_sat = (0.18 + 0.05 * lo) * (0.65 + 0.40 * self.fp.tonality);
|
||||
let acc = oklch((0.68 + 0.20 * lo).min(0.92), acc_sat, self.hue_a);
|
||||
// Secondary accent slightly less saturated so the body's primary tint
|
||||
// still reads — the partner is a *contrast*, not a competing colour.
|
||||
let acc2 = oklch(
|
||||
(0.66 + 0.20 * lo).min(0.90),
|
||||
(acc_sat * 0.85).min(0.30),
|
||||
@@ -366,29 +368,22 @@ impl Monolith {
|
||||
|
||||
let mut u = [0.0f32; UBO_LEN];
|
||||
|
||||
// row0 cam — held during stutter (see above)
|
||||
// row0 cam
|
||||
u[0] = yaw;
|
||||
u[1] = pitch;
|
||||
u[2] = roll;
|
||||
u[3] = dist.clamp(1.8, 4.5);
|
||||
|
||||
// row1 p0 = scale, glow_gain, ca_px, edge_softness
|
||||
// `scale` (caller's expressive multiplier) rides on top of breath.
|
||||
u[4] = (scale * scl).clamp(0.4, 1.8);
|
||||
u[4] = (scale * scl).clamp(0.4, 2.2);
|
||||
u[5] = glow.clamp(0.18, 0.85);
|
||||
// CA: small base, stutter lifts it modestly (the smear during freeze
|
||||
// reads as signal corruption — but not screen-wide prism shimmer).
|
||||
u[6] = ca_px * (0.6 + 0.4 * dr) * (1.0 + 0.6 * s);
|
||||
// Tonal music keeps a crisp particle edge; noisy/atonal softens.
|
||||
u[7] = (0.50 + 0.45 * (1.0 - self.fp.tonality) + 0.15 * self.b.flatness)
|
||||
.clamp(0.30, 1.10);
|
||||
u[7] = (0.50 + 0.45 * (1.0 - self.fp.tonality) + 0.15 * self.b.flatness).clamp(0.30, 1.10);
|
||||
|
||||
// row2 col0 = base.rgb, fade
|
||||
u[8] = base[0];
|
||||
u[9] = base[1];
|
||||
u[10] = base[2];
|
||||
// Wider fade (longer trail) when dyn_motion is low (calm tracks leave
|
||||
// more wake). Drive doesn't pull it shorter — the stutter does.
|
||||
u[11] = (fade * (1.0 - 0.25 * (1.0 - dyn_m))).clamp(0.02, 1.0);
|
||||
|
||||
// row3 col1 = accent.rgb, flash
|
||||
@@ -403,17 +398,23 @@ impl Monolith {
|
||||
u[18] = (self.frame & 0xffff) as f32;
|
||||
u[19] = self.t;
|
||||
|
||||
// row5 p2 = march_steps, power_n, feedback_on, world_r (bounding)
|
||||
// Mandelbulb takes ~8 iters/step — heavier than the capsule field, so
|
||||
// hold the request slightly lower than breakcore's.
|
||||
// row5 p2 = march_steps, power_n, feedback_on, world_r
|
||||
u[20] = (20.0 + 7.0 * dr).clamp(16.0, march_cap.min(96) as f32);
|
||||
u[21] = self.sp_power.x.clamp(6.5, 9.5);
|
||||
u[22] = if feedback && self.gpu.primed() { 1.0 } else { 0.0 };
|
||||
// Bulb fits in r ≈ 1.25; pad for breath + sub-bass extension.
|
||||
u[23] = (1.30 * scl + 0.20).clamp(1.0, 3.0);
|
||||
u[22] = if feedback && self.gpu.primed() {
|
||||
1.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
u[23] = (1.30 * scl + 0.20).clamp(1.0, 3.5);
|
||||
|
||||
// row6 p3 = grain, glitch_a, fog, beat
|
||||
u[24] = (0.006 + 0.014 * self.b.flux * dr).clamp(0.0, 0.022);
|
||||
// Gated grain: ambient/breakdown reduces grain base to near zero.
|
||||
let g_base = match arch {
|
||||
Arch::Ambient | Arch::Breakdown => 0.001,
|
||||
_ => 0.005,
|
||||
};
|
||||
u[24] = (g_base + 0.012 * self.b.flux * dr).clamp(0.0, 0.022);
|
||||
u[25] = (self.glitch_env * dr).clamp(0.0, 1.2);
|
||||
u[26] = (0.30 + 0.35 * self.b.loud).clamp(0.20, 0.75);
|
||||
u[27] = self.b.beat;
|
||||
@@ -430,11 +431,11 @@ impl Monolith {
|
||||
u[34] = self.rw as f32 / self.rh.max(1) as f32;
|
||||
u[35] = self.fp.tonality.clamp(0.0, 1.0);
|
||||
|
||||
// row9 col2 = secondary accent.rgb, _ — paints surface contrast
|
||||
// row9 col2 = secondary accent.rgb, release
|
||||
u[36] = acc2[0];
|
||||
u[37] = acc2[1];
|
||||
u[38] = acc2[2];
|
||||
u[39] = 0.0;
|
||||
u[39] = self.structure.release();
|
||||
|
||||
self.gpu.render(device, queue, &u)
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ struct U {
|
||||
p3: vec4<f32>, // grain, glitch_a, fog, beat
|
||||
p4: vec4<f32>, // loud, low, mid, high
|
||||
p5: vec4<f32>, // stutter_w, swirl, aspect, tonality
|
||||
col2: vec4<f32>, // accent2.rgb (secondary neon), _
|
||||
col2: vec4<f32>, // accent2.rgb (secondary neon), release
|
||||
};
|
||||
|
||||
@group(0) @binding(0) var<uniform> u: U;
|
||||
@@ -141,8 +141,10 @@ fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
|
||||
let swirl = u.p5.y; // accumulated swirl angle (rad)
|
||||
let aspect = u.p5.z;
|
||||
let tonal = u.p5.w;
|
||||
let release = u.col2.w;
|
||||
|
||||
// --- hi-band glitch grid: coarse-cell UV displacement of the FEEDBACK
|
||||
// --- hi-band glitch grid
|
||||
// : coarse-cell UV displacement of the FEEDBACK
|
||||
// sample only — the bulb itself stays geometrically stable so a busy hat
|
||||
// pattern can't tear the whole picture. Deadband below 0.35 so quiet
|
||||
// music produces no glitch at all; only a small fraction of cells shove
|
||||
@@ -166,12 +168,11 @@ fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
|
||||
let uv = in.uv;
|
||||
let ndc = vec2<f32>(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0);
|
||||
let dist = u.cam.w;
|
||||
let focal = 1.6;
|
||||
let focal = 1.6 + 0.4 * release * release; // FOV warp on drop
|
||||
let ro = vec3<f32>(0.0, 0.0, -dist);
|
||||
let rd = normalize(vec3<f32>(ndc.x * aspect, ndc.y, focal));
|
||||
|
||||
// Ray vs bounding sphere — the background-pixel early-out that keeps the
|
||||
// raymarch from melting the GPU. `rb` is set to (scale + breath + pad).
|
||||
// Ray vs bounding sphere
|
||||
let b = dot(ro, rd);
|
||||
let c = dot(ro, ro) - rb * rb;
|
||||
let disc = b * b - c;
|
||||
@@ -234,12 +235,11 @@ fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
|
||||
// zero, only strong snares light the edge.
|
||||
body = body + accent2 * highf * highf * fres * fres * 0.40;
|
||||
}
|
||||
// Particle-dither: per-pixel hash threshold against intensity.
|
||||
// Each pixel is a "particle" that either shows or doesn't —
|
||||
// identifies the form as point-cloud rather than solid surface.
|
||||
// Particle-dither: less hectic in quiet parts
|
||||
let dither_thresh = clamp(inten * 2.2 - 0.08, 0.0, 1.0);
|
||||
let pcell = hash21(uv * vec2<f32>(res_w, res_h) * 0.85
|
||||
+ vec2<f32>(frame * 0.013, frame * 0.029));
|
||||
let pkeep = step(pcell, clamp(inten * 1.65 + 0.12, 0.0, 1.0));
|
||||
let pkeep = step(pcell, dither_thresh);
|
||||
col = body * inten * (0.50 + 0.55 * pkeep);
|
||||
// Core punch — uses the per-pixel tint so the bulb's deep core
|
||||
// glows different shades in different regions, not one hot dot.
|
||||
|
||||
+1
-3
@@ -37,9 +37,7 @@ use nannou::wgpu;
|
||||
/// exactly this invariant for its one UBO, so this needs no `bytemuck` dep —
|
||||
/// but `U` MUST stay padding-free and match the shader's UBO layout.
|
||||
fn as_bytes<U: Copy>(u: &U) -> &[u8] {
|
||||
unsafe {
|
||||
std::slice::from_raw_parts((u as *const U).cast::<u8>(), std::mem::size_of::<U>())
|
||||
}
|
||||
unsafe { std::slice::from_raw_parts((u as *const U).cast::<u8>(), std::mem::size_of::<U>()) }
|
||||
}
|
||||
|
||||
/// Fullscreen fragment pipeline with ping-pong feedback. `U` is the uniform
|
||||
|
||||
@@ -93,8 +93,7 @@ impl Structure {
|
||||
let a_bass = 1.0 - (-dt / 0.8).exp();
|
||||
let a_long = 1.0 - (-dt / 8.0).exp();
|
||||
|
||||
let busy_in =
|
||||
(b.flux + 0.5 * (b.low_on + b.mid_on + b.high_on)).min(1.5);
|
||||
let busy_in = (b.flux + 0.5 * (b.low_on + b.mid_on + b.high_on)).min(1.5);
|
||||
self.e += (b.loud - self.e) * a_fast;
|
||||
self.br += (b.centroid - self.br) * a_fast;
|
||||
self.ba += (b.low - self.ba) * a_bass;
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
//! underground — geometric techno/dnb visuals. grid, boxes, strobe.
|
||||
//!
|
||||
//! §1 geometry: 8x8 grid of boxes. each box size/rotation tracks band energy.
|
||||
//! §2 audio: kicks drive global scale + strobe. high-flux drive chromatic aberration.
|
||||
//! §3 render: raymarcher with box SDF. volumetric glow + post-fx.
|
||||
|
||||
use crate::audio::Bands;
|
||||
use crate::viz::core::{RenderContext, Visualizer};
|
||||
use crate::viz::math::Spring;
|
||||
use crate::viz::post::read_texture_rgba;
|
||||
use crate::viz::shader::ShaderPipeline;
|
||||
use nannou::wgpu;
|
||||
|
||||
const N: usize = 64; // 8x8 grid
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
struct UndergroundUbo {
|
||||
// p0..p10: 11 rows of 4 floats (std140)
|
||||
header: [f32; 44],
|
||||
// NP * vec4
|
||||
points: [[f32; 4]; N],
|
||||
}
|
||||
|
||||
pub struct Underground {
|
||||
seed: u64,
|
||||
pipe: ShaderPipeline<UndergroundUbo>,
|
||||
boxes: [BoxState; N],
|
||||
// Global reactives
|
||||
strobe: Spring,
|
||||
scale: Spring,
|
||||
flux: Spring,
|
||||
time: f32,
|
||||
w: u32,
|
||||
h: u32,
|
||||
}
|
||||
|
||||
struct BoxState {
|
||||
size: Spring,
|
||||
rot_v: f32,
|
||||
rot: f32,
|
||||
}
|
||||
|
||||
impl Underground {
|
||||
pub fn new(device: &wgpu::Device, w: u32, h: u32, seed: u64) -> Self {
|
||||
let wgsl = include_str!("underground.wgsl");
|
||||
let pipe = ShaderPipeline::new(device, wgsl, w, h, wgpu::TextureFormat::Rgba8UnormSrgb);
|
||||
|
||||
let mut boxes = Vec::with_capacity(N);
|
||||
for _ in 0..N {
|
||||
boxes.push(BoxState {
|
||||
size: Spring { x: 0.1, v: 0.0 },
|
||||
rot_v: 0.0,
|
||||
rot: 0.0,
|
||||
});
|
||||
}
|
||||
|
||||
Self {
|
||||
seed,
|
||||
pipe,
|
||||
boxes: boxes.try_into().unwrap_or_else(|_| panic!("N mismatch")),
|
||||
strobe: Spring { x: 0.0, v: 0.0 },
|
||||
scale: Spring { x: 1.0, v: 0.0 },
|
||||
flux: Spring { x: 0.0, v: 0.0 },
|
||||
time: 0.0,
|
||||
w,
|
||||
h,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Visualizer for Underground {
|
||||
fn name(&self) -> &'static str {
|
||||
"underground"
|
||||
}
|
||||
fn seed(&self) -> u64 {
|
||||
self.seed
|
||||
}
|
||||
fn reseed(&mut self, seed: u64) {
|
||||
self.seed = seed;
|
||||
}
|
||||
|
||||
fn update(&mut self, b: &Bands, dt: f32) {
|
||||
self.time += dt;
|
||||
|
||||
// Global reaction
|
||||
self.scale.step(1.0 + b.low * 0.5, 15.0, dt);
|
||||
self.strobe
|
||||
.step(if b.low_on > 0.8 { 1.0 } else { 0.0 }, 30.0, dt);
|
||||
self.flux.step(b.flux, 10.0, dt);
|
||||
|
||||
// Per-box update
|
||||
for i in 0..N {
|
||||
let energy = b.spec[i % b.spec.len()];
|
||||
self.boxes[i].size.step(0.1 + energy * 0.8, 20.0, dt);
|
||||
self.boxes[i].rot_v += energy * dt * 20.0;
|
||||
self.boxes[i].rot_v *= 0.95; // friction
|
||||
self.boxes[i].rot += self.boxes[i].rot_v * dt;
|
||||
}
|
||||
}
|
||||
|
||||
fn element_count(&self) -> usize {
|
||||
N
|
||||
}
|
||||
fn is_gpu(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn render_gpu(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
ctx: &RenderContext,
|
||||
) -> Option<&wgpu::Texture> {
|
||||
let mut points = [[0.0; 4]; N];
|
||||
for i in 0..N {
|
||||
let x = (i % 8) as f32 - 3.5;
|
||||
let y = (i / 8) as f32 - 3.5;
|
||||
points[i] = [x, y, self.boxes[i].size.x, self.boxes[i].rot];
|
||||
}
|
||||
|
||||
let mut header = [0.0; 44];
|
||||
header[0] = self.time;
|
||||
header[1] = self.scale.x;
|
||||
header[2] = self.strobe.x;
|
||||
header[3] = self.flux.x;
|
||||
header[4] = ctx.scale;
|
||||
header[5] = ctx.warp;
|
||||
header[6] = if ctx.feedback { 1.0 } else { 0.0 };
|
||||
header[7] = ctx.fade;
|
||||
header[8] = ctx.ca_px;
|
||||
header[9] = ctx.drive;
|
||||
header[10] = ctx.march_cap as f32;
|
||||
header[40] = self.w as f32;
|
||||
header[41] = self.h as f32;
|
||||
header[42] = (self.w as f32) / (self.h as f32);
|
||||
|
||||
let ubo = UndergroundUbo { header, points };
|
||||
Some(self.pipe.render(device, queue, &ubo))
|
||||
}
|
||||
|
||||
fn current_tex(&self) -> Option<&wgpu::Texture> {
|
||||
Some(self.pipe.current())
|
||||
}
|
||||
|
||||
fn capture_raw(
|
||||
&self,
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
) -> Option<anyhow::Result<Vec<u8>>> {
|
||||
Some(read_texture_rgba(
|
||||
device,
|
||||
queue,
|
||||
self.pipe.current(),
|
||||
self.w,
|
||||
self.h,
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
struct Ubo {
|
||||
header: array<vec4<f32>, 11>,
|
||||
points: array<vec4<f32>, 64>,
|
||||
}
|
||||
|
||||
@group(0) @binding(0) var<uniform> ubo: Ubo;
|
||||
@group(0) @binding(1) var prev_tex: texture_2d<f32>;
|
||||
@group(0) @binding(2) var smp: sampler;
|
||||
|
||||
struct VertexOutput {
|
||||
@builtin(position) position: vec4<f32>,
|
||||
@location(0) uv: vec2<f32>,
|
||||
}
|
||||
|
||||
@vertex
|
||||
fn vs_main(@builtin(vertex_index) vi: u32) -> VertexOutput {
|
||||
var out: VertexOutput;
|
||||
let uv = vec2<f32>(f32((vi << 1u) & 2u), f32(vi & 2u));
|
||||
out.uv = uv;
|
||||
out.position = vec4<f32>(uv * 2.0 - 1.0, 0.0, 1.0);
|
||||
return out;
|
||||
}
|
||||
|
||||
fn sdBox(p: vec3<f32>, b: vec3<f32>) -> f32 {
|
||||
let q = abs(p) - b;
|
||||
return length(max(q, vec3<f32>(0.0))) + min(max(q.x, max(q.y, q.z)), 0.0);
|
||||
}
|
||||
|
||||
fn rotate(p: vec2<f32>, a: f32) -> vec2<f32> {
|
||||
let s = sin(a);
|
||||
let c = cos(a);
|
||||
return vec2<f32>(p.x * c - p.y * s, p.x * s + p.y * c);
|
||||
}
|
||||
|
||||
fn map(p: vec3<f32>) -> f32 {
|
||||
var d = 1e10;
|
||||
let grid_size = 1.0;
|
||||
|
||||
for (var i = 0; i < 64; i = i + 1) {
|
||||
let pt = ubo.points[i];
|
||||
let pos = vec3<f32>(pt.x, pt.y, 0.0);
|
||||
var q = p - pos;
|
||||
|
||||
q.x = rotate(q.xz, pt.w).x; // basic rotate
|
||||
|
||||
let size = pt.z * 0.4;
|
||||
d = min(d, sdBox(q, vec3<f32>(size)));
|
||||
}
|
||||
|
||||
return d;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||
let time = ubo.header[0].x;
|
||||
let scale = ubo.header[0].y;
|
||||
let strobe = ubo.header[0].z;
|
||||
let flux = ubo.header[0].w;
|
||||
|
||||
let res = vec2<f32>(ubo.header[10].x, ubo.header[10].y);
|
||||
let aspect = ubo.header[10].z;
|
||||
|
||||
var uv = (in.position.xy / res) * 2.0 - 1.0;
|
||||
uv.x *= aspect;
|
||||
|
||||
let ro = vec3<f32>(0.0, 0.0, -8.0 / scale);
|
||||
let rd = normalize(vec3<f32>(uv, 2.0));
|
||||
|
||||
var t = 0.0;
|
||||
var glow = 0.0;
|
||||
let march_cap = i32(ubo.header[2].z);
|
||||
|
||||
for (var i = 0; i < 96; i = i + 1) {
|
||||
if (i >= march_cap) { break; }
|
||||
let p = ro + rd * t;
|
||||
let d = map(p);
|
||||
|
||||
glow += exp(-d * 4.0) * (0.01 + flux * 0.05);
|
||||
|
||||
if (d < 0.001 || t > 20.0) { break; }
|
||||
t += d * 0.8;
|
||||
}
|
||||
|
||||
var col = vec3<f32>(glow);
|
||||
col *= vec3<f32>(0.2, 0.5, 1.0); // Cyan-ish
|
||||
|
||||
if (strobe > 0.5) {
|
||||
col += 0.2;
|
||||
}
|
||||
|
||||
// Scanlines
|
||||
col *= 0.8 + 0.2 * sin(in.position.y * 1.5 + time * 10.0);
|
||||
|
||||
return vec4<f32>(col, 1.0);
|
||||
}
|
||||
Reference in New Issue
Block a user