Files
skate-3-rust-engine/crates/skate-data/examples/audio_surfaces.rs
T
Hails e7af7b77e0 Game audio: ambience, water, rolling, trick cues, footsteps, settings
New crates/skate-game/src/game_audio/ module (Bevy audio):
- Loudness rules: master 25 % by default (settings/audio.json, Master/
  Ambience/Effects rows in the GRAPHICS menu), --mute; a clip may be
  raised only to its own full-scale peak (x4 max); fades, voice and
  repeat limits; silent while the menu is open or a replay runs. Mod
  audio follows the master volume and shares the one listener.
- Per-map ambience beds; water emitters on the map's water bodies
  (fountains: moving water, canals/lakes: lapping).
- Rolling: grain speed bands by board speed and the wheels' audio
  surface (new WheelLineState::audio_surfaces in skate-core, additive),
  restarted cleanly after leaving the ground.
- Cues found from physics/animation state each tick: pop, flip/grab
  (new trick only), landing (+ weight layer by impact), board-down,
  grind (metal vs other surfaces), powerslide and foot drag (shuffled),
  push, bail tiers, splash (retail patch record 869, played with its
  layers), footsteps from foot strikes. Sample picks confirmed by ear;
  each matches a retail SPLC patch record.
- AUDIO_CUE / AUDIO_LOOP / AUDIO_EVENT log lines; SKATE_AUDIO_TRACE=1.
- Read-only helpers: physics::foot_clearance, an animation-event latch
  in AnimationInput (physics never reads it).
- skate-data example audio_surfaces (per-map audio IDs, AT= point query).
2026-10-01 17:37:56 -06:00

179 lines
8.1 KiB
Rust

//! Lists the audio material IDs (`surface & 0x7f`) used by retail collision in
//! SKATE maps, to label them for rolling/grind sounds (game_audio). For each ID:
//! triangle count, upward-facing area, the physics types (`(surface >> 7) & 31`)
//! it appears with, and the albedo textures of the rendered geometry found at
//! sample points on it.
//! Usage: cargo run -p skate-data --example audio_surfaces -- <map.skate>...
use std::collections::{BTreeMap, BTreeSet, HashMap};
const SAMPLES: usize = 12;
const CELL: f32 = 4.0;
#[derive(Default)]
struct Audio {
triangles: usize,
up_area: f32,
physics: BTreeSet<u16>,
samples: Vec<[f32; 3]>,
}
fn sub(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
fn cross(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
[a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]
}
/// Render triangles bucketed on the XZ plane.
struct Grid {
cells: HashMap<(i32, i32), Vec<u32>>,
}
impl Grid {
fn new(map: &skate_data::skate_map::SkateMap) -> Self {
let mut cells = HashMap::<(i32, i32), Vec<u32>>::new();
for (index, tri) in map.geometry.indices.chunks_exact(3).enumerate() {
let p: [[f32; 3]; 3] = std::array::from_fn(|k| map.geometry.vertices[tri[k] as usize].position);
let (x0, x1) = (p.iter().map(|v| v[0]).fold(f32::MAX, f32::min), p.iter().map(|v| v[0]).fold(f32::MIN, f32::max));
let (z0, z1) = (p.iter().map(|v| v[2]).fold(f32::MAX, f32::min), p.iter().map(|v| v[2]).fold(f32::MIN, f32::max));
if x1 - x0 > 200. || z1 - z0 > 200. {
continue; // sky/backdrop sheets
}
for cx in (x0 / CELL).floor() as i32..=(x1 / CELL).floor() as i32 {
for cz in (z0 / CELL).floor() as i32..=(z1 / CELL).floor() as i32 {
cells.entry((cx, cz)).or_default().push(index as u32);
}
}
}
Self { cells }
}
/// Material of the render triangle under/at `point` (within 0.4 m vertically).
fn material_at(&self, map: &skate_data::skate_map::SkateMap, point: [f32; 3]) -> Option<u32> {
let key = ((point[0] / CELL).floor() as i32, (point[2] / CELL).floor() as i32);
let mut best: Option<(f32, u32)> = None;
for &index in self.cells.get(&key)? {
let tri = &map.geometry.indices[index as usize * 3..index as usize * 3 + 3];
let v = [0, 1, 2].map(|k| &map.geometry.vertices[tri[k] as usize]);
let [a, b, c] = v.map(|v| v.position);
let (x, z) = (point[0], point[2]);
let det = (b[2] - c[2]) * (a[0] - c[0]) + (c[0] - b[0]) * (a[2] - c[2]);
if det.abs() < 1e-6 {
continue;
}
let l1 = ((b[2] - c[2]) * (x - c[0]) + (c[0] - b[0]) * (z - c[2])) / det;
let l2 = ((c[2] - a[2]) * (x - c[0]) + (a[0] - c[0]) * (z - c[2])) / det;
let l3 = 1. - l1 - l2;
if l1 < 0. || l2 < 0. || l3 < 0. {
continue;
}
let y = l1 * a[1] + l2 * b[1] + l3 * c[1];
let dy = (y - point[1]).abs();
if dy < 0.4 && best.is_none_or(|(d, _)| dy < d) {
best = Some((dy, v[0].material));
}
}
best.map(|(_, m)| m)
}
}
fn texture_name(map: &skate_data::skate_map::SkateMap, material: u32) -> String {
let Some(m) = map.materials.get(material as usize - 1) else { return "?".into() };
let albedo = m.textures[0];
if albedo == 0 {
return format!("({})", m.name);
}
map.textures.get(albedo as usize - 1).map_or_else(|| m.name.clone(), |t| t.name.clone())
}
fn main() {
let mut failed = false;
for path in std::env::args_os().skip(1) {
let map = match skate_data::skate_map::SkateMap::load(std::path::Path::new(&path)) {
Ok(map) => map,
Err(e) => {
eprintln!("{}: {e}", path.to_string_lossy());
failed = true;
continue;
}
};
let Some(archive) = map.extensions.iter().find(|e| e.tag == *b"RWCM") else {
println!("{}: no RWCM collision", map.name);
continue;
};
// AT=x,y,z[,r]: collision surfaces within r (default 3) m of a point.
if let Ok(at) = std::env::var("AT") {
let v: Vec<f32> = at.split(',').filter_map(|s| s.trim().parse().ok()).collect();
if let [x, y, z, ref rest @ ..] = v[..] {
let r = rest.first().copied().unwrap_or(3.0);
let mut near = BTreeMap::<(u16, u16), (usize, f32, [usize; 3])>::new();
let _ = skate_data::retail_collision::visit_clusters(&archive.payload, |_, cluster| {
for t in cluster {
let c: [f32; 3] = std::array::from_fn(|a| t.points.iter().map(|p| p[a]).sum::<f32>() / 3.);
let d = ((c[0] - x).powi(2) + (c[1] - y).powi(2) + (c[2] - z).powi(2)).sqrt();
if d < r {
let e = near.entry((t.surface & 0x7F, (t.surface >> 7) & 31)).or_insert((0, f32::MAX, [0usize; 3]));
e.0 += 1;
e.1 = e.1.min(d);
// Slope: 0 flat (< 20°), 1 ramp (20-70°), 2 wall (> 70°).
let n = cross(sub(t.points[1], t.points[0]), sub(t.points[2], t.points[0]));
let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt().max(1e-9);
let angle = (n[1].abs() / len).clamp(0.0, 1.0).acos().to_degrees();
e.2[if angle < 20.0 { 0 } else if angle < 70.0 { 1 } else { 2 }] += 1;
}
}
Ok(())
});
println!("{}: surfaces within {r} m of ({x}, {y}, {z})", map.name);
for ((audio, physics), (n, d, slopes)) in near {
println!(" audio {audio:3} physics {physics:2}: {n:4} tris, nearest {d:.2} m, flat/ramp/wall {slopes:?}");
}
}
continue;
}
let mut ids = BTreeMap::<u16, Audio>::new();
let result = skate_data::retail_collision::visit_clusters(&archive.payload, |_, cluster| {
for t in cluster {
let entry = ids.entry(t.surface & 0x7F).or_default();
entry.triangles += 1;
entry.physics.insert((t.surface >> 7) & 31);
let n = cross(sub(t.points[1], t.points[0]), sub(t.points[2], t.points[0]));
let area = 0.5 * (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
if area > 0. && n[1].abs() / (2. * area) > 0.7 {
entry.up_area += area;
// Keep a spread of samples: every k-th upward triangle.
if entry.samples.len() < SAMPLES * 8 {
entry.samples.push(std::array::from_fn(|a| t.points.iter().map(|p| p[a]).sum::<f32>() / 3.));
}
}
}
Ok(())
});
if let Err(e) = result {
eprintln!("{}: {e}", map.name);
failed = true;
continue;
}
let grid = Grid::new(&map);
println!("== {} ({} audio IDs)", map.name, ids.len());
for (id, audio) in &ids {
let mut textures = BTreeMap::<String, usize>::new();
let step = (audio.samples.len() / SAMPLES).max(1);
for point in audio.samples.iter().step_by(step) {
if let Some(material) = grid.material_at(&map, *point) {
*textures.entry(texture_name(&map, material)).or_default() += 1;
}
}
let mut top: Vec<_> = textures.into_iter().collect();
top.sort_by(|a, b| b.1.cmp(&a.1));
let top: Vec<String> = top.iter().take(4).map(|(name, n)| format!("{name} x{n}")).collect();
println!(
" audio {id:3}: {:7} tris, {:9.0} m2 up, physics {:?}, at: {}",
audio.triangles, audio.up_area, audio.physics, top.join(", ")
);
}
}
if failed {
std::process::exit(1);
}
}