aboutsummaryrefslogtreecommitdiff
path: root/crates/fparkan-render/src/lib.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/fparkan-render/src/lib.rs')
-rw-r--r--crates/fparkan-render/src/lib.rs182
1 files changed, 182 insertions, 0 deletions
diff --git a/crates/fparkan-render/src/lib.rs b/crates/fparkan-render/src/lib.rs
index 09d28f9..a481732 100644
--- a/crates/fparkan-render/src/lib.rs
+++ b/crates/fparkan-render/src/lib.rs
@@ -127,6 +127,123 @@ impl RawCameraTransform {
1.0,
])
}
+
+ /// Reproduces Ngi32's `Direct3D7` view-matrix conversion for this transform.
+ ///
+ /// The legacy renderer copies selector-0 into its camera state, then maps
+ /// its axes and translation in this exact order before calling
+ /// `IDirect3DDevice7::SetTransform(D3DTRANSFORMSTATE_VIEW, ...)`. This is
+ /// deliberately distinct from [`Self::try_inverse_affine_row_major`]: it
+ /// includes the original renderer's coordinate-system conversion.
+ ///
+ /// The returned matrix is row-major D3D7 data, not yet a Vulkan view
+ /// matrix. A later adapter must explicitly account for clip-space and
+ /// shader-vector conventions.
+ #[must_use]
+ pub fn try_direct3d7_view_row_major(self) -> Option<[f32; 16]> {
+ let matrix = self.words.map(f32::from_bits);
+ if !matrix.iter().all(|value| value.is_finite())
+ || matrix[12].abs() > f32::EPSILON
+ || matrix[13].abs() > f32::EPSILON
+ || matrix[14].abs() > f32::EPSILON
+ || (matrix[15] - 1.0).abs() > f32::EPSILON
+ {
+ return None;
+ }
+
+ let [m00, m01, m02, m03, m10, m11, m12, m13, m20, m21, m22, m23, _, _, _, _] = matrix;
+ Some([
+ -m01,
+ m02,
+ m00,
+ 0.0,
+ -m11,
+ m12,
+ m10,
+ 0.0,
+ -m21,
+ m22,
+ m20,
+ 0.0,
+ m23.mul_add(m21, m13.mul_add(m11, m03 * m01)),
+ -(m23.mul_add(m12, m13.mul_add(m02, m03 * m10))),
+ -(m00.mul_add(m03, m23.mul_add(m20, m13 * m10))),
+ 1.0,
+ ])
+ }
+}
+
+/// Parameters consumed by Ngi32's `Direct3D7` projection-matrix builder.
+///
+/// These are a separate legacy-renderer boundary from
+/// [`RawCameraProjection`]. The latter preserves Terrain's source ABI, while
+/// this type records the already-resolved Ngi32 values submitted to `Direct3D7`.
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub struct LegacyD3d7Projection {
+ /// Viewport rectangle as `(left, top, right, bottom)`.
+ pub viewport: [i32; 4],
+ /// Positive camera near-plane distance.
+ pub near_plane: f32,
+ /// Camera far-plane distance, greater than [`Self::near_plane`].
+ pub far_plane: f32,
+ /// Full field-of-view angle in radians.
+ pub field_of_view_radians: f32,
+}
+
+impl LegacyD3d7Projection {
+ /// Reconstructs the exact row-major matrix passed to D3D7 projection state.
+ ///
+ /// Ngi32 uses the viewport's `height / width`, writes `cos(fov / 2)` to
+ /// the diagonal and `sin(fov / 2)` to the homogeneous-W term. The ratio
+ /// after D3D's perspective divide is therefore the expected cotangent
+ /// scale. This remains legacy D3D7 data rather than a Vulkan projection.
+ #[must_use]
+ pub fn try_direct3d7_projection_row_major(self) -> Option<[f32; 16]> {
+ let width = self.viewport[2].checked_sub(self.viewport[0])?;
+ let height = self.viewport[3].checked_sub(self.viewport[1])?;
+ if width <= 0
+ || height <= 0
+ || !self.near_plane.is_finite()
+ || !self.far_plane.is_finite()
+ || !self.field_of_view_radians.is_finite()
+ || self.near_plane <= 0.0
+ || self.far_plane <= self.near_plane
+ || self.field_of_view_radians <= 0.0
+ || self.field_of_view_radians >= std::f32::consts::PI
+ {
+ return None;
+ }
+
+ // Ngi32 converts these signed viewport dimensions into single-precision
+ // arithmetic before building the legacy matrix.
+ #[allow(clippy::cast_precision_loss)]
+ let aspect = (height as f32) / (width as f32);
+ let half_fov = self.field_of_view_radians * 0.5;
+ let cosine = half_fov.cos();
+ let sine = half_fov.sin();
+ let depth_scale = 1.0 / (1.0 - self.near_plane / self.far_plane);
+ [aspect, cosine, sine, depth_scale]
+ .iter()
+ .all(|value| value.is_finite())
+ .then_some([
+ cosine,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ aspect * cosine,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ depth_scale,
+ sine,
+ 0.0,
+ 0.0,
+ -(depth_scale * self.near_plane),
+ 0.0,
+ ])
+ }
}
/// Raw camera state observed through the original Terrain camera interface.
@@ -951,6 +1068,71 @@ mod tests {
assert_eq!(singular.try_inverse_affine_row_major(), None);
}
+ #[test]
+ fn raw_camera_transform_reproduces_direct3d7_view_axis_conversion() {
+ let transform = RawCameraTransform {
+ words: [
+ 0.0_f32.to_bits(),
+ (-1.0_f32).to_bits(),
+ 0.0_f32.to_bits(),
+ 10.0_f32.to_bits(),
+ 1.0_f32.to_bits(),
+ 0.0_f32.to_bits(),
+ 0.0_f32.to_bits(),
+ 20.0_f32.to_bits(),
+ 0.0_f32.to_bits(),
+ 0.0_f32.to_bits(),
+ 1.0_f32.to_bits(),
+ 30.0_f32.to_bits(),
+ 0.0_f32.to_bits(),
+ 0.0_f32.to_bits(),
+ 0.0_f32.to_bits(),
+ 1.0_f32.to_bits(),
+ ],
+ };
+
+ assert_eq!(
+ transform.try_direct3d7_view_row_major(),
+ Some([
+ 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, -10.0, -10.0, -20.0,
+ 1.0,
+ ])
+ );
+ assert_eq!(
+ RawCameraTransform { words: [0_u32; 16] }.try_direct3d7_view_row_major(),
+ None
+ );
+ }
+
+ #[test]
+ fn legacy_d3d7_projection_matches_recovered_camera_formula() {
+ let projection = LegacyD3d7Projection {
+ viewport: [0, 0, 1024, 768],
+ near_plane: 0.5,
+ far_plane: 700.0,
+ field_of_view_radians: 1.3,
+ };
+ let matrix = projection
+ .try_direct3d7_projection_row_major()
+ .expect("live Ngi32 projection parameters are valid");
+ let half_fov = 0.65_f32;
+ let depth_scale = 700.0_f32 / 699.5;
+
+ assert_eq!(matrix[0], half_fov.cos());
+ assert_eq!(matrix[5], 0.75 * half_fov.cos());
+ assert_eq!(matrix[10], depth_scale);
+ assert_eq!(matrix[11], half_fov.sin());
+ assert_eq!(matrix[14], -(depth_scale * 0.5));
+ assert_eq!(
+ LegacyD3d7Projection {
+ viewport: [0, 0, 0, 768],
+ ..projection
+ }
+ .try_direct3d7_projection_row_major(),
+ None
+ );
+ }
+
fn snapshot_draw(
id: u64,
phase: RenderPhase,