aboutsummaryrefslogtreecommitdiff
path: root/crates/fparkan-render/src
diff options
context:
space:
mode:
Diffstat (limited to 'crates/fparkan-render/src')
-rw-r--r--crates/fparkan-render/src/lib.rs127
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,