unify perspective/orthogonal cameras

This commit is contained in:
numzero 2026-10-18 02:35:05 +03:00
parent 5c6ef7c758
commit 8c0ae03add
3 changed files with 75 additions and 6 deletions

View File

@ -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,
);
}
}
}

View File

@ -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 {

View File

@ -143,7 +143,7 @@
<number>1</number>
</property>
<property name="minimum">
<double>15.000000000000000</double>
<double>0.000000000000000</double>
</property>
<property name="maximum">
<double>150.000000000000000</double>