From 8c0ae03addfdda9105728443fa32af7f7081f6c7 Mon Sep 17 00:00:00 2001 From: numzero Date: Sun, 18 Oct 2026 02:35:05 +0300 Subject: [PATCH] unify perspective/orthogonal cameras --- src/camera.rs | 62 +++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 17 ++++++++---- ui/src/main_window.ui | 2 +- 3 files changed, 75 insertions(+), 6 deletions(-) diff --git a/src/camera.rs b/src/camera.rs index 4d3311c..cda1641 100644 --- a/src/camera.rs +++ b/src/camera.rs @@ -16,6 +16,39 @@ pub(crate) fn projection(fovy: f32, aspect: f32, base_distance: f32) -> Mat4 { Mat4::perspective_rh(fovy, aspect, 0.1 * base_distance, 10.0 * base_distance) } +pub(crate) struct Camera { + pub y_frac: f32, + pub y_angle: f32, + pub y_range: f32, + pub z_min: f32, + pub z_max: f32, +} + +impl Camera { + pub fn clamp_z_min(&mut self, min_z: f32) -> &mut Self { + let (s, c) = (0.5 * self.y_angle).sin_cos(); + // let distance = 0.5 * self.y_len * c / s; + let distance_scaled = 0.5 * self.y_range * c; + if distance_scaled + self.z_min * s < min_z * s { + self.z_min = min_z - distance_scaled / s; + } + self + } + + pub fn to_mat4(&self) -> Mat4 { + let tan_fov = (0.5 * self.y_angle).tan(); + let z_range = self.z_max - self.z_min; + let y_far = 0.5 * self.y_range + self.z_max * tan_fov; + let u = -y_far / z_range; + Mat4::from_cols_array_2d(&[ + [self.y_frac, 0., 0., 0.], + [0., 1., 0., 0.], + [0., 0., u, -tan_fov], + [0., 0., u * self.z_min, 0.5 * self.y_range], + ]) + } +} + #[cfg(test)] mod tests { use super::*; @@ -76,4 +109,33 @@ mod tests { panic!(); } } + + #[test] + fn test_proj() { + for (y_range, y_angle_deg) in [ + (10.000000, 53.130102f32), + (11.547005, 60.000000f32), + (20.000000, 90.000000f32), + (34.641016, 120.000000f32), + (40.000000, 126.869898f32), + ] { + let y_angle = y_angle_deg.to_radians(); + let camera = Camera { + y_frac: 0.5, + y_angle, + y_range, + z_min: -9.0, + z_max: 90.0, + }; + let actual = camera.to_mat4(); + let expected = projection(y_angle, 1.0 / camera.y_frac, 10.0) + * Affine3A::from_translation(vec3(0., 0., -10.0)); + let c = expected.x_axis.x / actual.x_axis.x; + assert!( + (c * actual).abs_diff_eq(expected, 1e-3), + "projection at {y_angle_deg}d is wrong\nexpected: {expected}\nexpected: {}\ncomputed: {actual}", + expected / c, + ); + } + } } diff --git a/src/lib.rs b/src/lib.rs index 1eae7df..11b45d5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -108,15 +108,22 @@ impl Core { } fn render(&self, output: &wgpu::Texture, args: &RedrawArgs) { - let aspect = { + let y_frac = { let size = output.size(); let w = size.width as f32; let h = size.height as f32; - w / h + h / w }; - let distance = 0.5 * args.cam_view_y / (0.5 * args.cam_view_angle).tan(); - let view = Mat4::from(camera::view(args.cam_yaw, args.cam_pitch, distance)); - let proj = camera::projection(args.cam_view_angle, aspect, distance); + let proj = camera::Camera { + y_frac, + y_angle: args.cam_view_angle, + y_range: args.cam_view_y, + z_min: -0.5 * args.cam_view_y, + z_max: 0.5 * args.cam_view_y, + } + .clamp_z_min(0.01 * args.cam_view_y) + .to_mat4(); + let view = Mat4::from_mat3(camera::rotation(-args.cam_yaw, -args.cam_pitch)); self.mesh_pipeline.set_params( &self.queue, render::mesh::Params { diff --git a/ui/src/main_window.ui b/ui/src/main_window.ui index 19d28a8..748c5e2 100644 --- a/ui/src/main_window.ui +++ b/ui/src/main_window.ui @@ -143,7 +143,7 @@ 1 - 15.000000000000000 + 0.000000000000000 150.000000000000000