shader fix
This commit is contained in:
+43
-20
@@ -209,7 +209,10 @@ pub fn print_devices() -> anyhow::Result<()> {
|
|||||||
let host = cpal::default_host();
|
let host = cpal::default_host();
|
||||||
println!("Input devices:");
|
println!("Input devices:");
|
||||||
for (i, d) in host.input_devices()?.enumerate() {
|
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!(
|
println!(
|
||||||
"Tips: pass an index, or `monitor`/`loopback`, or a file path.\n \
|
"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], _| {
|
move |data: &[f32], _| {
|
||||||
for f in data.chunks(channels) {
|
for f in data.chunks(channels) {
|
||||||
let mid: f32 = f.iter().sum::<f32>() / f.len().max(1) as f32;
|
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);
|
push_ms(mid, side);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -410,9 +417,7 @@ fn spawn_file_source(
|
|||||||
// back to the device's native rate and linear-resample.
|
// back to the device's native rate and linear-resample.
|
||||||
let supports_file_sr = dev
|
let supports_file_sr = dev
|
||||||
.supported_output_configs()
|
.supported_output_configs()
|
||||||
.map(|mut it| {
|
.map(|mut it| it.any(|c| c.min_sample_rate() <= file_sr && file_sr <= c.max_sample_rate()))
|
||||||
it.any(|c| c.min_sample_rate() <= file_sr && file_sr <= c.max_sample_rate())
|
|
||||||
})
|
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
let (scfg, out_sr) = if supports_file_sr {
|
let (scfg, out_sr) = if supports_file_sr {
|
||||||
(
|
(
|
||||||
@@ -466,7 +471,7 @@ fn spawn_file_source(
|
|||||||
loop {
|
loop {
|
||||||
let packet = match reader.next_packet() {
|
let packet = match reader.next_packet() {
|
||||||
Ok(Some(p)) => p,
|
Ok(Some(p)) => p,
|
||||||
Ok(None) => break, // EOF -> stop feeding; output silences
|
Ok(None) => break, // EOF -> stop feeding; output silences
|
||||||
Err(_) => break,
|
Err(_) => break,
|
||||||
};
|
};
|
||||||
if packet.track_id != track_id {
|
if packet.track_id != track_id {
|
||||||
@@ -481,7 +486,11 @@ fn spawn_file_source(
|
|||||||
|
|
||||||
for frame in ilv.chunks(ch) {
|
for frame in ilv.chunks(ch) {
|
||||||
let mid = frame.iter().sum::<f32>() / ch as f32;
|
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).
|
// Emit `resample` output frames per input frame (linear).
|
||||||
frac += resample;
|
frac += resample;
|
||||||
while frac >= 1.0 {
|
while frac >= 1.0 {
|
||||||
@@ -536,9 +545,9 @@ pub struct Analyzer {
|
|||||||
agc_loud: f32,
|
agc_loud: f32,
|
||||||
agc_broad: f32,
|
agc_broad: f32,
|
||||||
agc_csd: f32,
|
agc_csd: f32,
|
||||||
pop: [f32; 3], // low/mid/high onset envelopes
|
pop: [f32; 3], // low/mid/high onset envelopes
|
||||||
broad_pop: f32, // broadband onset envelope
|
broad_pop: f32, // broadband onset envelope
|
||||||
csd_pop: f32, // complex-domain onset envelope
|
csd_pop: f32, // complex-domain onset envelope
|
||||||
spec_edges: [(usize, usize); SPEC_N],
|
spec_edges: [(usize, usize); SPEC_N],
|
||||||
// Predictive-IOI beat model (robust for breakcore's unstable tempo: it
|
// Predictive-IOI beat model (robust for breakcore's unstable tempo: it
|
||||||
// tracks the running inter-onset interval, not a brittle global BPM).
|
// 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];
|
let mut dct = vec![0.0f32; MFCC_N * MEL_N];
|
||||||
for k in 1..=MFCC_N {
|
for k in 1..=MFCC_N {
|
||||||
for j in 0..MEL_N {
|
for j in 0..MEL_N {
|
||||||
dct[(k - 1) * MEL_N + j] = (std::f32::consts::PI * k as f32
|
dct[(k - 1) * MEL_N + j] =
|
||||||
* (j as f32 + 0.5)
|
(std::f32::consts::PI * k as f32 * (j as f32 + 0.5) / MEL_N as f32).cos();
|
||||||
/ MEL_N as f32)
|
|
||||||
.cos();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -825,7 +832,11 @@ impl Analyzer {
|
|||||||
let nbin = (half - 1).max(1) as f32;
|
let nbin = (half - 1).max(1) as f32;
|
||||||
let gm = (log_sum / nbin).exp();
|
let gm = (log_sum / nbin).exp();
|
||||||
let am = lin_sum / nbin;
|
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
|
// MFCC: mel-filterbank energies (magnitude) -> log -> DCT-II. c0
|
||||||
// (overall energy) is dropped; c1.. = pitch-independent timbre.
|
// (overall energy) is dropped; c1.. = pitch-independent timbre.
|
||||||
@@ -874,7 +885,11 @@ impl Analyzer {
|
|||||||
for i in 0..SPEC_N {
|
for i in 0..SPEC_N {
|
||||||
spec[i] = norm(spec_raw[i], &mut self.agc_spec[i]);
|
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 centroid = norm(centroid_hz, &mut self.agc_centroid);
|
||||||
let loud = norm(loud_sum / half as f32, &mut self.agc_loud);
|
let loud = norm(loud_sum / half as f32, &mut self.agc_loud);
|
||||||
let broad = norm(broad_flux / half as f32, &mut self.agc_broad);
|
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) {
|
if (0.18..1.20).contains(&obs) {
|
||||||
let ratio = obs / self.beat_ioi.max(1e-3);
|
let ratio = obs / self.beat_ioi.max(1e-3);
|
||||||
// In-range: trust it. Way off (real tempo change): adopt slowly.
|
// 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_ioi = (self.beat_ioi + (obs - self.beat_ioi) * k).clamp(0.18, 1.0);
|
||||||
}
|
}
|
||||||
self.beat_clock = 0.0;
|
self.beat_clock = 0.0;
|
||||||
@@ -1028,8 +1047,8 @@ impl Analyzer {
|
|||||||
if best > ACF_SNAP && !(0.75..1.34).contains(&ratio) {
|
if best > ACF_SNAP && !(0.75..1.34).contains(&ratio) {
|
||||||
self.beat_ioi = ioi;
|
self.beat_ioi = ioi;
|
||||||
} else {
|
} else {
|
||||||
self.beat_ioi = (self.beat_ioi + (ioi - self.beat_ioi) * (best * 0.20))
|
self.beat_ioi =
|
||||||
.clamp(0.18, 1.0);
|
(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);
|
decoded.copy_to_vec_interleaved::<f32>(&mut ilv);
|
||||||
for frame in ilv.chunks(ch) {
|
for frame in ilv.chunks(ch) {
|
||||||
let mid = frame.iter().sum::<f32>() / ch as f32;
|
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;
|
samples += 1;
|
||||||
if let Some(b) = an.push(mid, side) {
|
if let Some(b) = an.push(mid, side) {
|
||||||
frames.push(b);
|
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::fingerprint::{self, Accum as FpAccum};
|
||||||
use audio_visualizer::viz::monolith::Monolith;
|
use audio_visualizer::viz::monolith::Monolith;
|
||||||
use audio_visualizer::viz::palette::Palette;
|
use audio_visualizer::viz::palette::Palette;
|
||||||
|
use audio_visualizer::viz::underground::Underground;
|
||||||
use clap::{Parser, ValueEnum};
|
use clap::{Parser, ValueEnum};
|
||||||
use nannou::app::LoopMode;
|
use nannou::app::LoopMode;
|
||||||
use nannou::prelude::*;
|
use nannou::prelude::*;
|
||||||
@@ -113,6 +114,7 @@ impl Preset {
|
|||||||
enum VisMode {
|
enum VisMode {
|
||||||
Breakcore,
|
Breakcore,
|
||||||
Monolith,
|
Monolith,
|
||||||
|
Underground,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VisMode {
|
impl VisMode {
|
||||||
@@ -120,12 +122,14 @@ impl VisMode {
|
|||||||
match self {
|
match self {
|
||||||
VisMode::Breakcore => "breakcore",
|
VisMode::Breakcore => "breakcore",
|
||||||
VisMode::Monolith => "monolith",
|
VisMode::Monolith => "monolith",
|
||||||
|
VisMode::Underground => "underground",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn parse(s: &str) -> Option<Self> {
|
fn parse(s: &str) -> Option<Self> {
|
||||||
Some(match s {
|
Some(match s {
|
||||||
"breakcore" => VisMode::Breakcore,
|
"breakcore" => VisMode::Breakcore,
|
||||||
"monolith" => VisMode::Monolith,
|
"monolith" => VisMode::Monolith,
|
||||||
|
"underground" => VisMode::Underground,
|
||||||
_ => return None,
|
_ => return None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -133,7 +137,8 @@ impl VisMode {
|
|||||||
fn next(self) -> Self {
|
fn next(self) -> Self {
|
||||||
match self {
|
match self {
|
||||||
VisMode::Breakcore => VisMode::Monolith,
|
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 {
|
if scaled {
|
||||||
cmd.args([
|
cmd.args([
|
||||||
"-vf",
|
"-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([
|
cmd.args([
|
||||||
@@ -821,6 +830,7 @@ fn build_visualizer(
|
|||||||
match mode {
|
match mode {
|
||||||
VisMode::Breakcore => Box::new(Breakcore::new(seed, device, rw, rh)),
|
VisMode::Breakcore => Box::new(Breakcore::new(seed, device, rw, rh)),
|
||||||
VisMode::Monolith => Box::new(Monolith::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
|
/// the deterministic file-derived seed so the same file looks different per
|
||||||
/// invocation but the chosen *form* still tracks the song fingerprint.
|
/// invocation but the chosen *form* still tracks the song fingerprint.
|
||||||
fn wallclock_seed(app: &App) -> u64 {
|
fn wallclock_seed(app: &App) -> u64 {
|
||||||
(app.duration.since_start.as_nanos() as u64)
|
(app.duration.since_start.as_nanos() as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15)
|
||||||
.wrapping_mul(0x9E37_79B9_7F4A_7C15)
|
|
||||||
^ 0xD1B5_4A32_D192_ED03
|
^ 0xD1B5_4A32_D192_ED03
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -945,18 +954,19 @@ fn update(app: &App, m: &mut Model, upd: Update) {
|
|||||||
m.visual.install_fingerprint(fp);
|
m.visual.install_fingerprint(fp);
|
||||||
println!(
|
println!(
|
||||||
"fingerprint: centroid={:.2} chroma={} tonal={:.2} dyn={:.2} bpm_cls={:.2}",
|
"fingerprint: centroid={:.2} chroma={} tonal={:.2} dyn={:.2} bpm_cls={:.2}",
|
||||||
fp.centroid_mean,
|
fp.centroid_mean, fp.chroma_dom, fp.tonality, fp.dyn_range, fp.tempo_class,
|
||||||
fp.chroma_dom,
|
|
||||||
fp.tonality,
|
|
||||||
fp.dyn_range,
|
|
||||||
fp.tempo_class,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Audio-driven motion. The visual's own update grows/morphs/restructures.
|
// Audio-driven motion. The visual's own update grows/morphs/restructures.
|
||||||
m.visual.update(&b, dt);
|
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 pal = Palette::from_audio(&b);
|
||||||
let scale = 1.0 + (b.low * m.g.low).min(0.9) + b.low_on * 0.4;
|
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 {
|
if m.hud {
|
||||||
let g = m.g;
|
let g = m.g;
|
||||||
let extra = match &m.mode {
|
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()),
|
Mode::Live(_) => format!("fps {:.0}", app.fps()),
|
||||||
};
|
};
|
||||||
let fp_state = if m.visual.fingerprint_ready() {
|
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).
|
/// Audio still rides on top of these via the springs (premise §3).
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
struct Regime {
|
struct Regime {
|
||||||
scale: f32, // backbone normalize-scale target
|
scale: f32, // backbone normalize-scale target
|
||||||
melt: f32, // smooth-min fuse weight (0..1, scales melt_k)
|
melt: f32, // smooth-min fuse weight (0..1, scales melt_k)
|
||||||
glow: f32, // filament brightness base
|
glow: f32, // filament brightness base
|
||||||
speed: f32, // rotation + attractor integration multiplier
|
speed: f32, // rotation + attractor integration multiplier
|
||||||
tube: f32, // base capsule radius
|
tube: f32, // base capsule radius
|
||||||
rib: f32, // spectral-shell presence 0..1
|
rib: f32, // spectral-shell presence 0..1
|
||||||
deb: f32, // debris presence 0..1
|
deb: f32, // debris presence 0..1
|
||||||
heat: f32, // colour-warmth / energy push base 0..1
|
heat: f32, // colour-warmth / energy push base 0..1
|
||||||
warpd: f32, // spine spectral-displacement amount
|
warpd: f32, // spine spectral-displacement amount
|
||||||
ca: f32, // chromatic-aberration multiplier
|
ca: f32, // chromatic-aberration multiplier
|
||||||
fade: f32, // feedback-fade multiplier (smaller ⇒ longer trail)
|
fade: f32, // feedback-fade multiplier (smaller ⇒ longer trail)
|
||||||
prefer_knot: bool, // bias the next restructure's backbone kind
|
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.
|
// §3 springs — regime sets the base, audio + tension ride on top.
|
||||||
// Build-up contracts the backbone toward a dense core; release kicks
|
// Build-up contracts the backbone toward a dense core; release kicks
|
||||||
// it back out (spring overshoot makes the drop *punch*).
|
// 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
|
let scale_t = rg.scale * (1.0 - 0.42 * tn) + 0.30 * b.low + 0.25 * b.low_on + 0.85 * rel;
|
||||||
+ 0.85 * rel;
|
|
||||||
self.sp_scale.step(scale_t, 14.0, dt);
|
self.sp_scale.step(scale_t, 14.0, dt);
|
||||||
self.sp_tube
|
self.sp_tube
|
||||||
.step(rg.tube + 0.05 * b.mid + 0.02 * b.mid_on, 11.0, dt);
|
.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_dist.step(3.2 - 0.85 * b.low, 6.0, dt);
|
||||||
self.sp_glow.step(
|
self.sp_glow
|
||||||
rg.glow + 0.45 * b.loud + 0.4 * b.flux + 0.35 * tn,
|
.step(rg.glow + 0.45 * b.loud + 0.4 * b.flux + 0.35 * tn, 9.0, dt);
|
||||||
9.0,
|
|
||||||
dt,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Music-locked rotation. No constant baseline — true silence leaves
|
// Music-locked rotation. No constant baseline — true silence leaves
|
||||||
// the field still; every term is audio · regime · tension. A snare/hat
|
// 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).
|
// onset adds a quick rotational *jolt* (decays, so it reads as a kick).
|
||||||
let sp = rg.speed * (1.0 + 0.7 * tn);
|
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.yaw += (0.7 * b.mid + 0.9 * self.jolt) * sp * dt;
|
||||||
self.pitch += 0.4 * b.low * sp * dt;
|
self.pitch += 0.4 * b.low * sp * dt;
|
||||||
self.roll += (0.35 * b.high + 1.4 * self.jolt) * 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.
|
// widens through a build then snaps back on the drop.
|
||||||
self.sp_dolly
|
self.sp_dolly
|
||||||
.step(0.55 * b.low_on + 0.45 * b.beat, 20.0, dt);
|
.step(0.55 * b.low_on + 0.45 * b.beat, 20.0, dt);
|
||||||
self.sp_focal
|
self.sp_focal.step(
|
||||||
.step((1.70 + 0.55 * tn - 0.15 * b.loud).clamp(1.25, 2.40), 6.0, dt);
|
(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
|
// Broadband-onset radial shock: the whole sigil pulses out per big
|
||||||
// hit, distinct from the section restructure. Decays in ~0.4 s.
|
// 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 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 disp = warpd * (0.05 + 0.55 * band + 0.4 * self.b.high_on) + 0.05 * kw;
|
||||||
let p = (base[i] + nrm * disp) * scale;
|
let p = (base[i] + nrm * disp) * scale;
|
||||||
let r = (self.sp_tube.x * (0.4 + 0.7 * band) * (1.0 + 0.8 * kw))
|
let r = (self.sp_tube.x * (0.4 + 0.7 * band) * (1.0 + 0.8 * kw)).clamp(0.0015, 0.0075);
|
||||||
.clamp(0.0015, 0.0075);
|
|
||||||
out[i] = [p.x, p.y, p.z, r];
|
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;
|
let ang = pc as f32 / CHROMA_N as f32 * TAU;
|
||||||
// A separate tilted plane from the ribs so spokes read distinctly.
|
// A separate tilted plane from the ribs so spokes read distinctly.
|
||||||
let dir = vec3(ang.cos(), 0.35 * (ang * 2.0).sin(), ang.sin())
|
let dir = vec3(ang.cos(), 0.35 * (ang * 2.0).sin(), ang.sin()).normalize_or_zero();
|
||||||
.normalize_or_zero();
|
|
||||||
let inner = dir * (0.16 * scale);
|
let inner = dir * (0.16 * scale);
|
||||||
let outer = dir * ((0.16 + 0.62 * cv) * 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);
|
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,
|
0.13 + 0.06 * lo,
|
||||||
self.hue_b,
|
self.hue_b,
|
||||||
);
|
);
|
||||||
let sat = (0.17 + 0.12 * (lo * 0.5 + self.b.mid * 0.4))
|
let sat = (0.17 + 0.12 * (lo * 0.5 + self.b.mid * 0.4)) * (1.0 - 0.25 * self.b.flatness);
|
||||||
* (1.0 - 0.25 * self.b.flatness);
|
|
||||||
let acc = oklch((0.62 + 0.28 * lo).min(0.97), sat, self.hue_a);
|
let acc = oklch((0.62 + 0.28 * lo).min(0.97), sat, self.hue_a);
|
||||||
|
|
||||||
let mut u = [0.0f32; UBO_LEN];
|
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);
|
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.
|
// 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[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
|
// bounding-sphere radius: covers spine (0.92) + ribs/spokes/debris
|
||||||
// (≤~1.05·scale) + tube, and grows with the shock so a pulse never
|
// (≤~1.05·scale) + tube, and grows with the shock so a pulse never
|
||||||
// clips. Parked capsules sit far outside and contribute zero glow.
|
// clips. Parked capsules sit far outside and contribute zero glow.
|
||||||
|
|||||||
+20
-2
@@ -125,7 +125,16 @@ impl Visualizer for Breakcore {
|
|||||||
c: &RenderContext,
|
c: &RenderContext,
|
||||||
) -> Option<&wgpu::Texture> {
|
) -> Option<&wgpu::Texture> {
|
||||||
Some(Breakcore::render(
|
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,
|
c.march_cap,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
@@ -167,7 +176,16 @@ impl Visualizer for Monolith {
|
|||||||
c: &RenderContext,
|
c: &RenderContext,
|
||||||
) -> Option<&wgpu::Texture> {
|
) -> Option<&wgpu::Texture> {
|
||||||
Some(Monolith::render(
|
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,
|
c.march_cap,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-2
@@ -83,8 +83,7 @@ fn hash2(mut x: u32) -> f32 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn grad(ix: i32, iy: i32, seed: u32) -> Vec2 {
|
fn grad(ix: i32, iy: i32, seed: u32) -> Vec2 {
|
||||||
let h = (ix as u32)
|
let h = (ix as u32).wrapping_mul(0x9E37_79B1)
|
||||||
.wrapping_mul(0x9E37_79B1)
|
|
||||||
^ (iy as u32).wrapping_mul(0x85EB_CA77)
|
^ (iy as u32).wrapping_mul(0x85EB_CA77)
|
||||||
^ seed.wrapping_mul(0xC2B2_AE3D);
|
^ seed.wrapping_mul(0xC2B2_AE3D);
|
||||||
let a = hash2(h) * std::f32::consts::TAU;
|
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 * (rho - p.z) - p.y,
|
||||||
p.x * p.y - beta * p.z,
|
p.x * p.y - beta * p.z,
|
||||||
),
|
),
|
||||||
Attr::Rossler { a, b, c } => {
|
Attr::Rossler { a, b, c } => vec3(-p.y - p.z, p.x + a * p.y, b + p.z * (p.x - 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 t = tau * p[3].max(1.0) * u;
|
||||||
let (pn, qn) = (p[0], p[1]);
|
let (pn, qn) = (p[0], p[1]);
|
||||||
let r = 1.0 + p[2] * (qn * t).cos();
|
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 => {
|
1 => {
|
||||||
let sf = |ang: f32, m: f32, n1: f32, n2: f32, n3: f32| -> f32 {
|
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 post;
|
||||||
pub mod shader;
|
pub mod shader;
|
||||||
pub mod structure;
|
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::palette::{Palette, oklch};
|
||||||
use crate::viz::post::read_texture_rgba;
|
use crate::viz::post::read_texture_rgba;
|
||||||
use crate::viz::shader::ShaderPipeline;
|
use crate::viz::shader::ShaderPipeline;
|
||||||
|
use crate::viz::structure::{Arch, Structure};
|
||||||
use nannou::wgpu;
|
use nannou::wgpu;
|
||||||
|
|
||||||
/// UBO length in f32: **10** std140 rows (40). Header layout is duplicated
|
/// UBO length in f32: **10** std140 rows (40). Header layout is duplicated
|
||||||
@@ -98,6 +99,8 @@ pub struct Monolith {
|
|||||||
fp_committed: bool,
|
fp_committed: bool,
|
||||||
accent_class: Accent,
|
accent_class: Accent,
|
||||||
|
|
||||||
|
structure: Structure,
|
||||||
|
|
||||||
// springs
|
// springs
|
||||||
sp_scale: Spring, // sub-bass breath (bulb world scale)
|
sp_scale: Spring, // sub-bass breath (bulb world scale)
|
||||||
sp_glow: Spring,
|
sp_glow: Spring,
|
||||||
@@ -152,6 +155,7 @@ impl Monolith {
|
|||||||
fp: Fingerprint::default(),
|
fp: Fingerprint::default(),
|
||||||
fp_committed: false,
|
fp_committed: false,
|
||||||
accent_class: Accent::Cyan,
|
accent_class: Accent::Cyan,
|
||||||
|
structure: Structure::new(),
|
||||||
sp_scale: Spring { x: 1.0, v: 0.0 },
|
sp_scale: Spring { x: 1.0, v: 0.0 },
|
||||||
sp_glow: Spring { x: 0.55, v: 0.0 },
|
sp_glow: Spring { x: 0.55, v: 0.0 },
|
||||||
sp_power: Spring { x: 8.0, 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.frame = self.frame.wrapping_add(1);
|
||||||
self.b = *b;
|
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).
|
// 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);
|
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;
|
let flux_q = b.flux * b.flux;
|
||||||
|
|
||||||
// --- sub-bass breath: sp_scale grows on low²; the kick still reads,
|
// --- 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.
|
// a mid-range hum doesn't.
|
||||||
let scale_t = 1.0 + 0.20 * low_q + 0.12 * b.low_on;
|
let scale_t = 1.0 + 0.40 * low_q + 0.22 * b.low_on;
|
||||||
self.sp_scale.step(scale_t, 4.0 * dyn_m, dt);
|
self.sp_scale.step(scale_t, 5.0 * dyn_m, dt);
|
||||||
let dist_t = 2.8 + 0.25 * low_q + 0.10 * b.low_on;
|
|
||||||
|
// --- 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.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
|
// --- bulb power: small drift around 8 — mid² nudges it gently and
|
||||||
// the fingerprint's tonality biases the resting point (tonal music
|
// the fingerprint's tonality biases the resting point.
|
||||||
// keeps a cleaner low-power bulb, noisy/atonal goes higher).
|
|
||||||
let power_t = 7.8 + 0.5 * self.fp.tonality + 0.2 * (mid_q - 0.25);
|
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);
|
self.sp_power.step(power_t.clamp(6.5, 9.5), 2.0, dt);
|
||||||
|
|
||||||
@@ -250,31 +260,32 @@ impl Monolith {
|
|||||||
dt,
|
dt,
|
||||||
);
|
);
|
||||||
|
|
||||||
// --- glitch envelope: smoothed hi-band onset / flux with a deadband
|
// --- 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.
|
|
||||||
let raw = 0.7 * b.high_on * b.high_on + 0.35 * flux_q;
|
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 glitch_target = (raw - 0.10).clamp(0.0, 1.2);
|
||||||
let a_g = if glitch_target > self.glitch_env {
|
let g_tau = if glitch_target > self.glitch_env {
|
||||||
0.18
|
0.08
|
||||||
} else {
|
} else {
|
||||||
0.04
|
0.40
|
||||||
};
|
};
|
||||||
|
let a_g = 1.0 - (-dt / g_tau).exp();
|
||||||
self.glitch_env += (glitch_target - self.glitch_env) * a_g;
|
self.glitch_env += (glitch_target - self.glitch_env) * a_g;
|
||||||
|
|
||||||
// --- stutter FSM (the "drops to 12 fps" simulation). Triggered only
|
// --- stutter FSM (the "drops to 12 fps" simulation). Triggered only
|
||||||
// by real snare-flux bursts — both bands strong, or one very strong.
|
// by real snare-flux bursts.
|
||||||
// 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.
|
|
||||||
self.stutter_gate = (self.stutter_gate - dt).max(0.0);
|
self.stutter_gate = (self.stutter_gate - dt).max(0.0);
|
||||||
self.stutter_w = (self.stutter_w - dt / 0.08).max(0.0);
|
self.stutter_w = (self.stutter_w - dt / 0.08).max(0.0);
|
||||||
let snare_burst =
|
let snare_burst = (b.high_on > 0.75 && b.flux > 0.70) || b.flux > 0.94 || b.high_on > 0.88;
|
||||||
(b.high_on > 0.72 && b.flux > 0.65) || b.flux > 0.90 || b.high_on > 0.85;
|
|
||||||
|
// 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 {
|
if snare_burst && self.stutter_gate <= 0.0 {
|
||||||
self.stutter_w = 1.0;
|
self.stutter_w = 1.0;
|
||||||
self.stutter_gate = 0.45;
|
self.stutter_gate = stutter_cooldown;
|
||||||
self.held_yaw = self.yaw;
|
self.held_yaw = self.yaw;
|
||||||
self.held_pitch = self.pitch;
|
self.held_pitch = self.pitch;
|
||||||
self.held_roll = self.roll;
|
self.held_roll = self.roll;
|
||||||
@@ -283,24 +294,24 @@ impl Monolith {
|
|||||||
self.held_glow = self.sp_glow.x;
|
self.held_glow = self.sp_glow.x;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- camera (BPM-paced + small audio jitter). No constant baseline
|
// --- camera: Section-aware rotation rate.
|
||||||
// beyond tempo — true silence keeps it nearly still. All terms
|
let energy_mult = match arch {
|
||||||
// gated quadratically so a quiet passage holds a steady frame.
|
Arch::Drop => 2.4,
|
||||||
let base_rate = 0.05 + 0.10 * self.fp.tempo_class;
|
Arch::Build => 1.4 + self.structure.tension(),
|
||||||
let rate = base_rate * dyn_m;
|
Arch::Ambient | Arch::Breakdown => 0.45,
|
||||||
self.yaw += rate * dt + 0.10 * b.beat * b.beat * dt;
|
_ => 1.0,
|
||||||
self.pitch += (0.06 * low_q - 0.02) * rate * dt;
|
};
|
||||||
self.roll += 0.02 * high_q * dt;
|
|
||||||
|
|
||||||
// --- swirl: mid² accumulates the feedback rotation. Sustained pad
|
let base_rate = 0.05 + 0.10 * self.fp.tempo_class;
|
||||||
// = a slow drift; sparse mids = nearly stationary trail.
|
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;
|
self.swirl_phase += (0.15 + 0.6 * mid_q) * 0.30 * dt;
|
||||||
|
|
||||||
// --- colour inertia. Base hue drifts blue↔purple with centroid; the
|
// --- colour inertia.
|
||||||
// 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.
|
|
||||||
let base_t = (5.0 - b.centroid * 1.4).rem_euclid(TAU);
|
let base_t = (5.0 - b.centroid * 1.4).rem_euclid(TAU);
|
||||||
let acc_t = self.accent_class.hue()
|
let acc_t = self.accent_class.hue()
|
||||||
+ (self.fp.chroma_dom as f32) / 12.0 * 0.3
|
+ (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);
|
self.hue_a2 = angle_to(self.hue_a2, acc2_t, ha);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Render this frame into the target and return it. Bin tunables match
|
/// Render this frame into the target and return it.
|
||||||
/// 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.
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn render(
|
pub fn render(
|
||||||
&mut self,
|
&mut self,
|
||||||
@@ -332,10 +340,9 @@ impl Monolith {
|
|||||||
) -> &wgpu::Texture {
|
) -> &wgpu::Texture {
|
||||||
let dr = drive.clamp(0.0, 3.0);
|
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 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
|
// Held-vs-live blending
|
||||||
// values so the picture freezes; the shader also pins the feedback
|
|
||||||
// floor near 1.0 from `stutter_w` so the trail survives unchanged.
|
|
||||||
let s = self.stutter_w.clamp(0.0, 1.0);
|
let s = self.stutter_w.clamp(0.0, 1.0);
|
||||||
let yaw = self.yaw * (1.0 - s) + self.held_yaw * s;
|
let yaw = self.yaw * (1.0 - s) + self.held_yaw * s;
|
||||||
let pitch = self.pitch * (1.0 - s) + self.held_pitch * 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 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;
|
let glow = self.sp_glow.x * (1.0 - s) + self.held_glow * s;
|
||||||
|
|
||||||
// Palette — base lands deep dark silver/blue; two accents on
|
// Palette
|
||||||
// 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.
|
|
||||||
let lo = self.b.loud;
|
let lo = self.b.loud;
|
||||||
let base = oklch(
|
let base = oklch(
|
||||||
(0.14 + 0.14 * lo * lo).min(0.42),
|
(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_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);
|
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(
|
let acc2 = oklch(
|
||||||
(0.66 + 0.20 * lo).min(0.90),
|
(0.66 + 0.20 * lo).min(0.90),
|
||||||
(acc_sat * 0.85).min(0.30),
|
(acc_sat * 0.85).min(0.30),
|
||||||
@@ -366,29 +368,22 @@ impl Monolith {
|
|||||||
|
|
||||||
let mut u = [0.0f32; UBO_LEN];
|
let mut u = [0.0f32; UBO_LEN];
|
||||||
|
|
||||||
// row0 cam — held during stutter (see above)
|
// row0 cam
|
||||||
u[0] = yaw;
|
u[0] = yaw;
|
||||||
u[1] = pitch;
|
u[1] = pitch;
|
||||||
u[2] = roll;
|
u[2] = roll;
|
||||||
u[3] = dist.clamp(1.8, 4.5);
|
u[3] = dist.clamp(1.8, 4.5);
|
||||||
|
|
||||||
// row1 p0 = scale, glow_gain, ca_px, edge_softness
|
// 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, 2.2);
|
||||||
u[4] = (scale * scl).clamp(0.4, 1.8);
|
|
||||||
u[5] = glow.clamp(0.18, 0.85);
|
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);
|
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
|
// row2 col0 = base.rgb, fade
|
||||||
u[8] = base[0];
|
u[8] = base[0];
|
||||||
u[9] = base[1];
|
u[9] = base[1];
|
||||||
u[10] = base[2];
|
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);
|
u[11] = (fade * (1.0 - 0.25 * (1.0 - dyn_m))).clamp(0.02, 1.0);
|
||||||
|
|
||||||
// row3 col1 = accent.rgb, flash
|
// row3 col1 = accent.rgb, flash
|
||||||
@@ -403,17 +398,23 @@ impl Monolith {
|
|||||||
u[18] = (self.frame & 0xffff) as f32;
|
u[18] = (self.frame & 0xffff) as f32;
|
||||||
u[19] = self.t;
|
u[19] = self.t;
|
||||||
|
|
||||||
// row5 p2 = march_steps, power_n, feedback_on, world_r (bounding)
|
// row5 p2 = march_steps, power_n, feedback_on, world_r
|
||||||
// Mandelbulb takes ~8 iters/step — heavier than the capsule field, so
|
|
||||||
// hold the request slightly lower than breakcore's.
|
|
||||||
u[20] = (20.0 + 7.0 * dr).clamp(16.0, march_cap.min(96) as f32);
|
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[21] = self.sp_power.x.clamp(6.5, 9.5);
|
||||||
u[22] = if feedback && self.gpu.primed() { 1.0 } else { 0.0 };
|
u[22] = if feedback && self.gpu.primed() {
|
||||||
// Bulb fits in r ≈ 1.25; pad for breath + sub-bass extension.
|
1.0
|
||||||
u[23] = (1.30 * scl + 0.20).clamp(1.0, 3.0);
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
u[23] = (1.30 * scl + 0.20).clamp(1.0, 3.5);
|
||||||
|
|
||||||
// row6 p3 = grain, glitch_a, fog, beat
|
// 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[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[26] = (0.30 + 0.35 * self.b.loud).clamp(0.20, 0.75);
|
||||||
u[27] = self.b.beat;
|
u[27] = self.b.beat;
|
||||||
@@ -430,11 +431,11 @@ impl Monolith {
|
|||||||
u[34] = self.rw as f32 / self.rh.max(1) as f32;
|
u[34] = self.rw as f32 / self.rh.max(1) as f32;
|
||||||
u[35] = self.fp.tonality.clamp(0.0, 1.0);
|
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[36] = acc2[0];
|
||||||
u[37] = acc2[1];
|
u[37] = acc2[1];
|
||||||
u[38] = acc2[2];
|
u[38] = acc2[2];
|
||||||
u[39] = 0.0;
|
u[39] = self.structure.release();
|
||||||
|
|
||||||
self.gpu.render(device, queue, &u)
|
self.gpu.render(device, queue, &u)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ struct U {
|
|||||||
p3: vec4<f32>, // grain, glitch_a, fog, beat
|
p3: vec4<f32>, // grain, glitch_a, fog, beat
|
||||||
p4: vec4<f32>, // loud, low, mid, high
|
p4: vec4<f32>, // loud, low, mid, high
|
||||||
p5: vec4<f32>, // stutter_w, swirl, aspect, tonality
|
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;
|
@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 swirl = u.p5.y; // accumulated swirl angle (rad)
|
||||||
let aspect = u.p5.z;
|
let aspect = u.p5.z;
|
||||||
let tonal = u.p5.w;
|
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
|
// 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
|
// 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
|
// 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 uv = in.uv;
|
||||||
let ndc = vec2<f32>(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0);
|
let ndc = vec2<f32>(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0);
|
||||||
let dist = u.cam.w;
|
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 ro = vec3<f32>(0.0, 0.0, -dist);
|
||||||
let rd = normalize(vec3<f32>(ndc.x * aspect, ndc.y, focal));
|
let rd = normalize(vec3<f32>(ndc.x * aspect, ndc.y, focal));
|
||||||
|
|
||||||
// Ray vs bounding sphere — the background-pixel early-out that keeps the
|
// Ray vs bounding sphere
|
||||||
// raymarch from melting the GPU. `rb` is set to (scale + breath + pad).
|
|
||||||
let b = dot(ro, rd);
|
let b = dot(ro, rd);
|
||||||
let c = dot(ro, ro) - rb * rb;
|
let c = dot(ro, ro) - rb * rb;
|
||||||
let disc = b * b - c;
|
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.
|
// zero, only strong snares light the edge.
|
||||||
body = body + accent2 * highf * highf * fres * fres * 0.40;
|
body = body + accent2 * highf * highf * fres * fres * 0.40;
|
||||||
}
|
}
|
||||||
// Particle-dither: per-pixel hash threshold against intensity.
|
// Particle-dither: less hectic in quiet parts
|
||||||
// Each pixel is a "particle" that either shows or doesn't —
|
let dither_thresh = clamp(inten * 2.2 - 0.08, 0.0, 1.0);
|
||||||
// identifies the form as point-cloud rather than solid surface.
|
|
||||||
let pcell = hash21(uv * vec2<f32>(res_w, res_h) * 0.85
|
let pcell = hash21(uv * vec2<f32>(res_w, res_h) * 0.85
|
||||||
+ vec2<f32>(frame * 0.013, frame * 0.029));
|
+ 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);
|
col = body * inten * (0.50 + 0.55 * pkeep);
|
||||||
// Core punch — uses the per-pixel tint so the bulb's deep core
|
// Core punch — uses the per-pixel tint so the bulb's deep core
|
||||||
// glows different shades in different regions, not one hot dot.
|
// 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 —
|
/// 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.
|
/// but `U` MUST stay padding-free and match the shader's UBO layout.
|
||||||
fn as_bytes<U: Copy>(u: &U) -> &[u8] {
|
fn as_bytes<U: Copy>(u: &U) -> &[u8] {
|
||||||
unsafe {
|
unsafe { std::slice::from_raw_parts((u as *const U).cast::<u8>(), std::mem::size_of::<U>()) }
|
||||||
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
|
/// 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_bass = 1.0 - (-dt / 0.8).exp();
|
||||||
let a_long = 1.0 - (-dt / 8.0).exp();
|
let a_long = 1.0 - (-dt / 8.0).exp();
|
||||||
|
|
||||||
let busy_in =
|
let busy_in = (b.flux + 0.5 * (b.low_on + b.mid_on + b.high_on)).min(1.5);
|
||||||
(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.e += (b.loud - self.e) * a_fast;
|
||||||
self.br += (b.centroid - self.br) * a_fast;
|
self.br += (b.centroid - self.br) * a_fast;
|
||||||
self.ba += (b.low - self.ba) * a_bass;
|
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