forked from 1b-agent/opencadstudio-upstream
fix(camera): keep the eye's coordinates out of the view basis
A drawing placed at UTM shimmered as the camera moved. The relative-to-eye path exists so the large eye translation never enters an f32 matrix, and it zeroed the translation column to make sure. But the basis was built first, by `look_at(eye, target)` with both positions cast to f32 — and at (639792, 4517057) that grid is half a metre across in y. Rounding both ends and then subtracting them left a direction that changed as the camera moved, so the whole view basis jittered. Zeroing the translation afterwards came too late to help: the damage was in the rotation. It never needed the positions. The eye is `target + rotation·Z·distance`, so the direction between them is the rotation's own Z — a unit vector carrying no coordinate at all. Two tests. Where the camera stands must not reach the matrix, which is the property being restored; and at the origin, where the old construction had no large coordinates to lose, the two must agree exactly — which is what says this kept the same basis rather than turning the view round. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7c9366e362
commit
35a2714025
1 file changed
+79
-2
@@ -276,8 +276,20 @@ impl Camera {
|
||||
let aspect = bounds.width / bounds.height;
|
||||
let up_dir = self.rotation * Vec3::Y;
|
||||
|
||||
let mut view = look_at_mat4(self.eye().as_vec3(), self.target.as_vec3(), up_dir);
|
||||
// Zero the translation column → pure rotation (world→view basis).
|
||||
// The basis, from the rotation alone. Zeroing the translation column
|
||||
// afterwards is not enough: `look_at` builds the basis out of
|
||||
// `target - eye`, and handing it two absolute positions cast to f32
|
||||
// rounds both to the f32 grid first. At UTM that grid is half a metre
|
||||
// across, so the difference — and with it the whole view basis —
|
||||
// jitters as the camera moves, which is the shimmer a survey-placed
|
||||
// drawing showed.
|
||||
//
|
||||
// It never needed them. `eye` is `target + rotation·Z·distance`, so
|
||||
// the direction between the two is the rotation's own Z, a unit
|
||||
// vector that carries no coordinate at all.
|
||||
let mut view = look_at_mat4(Vec3::ZERO, -(self.rotation * Vec3::Z), up_dir);
|
||||
// Already zero, since the eye is the origin here; kept so the invariant
|
||||
// survives someone reworking the line above.
|
||||
view.w_axis = glam::vec4(0.0, 0.0, 0.0, 1.0);
|
||||
|
||||
let proj = match self.projection {
|
||||
@@ -865,3 +877,68 @@ mod tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod rte_tests {
|
||||
use super::*;
|
||||
use iced::Rectangle;
|
||||
|
||||
/// The relative-to-eye view carries rotation and nothing else, so where the
|
||||
/// camera stands must not reach it at all.
|
||||
///
|
||||
/// It used to. The basis came from `look_at(eye, target)` with both cast to
|
||||
/// f32, and at survey coordinates that grid is half a metre across — so the
|
||||
/// difference between them, and the whole basis, jittered as the camera
|
||||
/// moved. A drawing placed at UTM shimmered.
|
||||
#[test]
|
||||
fn the_view_basis_does_not_depend_on_where_the_camera_stands() {
|
||||
let bounds = Rectangle {
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
width: 1600.0,
|
||||
height: 900.0,
|
||||
};
|
||||
let mut at_origin = Camera::default();
|
||||
at_origin.target = glam::DVec3::ZERO;
|
||||
|
||||
let mut at_utm = at_origin.clone();
|
||||
at_utm.target = glam::DVec3::new(639_792.184_2, 4_517_057.531_7, 12.5);
|
||||
|
||||
let here = at_origin.view_proj_rte(bounds);
|
||||
let there = at_utm.view_proj_rte(bounds);
|
||||
for (a, b) in here.to_cols_array().iter().zip(there.to_cols_array().iter()) {
|
||||
assert_eq!(a, b, "the camera's position reached the view matrix");
|
||||
}
|
||||
}
|
||||
|
||||
/// And it is the same basis as before, which the origin proves: there the
|
||||
/// old construction had no large coordinates to lose, so the two must
|
||||
/// agree exactly. Anything else would mean the refactor turned the view
|
||||
/// round rather than just keeping the coordinates out of it.
|
||||
#[test]
|
||||
fn the_basis_matches_what_two_positions_gave_where_they_were_exact() {
|
||||
for (yaw, pitch) in [(0.0, 1.2), (0.7, 0.3), (-2.1, -0.9)] {
|
||||
let mut camera = Camera::default();
|
||||
camera.target = glam::DVec3::ZERO;
|
||||
camera.rotation =
|
||||
Quat::from_rotation_z(yaw) * Quat::from_rotation_x(pitch);
|
||||
let up_dir = camera.rotation * Vec3::Y;
|
||||
|
||||
let mut old = look_at_mat4(
|
||||
camera.eye().as_vec3(),
|
||||
camera.target.as_vec3(),
|
||||
up_dir,
|
||||
);
|
||||
old.w_axis = glam::vec4(0.0, 0.0, 0.0, 1.0);
|
||||
let new = {
|
||||
let mut view =
|
||||
look_at_mat4(Vec3::ZERO, -(camera.rotation * Vec3::Z), up_dir);
|
||||
view.w_axis = glam::vec4(0.0, 0.0, 0.0, 1.0);
|
||||
view
|
||||
};
|
||||
for (a, b) in old.to_cols_array().iter().zip(new.to_cols_array().iter()) {
|
||||
assert!((a - b).abs() < 1e-6, "yaw {yaw} pitch {pitch}: {a} vs {b}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user