feat(render): recover legacy d3d camera matrices
This commit is contained in:
@@ -127,6 +127,123 @@ impl RawCameraTransform {
|
|||||||
1.0,
|
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.
|
/// 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);
|
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(
|
fn snapshot_draw(
|
||||||
id: u64,
|
id: u64,
|
||||||
phase: RenderPhase,
|
phase: RenderPhase,
|
||||||
|
|||||||
@@ -1383,6 +1383,40 @@ next recovery target is therefore the Direct3D transform consumer in
|
|||||||
`700`, `0.1`, and `0.99` values remain unlabelled until that consumer proves
|
`700`, `0.1`, and `0.99` values remain unlabelled until that consumer proves
|
||||||
their roles.
|
their roles.
|
||||||
|
|
||||||
|
### Ngi32 Direct3D7 camera matrices recovered
|
||||||
|
|
||||||
|
The downstream renderer is now concrete. `iron3d.dll` obtains an opaque
|
||||||
|
render interface through `Ngi32!niGet3DRender`; Ngi32 RVA `0x5640` returns the
|
||||||
|
global renderer object at RVA `0x3A460`. Its constructor at RVA `0x5E10` gives
|
||||||
|
the `0x8CC`-byte object vtable `0x100315E0`. During device setup, Ngi32 RVA
|
||||||
|
`0x8E70` calls the Direct3D7 device vtable at `+0x2C` (`SetTransform`) with
|
||||||
|
state `3` for the matrix made by RVA `0x7030` and state `2` for the matrix made
|
||||||
|
by RVA `0x9450`: projection then view.
|
||||||
|
|
||||||
|
RVA `0x7030` exactly builds the row-major D3D7 projection from renderer FOV
|
||||||
|
`f`, near `n`, far `z` and viewport width/height `w`/`h`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
[ cos(f/2), 0, 0, 0 ]
|
||||||
|
[ 0, h/w * cos(f/2), 0, 0 ]
|
||||||
|
[ 0, 0, z/(z-n), sin(f/2) ]
|
||||||
|
[ 0, 0, -n*z/(z-n), 0 ]
|
||||||
|
```
|
||||||
|
|
||||||
|
The sine in the homogeneous-W term is intentional: after the D3D perspective
|
||||||
|
divide the diagonal has the expected cotangent scale. RVA `0x9450` applies a
|
||||||
|
specific axis permutation/sign change and translated dot products to selector
|
||||||
|
0 before the view `SetTransform`; it is not merely the generic affine inverse.
|
||||||
|
|
||||||
|
An elevated read-only AutoDemo probe sampled a 1024×768 renderer with
|
||||||
|
near=`0.5`, far=`700`, FOV=`1.3` radians and showed its view-source pointer
|
||||||
|
byte-identical to the active Terrain outer camera's selector-0 block. This
|
||||||
|
proves ownership and the D3D7 boundary, while keeping the earlier
|
||||||
|
`CBufferingCamera` FOV=`1.04` as a distinct upstream interface value. The
|
||||||
|
backend-neutral render crate now exposes the two recovered D3D7 matrices, but
|
||||||
|
does not yet use them as Vulkan matrices: vector and clip-space conversion is
|
||||||
|
still an explicit next task.
|
||||||
|
|
||||||
A fresh no-input launch of the canonical `iron_3d.exe` did create a responsive
|
A fresh no-input launch of the canonical `iron_3d.exe` did create a responsive
|
||||||
window titled `Parkan. Железная Стратегия`. A read-only probe then requested
|
window titled `Parkan. Железная Стратегия`. A read-only probe then requested
|
||||||
`PROCESS_QUERY_INFORMATION | PROCESS_VM_READ` and attempted to read the known
|
`PROCESS_QUERY_INFORMATION | PROCESS_VM_READ` and attempted to read the known
|
||||||
|
|||||||
Reference in New Issue
Block a user