From 1c0520e4e437840a9f0fb9dfcab159955e171999 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Thu, 23 Apr 2026 09:28:54 +0300 Subject: [PATCH] fix: correct CPU projection axes to match GPU look_at_rh convention MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit viewport_content_wires() computed view_right via world_z × view_direction (cross-product). This gives the OPPOSITE sign to the right vector that look_at_rh produces for views with a Y component (FRONT/BACK views): GPU look_at_rh: screen_right = f × up = (−vd) × (rot*Y) = rot*X Old CPU formula: screen_right = Z × vd ← opposite sign for FRONT/BACK The mismatch caused the paper-space projection to appear mirror-flipped after panning/rotating in MSPACE. Fix: use camera_for_viewport() to obtain the exact same rotation quaternion the GPU uses, then derive both axes from it: view_right = rotation * X view_up = rotation * Y Also replaces the perspective depth dot-product (mp · vd) with mp · (rotation*Z) so the depth direction is consistent too. Co-Authored-By: Claude Sonnet 4.6 --- src/scene/mod.rs | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/src/scene/mod.rs b/src/scene/mod.rs index c52d93af0..768986c3c 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -702,22 +702,19 @@ impl Scene { // ── Per-viewport frozen layer set ───────────────────────────── let frozen: HSet = vp.frozen_layers.iter().cloned().collect(); - // ── View direction coordinate frame ─────────────────────────── - let vd = glam::Vec3::new( - vp.view_direction.x as f32, - vp.view_direction.y as f32, - vp.view_direction.z as f32, - ).normalize_or(glam::Vec3::Z); - - // Compute view_right and view_up from the direction vector. - let world_z = glam::Vec3::Z; - let view_right = if (vd.dot(world_z)).abs() > 0.99 { - // Looking straight up/down: use X as right. - glam::Vec3::X - } else { - world_z.cross(vd).normalize() + // ── View coordinate frame ───────────────────────────────────── + // Use camera_for_viewport so the axes match the GPU renderer exactly. + // GPU uses look_at_rh(eye, target, rotation*Y), which gives: + // screen_right = normalize((target-eye) × up) = rotation * X + // screen_up = rotation * Y + // The old Z×vd cross-product gives the opposite sign for views + // that have a Y component in view_direction (front/back), causing mirroring. + let cam_frame = match self.camera_for_viewport(vp.common.handle) { + Some(c) => c, + None => continue, }; - let view_up = vd.cross(view_right).normalize(); + let view_right = cam_frame.rotation * glam::Vec3::X; + let view_up = cam_frame.rotation * glam::Vec3::Y; // ── Scale & viewport parameters ─────────────────────────────── let scale = if vp.custom_scale.abs() > 1e-9 { @@ -795,9 +792,8 @@ impl Scene { let u = mp.dot(view_right); let v = mp.dot(view_up); if use_perspective { - // vd points from target toward camera, so depth along vd is the - // distance from the target plane toward the camera. - let d_vd = mp.dot(vd); + // Eye direction (target → camera) = rotation * Z. + let d_vd = mp.dot(cam_frame.rotation * glam::Vec3::Z); // Forward distance from camera (positive = in front of camera). let fwd = camera_dist - d_vd; if fwd <= 0.001 {