fix(render): use native shifted water reflection camera
Docs Deploy / Build and Deploy MkDocs (push) Successful in 35s

This commit is contained in:
Valentin Popov committed 2026-11-01 07:49:51 +04:00
1 parent 6d15bc2006
commit 0f8b14abbf
7 files changed
+168 -25

No files matched your search

+108
View File
@@ -51,10 +51,118 @@ pub fn legacy_embm_uv(world_xy: [f32; 2], bounds: [f32; 4]) -> Option<[f32; 2]>
])
}
/// The EMBM camera looks up from the mirrored eye, with its window on the water rectangle.
/// Terrain.dll+4EFF0 supplies `[min_y-eye_y, min_x-eye_x, span_y, span_x, eye_z-height]`.
/// The ordinary perspective's depth mapping is retained; orthographic inputs use the caller's fallback.
#[must_use]
pub fn legacy_embm_reflection_clip(
main_clip: [f32; 16],
eye: [f32; 3],
height: f32,
bounds: [f32; 4],
) -> Option<[f32; 16]> {
let span = [bounds[2] - bounds[0], bounds[3] - bounds[1]];
let distance = eye[2] - height;
if !main_clip
.into_iter()
.chain(eye)
.chain([height])
.chain(bounds)
.chain(span)
.all(f32::is_finite)
|| distance <= 0.0
|| span.into_iter().any(|v| v <= 0.0)
{
return None;
}
let w = [main_clip[3], main_clip[7], main_clip[11]];
let squared = w.into_iter().map(|v| v * v).sum::<f32>();
if !squared.is_finite() || squared <= f32::EPSILON {
return None;
}
let depth_a = (main_clip[2] * w[0] + main_clip[6] * w[1] + main_clip[10] * w[2]) / squared;
// shortcut: retain the main depth range until the native shifted depth matrix is verified.
let depth_b = (main_clip[14] - depth_a * main_clip[15]) / squared.sqrt();
if !depth_a.is_finite() || !depth_b.is_finite() || depth_a < 1.0 || depth_b >= 0.0 {
return None;
}
let z = 2.0 * height - eye[2];
let x_scale = -2.0 * distance / span[1];
let y_scale = -2.0 * distance / span[0];
let x_shift = 1.0 + 2.0 * (bounds[1] - eye[1]) / span[1];
let y_shift = 1.0 + 2.0 * (bounds[0] - eye[0]) / span[0];
let matrix = [
0.0,
y_scale,
0.0,
0.0,
x_scale,
0.0,
0.0,
0.0,
x_shift,
y_shift,
depth_a,
1.0,
-x_scale * eye[1] - x_shift * z,
-y_scale * eye[0] - y_shift * z,
depth_b - depth_a * z,
-z,
];
matrix.into_iter().all(f32::is_finite).then_some(matrix)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn shifted_camera_maps_native_water_corners_and_retains_perspective_depth() {
let projection = crate::LegacyD3d7Projection {
viewport: [0, 0, 1280, 1024],
near_plane: 0.5,
far_plane: 700.0,
field_of_view_radians: 1.3,
}
.try_direct3d7_projection_row_major()
.unwrap();
let eye = [1200.0, 800.0, 185.0];
let mut main = [0.0; 16];
main[2] = projection[10];
main[3] = projection[11];
main[14] = -eye[0] * projection[10] + projection[14];
main[15] = -eye[0] * projection[11];
let bounds = [1000.0, 500.0, 1500.0, 1200.0];
let matrix = legacy_embm_reflection_clip(main, eye, 150.0, bounds).unwrap();
let project = |point: [f32; 3]| -> [f32; 4] {
std::array::from_fn(|column| {
point[0] * matrix[column]
+ point[1] * matrix[4 + column]
+ point[2] * matrix[8 + column]
+ matrix[12 + column]
})
};
for point in [
[1000.0, 500.0, 150.0],
[1500.0, 1200.0, 150.0],
[1125.0, 1025.0, 150.0],
] {
let clip = project(point);
let expected = legacy_embm_uv([point[0], point[1]], bounds).unwrap();
for axis in 0..2 {
assert!((clip[axis] / clip[3] * 0.5 + 0.5 - expected[axis]).abs() < 0.00001);
}
assert_eq!(clip[3], 35.0);
}
let near = project([1200.0, 800.0, 115.5]);
assert!((near[2] / near[3]).abs() < 0.001);
let far = project([1200.0, 800.0, 815.0]);
assert!((far[2] / far[3] - 1.0).abs() < 0.00001);
assert!(legacy_embm_reflection_clip([0.0; 16], eye, 150.0, bounds).is_none());
assert!(legacy_embm_reflection_clip(main, eye, 185.0, bounds).is_none());
assert!(legacy_embm_reflection_clip(main, eye, 150.0, [1.0; 4]).is_none());
}
#[test]
fn wave_bytes_match_native_generation_and_clock_wraps_without_float_time_loss() {
let texels = legacy_embm_bump_map();