Skip to content

Commit abe0699

Browse files
committed
feat: add Retro VU Meters visualizer with analog needle simulation and WGSL shader implementation
1 parent 0bd6975 commit abe0699

3 files changed

Lines changed: 690 additions & 1 deletion

File tree

src/engine.rs

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,8 @@ pub struct VulkanEngine<'a> {
281281
last_history_cam_z: f64,
282282
smooth_time: f64,
283283
smooth_dt: f64,
284+
vu_needle_angles: Vec<f32>,
285+
vu_needle_velocities: Vec<f32>,
284286
}
285287

286288
pub(crate) fn generate_lamp_mesh() -> (Vec<Vertex>, Vec<u32>) {
@@ -671,6 +673,7 @@ impl<'a> VulkanEngine<'a> {
671673
15 => include_str!("shaders/vis_3drain.wgsl"),
672674
16 => include_str!("shaders/vis_synthwaveracer.wgsl"),
673675
17 => include_str!("shaders/vis_cuboids.wgsl"),
676+
18 => include_str!("shaders/vis_vumeters.wgsl"),
674677
_ => include_str!("shaders/vis_spectrum.wgsl"),
675678
}
676679
};
@@ -1862,6 +1865,8 @@ impl<'a> VulkanEngine<'a> {
18621865
last_history_cam_z: 0.0f64,
18631866
smooth_time: 0.0f64,
18641867
smooth_dt: 1.0f64 / 60.0f64,
1868+
vu_needle_angles: vec![0.0; 32],
1869+
vu_needle_velocities: vec![0.0; 32],
18651870
}
18661871
}
18671872

@@ -1935,7 +1940,51 @@ impl<'a> VulkanEngine<'a> {
19351940

19361941
uniforms.spectrum.copy_from_slice(&state.spectrum_data);
19371942
uniforms.fire_heat.copy_from_slice(&state.fire_heat);
1938-
1943+
1944+
// Update needle physics for 3D retro VU meters
1945+
let ch_count = state.channel_vus.len().max(2);
1946+
if self.vu_needle_angles.len() < ch_count {
1947+
self.vu_needle_angles.resize(ch_count, 0.0);
1948+
self.vu_needle_velocities.resize(ch_count, 0.0);
1949+
}
1950+
1951+
let rise_time_secs = 0.30;
1952+
let damping_ratio = 0.85;
1953+
let omega_n = 4.8 / rise_time_secs;
1954+
let spring = omega_n * omega_n;
1955+
let damping = 2.0 * damping_ratio * omega_n;
1956+
1957+
let dt_clamped = dt.min(0.05);
1958+
for i in 0..ch_count {
1959+
let target = if i < state.raw_channel_vus.len() {
1960+
state.raw_channel_vus[i]
1961+
} else if !state.file_loaded {
1962+
// Sway gently if no file is loaded
1963+
let t = self.smooth_time as f32;
1964+
let offset = if i == 0 { 0.0 } else { 1.5 };
1965+
(0.12 * (t * 2.0 + offset).sin() + 0.15 * (t * 3.7 - offset).cos() + 0.2).clamp(0.0, 1.0)
1966+
} else {
1967+
0.0
1968+
};
1969+
let angle = self.vu_needle_angles[i];
1970+
let vel = self.vu_needle_velocities[i];
1971+
1972+
let acceleration = spring * (target - angle) - damping * vel;
1973+
let new_vel = vel + acceleration * dt_clamped;
1974+
let mut new_angle = angle + new_vel * dt_clamped;
1975+
1976+
if new_angle < -0.05 {
1977+
new_angle = -0.05;
1978+
self.vu_needle_velocities[i] = 0.0;
1979+
} else if new_angle > 1.1 {
1980+
new_angle = 1.1;
1981+
self.vu_needle_velocities[i] = -new_vel * 0.2; // pin bounce
1982+
} else {
1983+
self.vu_needle_velocities[i] = new_vel;
1984+
}
1985+
self.vu_needle_angles[i] = new_angle;
1986+
}
1987+
19391988
let ch_len = state.channel_vus.len().min(32);
19401989

19411990
// 1. Populate UI Display Channels (may be visually remapped)
@@ -1976,6 +2025,20 @@ impl<'a> VulkanEngine<'a> {
19762025
}
19772026
}
19782027

2028+
if state.visualizer_mode == 18 {
2029+
let actual_ch = state.channel_vus.len().max(2);
2030+
uniforms.num_channels = actual_ch as u32;
2031+
for i in 0..actual_ch {
2032+
if i < 32 {
2033+
uniforms.channels[i] = if i < self.vu_needle_angles.len() {
2034+
self.vu_needle_angles[i]
2035+
} else {
2036+
0.0
2037+
};
2038+
}
2039+
}
2040+
}
2041+
19792042
let vis_width = state.visual_width.max(128).min(2048) as u32;
19802043
uniforms.waveform_resolution = vis_width;
19812044
uniforms.waveform_history_size = state.waveform_history.len().min(144) as u32;

0 commit comments

Comments
 (0)