diff options
Diffstat (limited to 'crates/fparkan-render/src/lib.rs')
| -rw-r--r-- | crates/fparkan-render/src/lib.rs | 127 |
1 files changed, 122 insertions, 5 deletions
diff --git a/crates/fparkan-render/src/lib.rs b/crates/fparkan-render/src/lib.rs index 08a285d..ad96204 100644 --- a/crates/fparkan-render/src/lib.rs +++ b/crates/fparkan-render/src/lib.rs @@ -361,6 +361,64 @@ pub enum LegacyCullMode { FrontFace, } +/// CPU diffuse alpha for a colourless MSH batch carrying flag `0x0008`. +/// +/// `packet_distance` is the distance to the packet's first accepted vertex, +/// after its object transform. Flag `0x0010` takes precedence over `0x0040`. +/// Returns `None` for invalid distance or projection inputs. Callers must keep +/// the material alpha for batches with a type-16 colour stream or without `0x0008`. +#[must_use] +pub fn legacy_msh_portal_alpha( + batch_flags: u16, + packet_distance: f32, + horizontal_fov: f32, +) -> Option<f32> { + if !packet_distance.is_finite() + || packet_distance < 0.0 + || !horizontal_fov.is_finite() + || horizontal_fov <= 0.0 + { + return None; + } + let (distance, near, far, slope) = if batch_flags & 0x0010 != 0 { + ( + packet_distance.sqrt(), + 2.6 / horizontal_fov, + 7.799_999_7 / horizontal_fov, + (1.0 / (7.799_999_7 - 2.6)) * horizontal_fov, + ) + } else if batch_flags & 0x0040 != 0 { + return Some(0.0); + } else { + let near = 75.0 * 1.3; + let far = 95.0 * 1.3; + ( + packet_distance, + horizontal_fov.recip() * near, + horizontal_fov.recip() * far, + (1.0 / (far - near)) * horizontal_fov, + ) + }; + Some(if distance < near { + 0.0 + } else if distance > far { + 1.0 + } else { + ((distance - near) * slope).clamp(0.0, 1.0) + }) +} + +/// Raster depth adjustment applied to one pipeline variant. +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub enum LegacyDepthBias { + /// Preserve the interpolated depth exactly. + #[default] + Disabled, + /// Move projected decals toward the camera so they pass against their + /// coplanar receiver under a less-or-equal depth test. + ProjectedDecal, +} + /// Legacy fixed-function state that changes graphics-pipeline structure. /// /// Alpha reference is deliberately not present: it is dynamic material data, @@ -375,6 +433,8 @@ pub struct LegacyPipelineState { pub cull: LegacyCullMode, /// Whether alpha-test shader logic is enabled. pub alpha_test: bool, + /// Raster depth bias for projected decals. + pub depth_bias: LegacyDepthBias, } impl LegacyPipelineState { @@ -405,15 +465,16 @@ impl LegacyPipelineState { /// Canonical, backend-neutral key for a graphics-pipeline variant. /// /// The value is explicitly packed rather than hashed, so captures and caches -/// remain stable across processes and Rust toolchain updates. Three bits are -/// reserved for the five native material blend modes. +/// remain stable across processes and Rust toolchain updates. Bits 0..7 encode +/// blend, depth, cull and alpha-test state; bit 8 distinguishes projected +/// decal depth bias. #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] -pub struct PipelineKey(u8); +pub struct PipelineKey(u16); impl PipelineKey { /// Returns the canonical packed representation. #[must_use] - pub const fn packed(self) -> u8 { + pub const fn packed(self) -> u16 { self.0 } } @@ -437,7 +498,11 @@ impl From<LegacyPipelineState> for PipelineKey { LegacyCullMode::BackFace => 1, LegacyCullMode::FrontFace => 2, }; - Self(blend | (depth << 3) | (cull << 5) | (u8::from(state.alpha_test) << 7)) + let depth_bias = match state.depth_bias { + LegacyDepthBias::Disabled => 0, + LegacyDepthBias::ProjectedDecal => 1 << 8, + }; + Self(blend | (depth << 3) | (cull << 5) | (u16::from(state.alpha_test) << 7) | depth_bias) } } @@ -445,6 +510,40 @@ impl From<LegacyPipelineState> for PipelineKey { mod tests { use super::*; + #[test] + fn portal_alpha_matches_native_packet_distance_vectors() { + // GOG Terrain+1B0F0 with its real +246A0/+247B0 packet methods, + // Ngi32 RSQRTSS dispatch, x87 CW=007F, FOV=1.04. + for (distance, flags, expected) in [ + (0.499_938_96, 0x18, 0.0), + (4.999_542, 0x18, 0.0), + (9.999_084, 0x18, 0.132_426_6), + (25.000_572, 0x18, 0.500_011_44), + (59.999_084, 0x18, 1.0), + (85.012_48, 0x08, 0.0), + (109.994_125, 0x08, 0.649_765_2), + (25.000_572, 0x58, 0.500_011_44), + (110.0, 0x48, 0.0), + ] { + let actual = legacy_msh_portal_alpha(flags, distance, 1.04).unwrap(); + assert!( + (actual - expected).abs() < 0.000_002, + "{distance}: {actual}" + ); + } + assert_eq!(legacy_msh_portal_alpha(8, 60.0, 1.3), Some(0.0)); + assert!((legacy_msh_portal_alpha(8, 85.0, 1.3).unwrap() - 0.5).abs() < 0.000_001); + assert_eq!(legacy_msh_portal_alpha(8, 110.0, 1.3), Some(1.0)); + for (distance, fov) in [ + (f32::NAN, 1.3), + (-1.0, 1.3), + (1.0, 0.0), + (1.0, f32::INFINITY), + ] { + assert_eq!(legacy_msh_portal_alpha(8, distance, fov), None); + } + } + fn multiply_row_major(left: [f32; 16], right: [f32; 16]) -> [f32; 16] { let mut result = [0.0; 16]; for row in 0..4 { @@ -614,6 +713,24 @@ mod tests { } #[test] + fn projected_decal_bias_is_a_distinct_preserved_pipeline_state() { + let ordinary = LegacyPipelineState::default(); + let decal = LegacyPipelineState { + depth_bias: LegacyDepthBias::ProjectedDecal, + ..ordinary + }; + assert_eq!(ordinary.depth_bias, LegacyDepthBias::Disabled); + assert_ne!(PipelineKey::from(ordinary), PipelineKey::from(decal)); + assert_eq!( + decal + .with_material_category(1) + .expect("native category is represented") + .depth_bias, + LegacyDepthBias::ProjectedDecal + ); + } + + #[test] fn native_material_categories_have_distinct_pipeline_keys() { let base = LegacyPipelineState { depth: LegacyDepthMode::TestWrite, |
