From 05572c7bf446572264d2cf97c531be6c3bfaaf30 Mon Sep 17 00:00:00 2001 From: Valentin Popov Date: Sun, 1 Nov 2026 06:12:44 +0400 Subject: feat(render): animate water with native EMBM waves --- crates/fparkan-render/src/lib.rs | 2 + crates/fparkan-render/src/water.rs | 108 +++++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 crates/fparkan-render/src/water.rs (limited to 'crates/fparkan-render') diff --git a/crates/fparkan-render/src/lib.rs b/crates/fparkan-render/src/lib.rs index 2ad4789..db0171f 100644 --- a/crates/fparkan-render/src/lib.rs +++ b/crates/fparkan-render/src/lib.rs @@ -1,6 +1,8 @@ #![forbid(unsafe_code)] //! Camera mathematics and graphics pipeline state. +pub mod water; + /// A 64-byte transform block returned by the original Terrain camera ABI. /// /// The original uses two selector-dependent transform pointers. Their exact diff --git a/crates/fparkan-render/src/water.rs b/crates/fparkan-render/src/water.rs new file mode 100644 index 0000000..0c7ad2a --- /dev/null +++ b/crates/fparkan-render/src/water.rs @@ -0,0 +1,108 @@ +//! The GOG Terrain EMBM wave map and its world-space animation clock. + +/// Side length of the native signed Du/Dv texture. +pub const EMBM_BUMP_SIZE: u32 = 128; +/// Native default wave tiling across the bounding rectangle of reflective faces. +pub const EMBM_BUMP_TILE: f32 = 100.0; +/// Native diagonal bump matrix, without luminance modulation. +pub const EMBM_COEFFICIENTS: [f32; 2] = [0.01, 0.01]; + +/// Generates the signed bytes written by Terrain.dll RVA 0x2BF50 with CW 0x007F. +#[must_use] +pub fn legacy_embm_bump_map() -> Vec<[i8; 2]> { + let mut texels = Vec::with_capacity((EMBM_BUMP_SIZE * EMBM_BUMP_SIZE) as usize); + for y in 0..EMBM_BUMP_SIZE { + for x in 0..EMBM_BUMP_SIZE { + // The native constant is slightly smaller than mathematical 4*pi. + let angle = ((y as f32 / EMBM_BUMP_SIZE as f32 - 0.5) + + (x as f32 / EMBM_BUMP_SIZE as f32 - 0.5)) + * f32::from_bits(0x4149_0fd0); + texels.push([ + (angle.cos() * 64.0).round_ties_even() as i8, + (angle.sin() * 64.0).round_ties_even() as i8, + ]); + } + } + texels +} + +/// Queue clock modulo the native ten-second wave scroll period. +#[must_use] +pub fn legacy_embm_phase(clock_ms: u32) -> f32 { + (clock_ms % 10_000) as f32 / 10_000.0 +} + +/// Native reflection coordinates: U reverses world Y and V reverses world X. +#[must_use] +pub fn legacy_embm_uv(world_xy: [f32; 2], bounds: [f32; 4]) -> Option<[f32; 2]> { + let span = [bounds[2] - bounds[0], bounds[3] - bounds[1]]; + if !world_xy + .into_iter() + .chain(bounds) + .chain(span) + .all(f32::is_finite) + || span.iter().any(|&value| value <= 0.0) + { + return None; + } + Some([ + 1.0 - (world_xy[1] - bounds[1]) / span[1], + 1.0 - (world_xy[0] - bounds[0]) / span[0], + ]) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn wave_bytes_match_native_generation_and_clock_wraps_without_float_time_loss() { + let texels = legacy_embm_bump_map(); + assert_eq!(texels.len(), 128 * 128); + assert_eq!( + &texels[..16], + &[ + [64, 0], + [64, 6], + [63, 12], + [61, 19], + [59, 24], + [56, 30], + [53, 36], + [49, 41], + [45, 45], + [41, 49], + [36, 53], + [30, 56], + [24, 59], + [19, 61], + [12, 63], + [6, 64], + ] + ); + assert_eq!(texels[16], [0, 64]); + assert_eq!(texels[32], [-64, 0]); + assert_eq!(texels[48], [0, -64]); + let native_hash = texels + .iter() + .flatten() + .fold(0xcbf29ce484222325u64, |hash, &byte| { + (hash ^ u64::from(byte as u8)).wrapping_mul(0x100000001b3) + }); + assert_eq!(native_hash, 0x0b92dc6a9e482625); + assert_eq!(legacy_embm_phase(0), 0.0); + assert_eq!(legacy_embm_phase(5_000), 0.5); + assert_eq!(legacy_embm_phase(10_000), 0.0); + assert_eq!(legacy_embm_phase(u32::MAX), 0.7295); + } + + #[test] + fn mapping_uses_the_reflective_rectangle_and_keeps_axis_order() { + let bounds = [100.0, 200.0, 300.0, 600.0]; + assert_eq!(legacy_embm_uv([100.0, 200.0], bounds), Some([1.0, 1.0])); + assert_eq!(legacy_embm_uv([300.0, 600.0], bounds), Some([0.0, 0.0])); + assert_eq!(legacy_embm_uv([150.0, 500.0], bounds), Some([0.25, 0.75])); + assert_eq!(legacy_embm_uv([0.0; 2], [1.0; 4]), None); + assert_eq!(legacy_embm_uv([f32::NAN, 0.0], bounds), None); + } +} -- cgit v1.2.3