From ab7cd63dec1899af5f5550b00b07e28063ff9a8e Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Tue, 23 Jun 2026 18:35:32 +0300 Subject: [PATCH] feat(camera): f64 CPU project/unproject helpers (world_offset removal P1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add camera-relative-f64 world↔screen on Camera (project_f64, unproject_on_plane_f64): the point is made eye-relative in f64 before the rotation-only projection, so CPU picking / overlay stay exact at large absolute coordinates — the CPU twin of the GPU's relative-to-eye path. Additive and inert for now; the upcoming phases route overlay, pick/snap and coordinate readout through these so world_offset can be dropped. Co-Authored-By: Claude Opus 4.7 --- src/scene/view/camera.rs | 62 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 60 insertions(+), 2 deletions(-) diff --git a/src/scene/view/camera.rs b/src/scene/view/camera.rs index 2583a03cf..66c865ea4 100644 --- a/src/scene/view/camera.rs +++ b/src/scene/view/camera.rs @@ -118,7 +118,6 @@ impl Camera { /// must already be expressed relative to the eye (done per-vertex with /// double-single precision in the shader), so the large eye translation /// never enters the f32 matrix and large-coordinate jitter disappears. - #[allow(dead_code)] // consumed by the RTE uniform / shaders (next phase) pub fn view_proj_rte(&self, bounds: Rectangle) -> Mat4 { let near = self.distance * 0.001; let far = self.distance * 1000.0; @@ -140,10 +139,69 @@ impl Camera { OPENGL_TO_WGPU * proj * view } + /// Project a world point to screen pixels with full f64 precision: the + /// point is made eye-relative in f64 (small numbers near the view) before + /// the rotation-only projection, so it stays exact at large absolute + /// coordinates — the CPU equivalent of the GPU's relative-to-eye path. + /// Returns `None` for points at/behind the eye plane (w ≈ 0). + #[allow(dead_code)] // consumed by the world_offset-removal CPU migration + pub fn project_f64(&self, p: glam::DVec3, bounds: Rectangle) -> Option { + let rel = (p - self.eye_f64()).as_vec3(); + let clip = self.view_proj_rte(bounds) * rel.extend(1.0); + if clip.w.abs() < 1e-9 { + return None; + } + let ndc = clip.truncate() / clip.w; + Some(glam::vec2( + (ndc.x * 0.5 + 0.5) * bounds.width, + (0.5 - ndc.y * 0.5) * bounds.height, + )) + } + + /// Unproject a screen point onto an arbitrary world plane in f64. The ray + /// is built in eye-relative space (precise), intersected with the plane + /// expressed relative to the eye, then shifted back by the f64 eye — so the + /// returned world point keeps full precision at large absolute coordinates. + #[allow(dead_code)] // consumed by the world_offset-removal CPU migration + pub fn unproject_on_plane_f64( + &self, + screen: Point, + bounds: Rectangle, + plane_normal: Vec3, + plane_point: glam::DVec3, + ) -> glam::DVec3 { + let eye = self.eye_f64(); + let ndc_x = (screen.x / bounds.width) * 2.0 - 1.0; + let ndc_y = 1.0 - (screen.y / bounds.height) * 2.0; + let inv = self.view_proj_rte(bounds).inverse(); + // Ray origin / direction in eye-relative space. + let (ray_origin, ray_dir) = match self.projection { + Projection::Perspective => { + let near_pt = inv.project_point3(Vec3::new(ndc_x, ndc_y, 0.0)); + let far_pt = inv.project_point3(Vec3::new(ndc_x, ndc_y, 1.0)); + (near_pt, (far_pt - near_pt).normalize()) + } + Projection::Orthographic => { + let origin = inv.project_point3(Vec3::new(ndc_x, ndc_y, 0.0)); + let forward = self.rotation * Vec3::NEG_Z; + (origin, forward) + } + }; + // Plane point relative to the eye (small) for a precise intersection. + let plane_rel = (plane_point - eye).as_vec3(); + let denom = ray_dir.dot(plane_normal); + let rel_hit = if denom.abs() < 1e-6 { + plane_rel + } else { + let t = (plane_rel - ray_origin).dot(plane_normal) / denom; + ray_origin + ray_dir * t + }; + eye + rel_hit.as_dvec3() + } + /// Eye position split into two f32 (high + low) emulating f64, for the /// double-single relative-to-eye shaders. `high + low ≈ eye` to ~f64 /// precision; the shader subtracts these from each vertex's own high/low. - #[allow(dead_code)] // consumed by the RTE uniform (next phase) pub fn eye_high_low(&self) -> ([f32; 3], [f32; 3]) { let e = self.eye_f64(); let high = [e.x as f32, e.y as f32, e.z as f32];