diff --git a/src/scene/model/image_model.rs b/src/scene/model/image_model.rs index 8755c242c..882271a47 100644 --- a/src/scene/model/image_model.rs +++ b/src/scene/model/image_model.rs @@ -23,6 +23,9 @@ pub struct ImageModel { /// [2] origin + U*W + V*H (top-right) /// [3] origin + V*H (top-left) pub corners: [[f32; 3]; 4], + /// Low residual paired with `corners` (double-single) so the GPU keeps + /// sub-unit precision at UTM-scale insertion points. + pub corners_low: [[f32; 3]; 4], /// Optional world-space XY rect [x0, y0, x1, y1] for paper-space /// viewport clipping. Mirrors `WireModel.vp_scissor` / /// `HatchModel.vp_scissor`. @@ -44,21 +47,33 @@ impl ImageModel { let h = img.size.y; // Model-space geometry is drawn in (WCS - world_offset) so large UTM- // scale coordinates stay within f32 precision; offset the image too. - let ox = (img.insertion_point.x - world_offset[0]) as f32; - let oy = (img.insertion_point.y - world_offset[1]) as f32; - let oz = (img.insertion_point.z - world_offset[2]) as f32; + // Corners come from a large insertion point plus small u/v spans. + // Split each into double-single (high, low) f32 so the GPU keeps + // sub-unit precision at UTM scale and after a cross-drawing paste. + let oxv = img.insertion_point.x - world_offset[0]; + let oyv = img.insertion_point.y - world_offset[1]; + let ozv = img.insertion_point.z - world_offset[2]; let ux = (img.u_vector.x * w) as f32; let uy = (img.u_vector.y * w) as f32; let uz = (img.u_vector.z * w) as f32; let vx = (img.v_vector.x * h) as f32; let vy = (img.v_vector.y * h) as f32; let vz = (img.v_vector.z * h) as f32; + // High/low split of the anchor; the u/v spans are small and added to + // the high half (their own residual is below f32 noise at this scale). + let ox = oxv as f32; + let oy = oyv as f32; + let oz = ozv as f32; + let oxl = (oxv - ox as f64) as f32; + let oyl = (oyv - oy as f64) as f32; + let ozl = (ozv - oz as f64) as f32; let corners = [ [ox, oy, oz], [ox + ux, oy + uy, oz + uz], [ox + ux + vx, oy + uy + vy, oz + uz + vz], [ox + vx, oy + vy, oz + vz], ]; + let corners_low = [[oxl, oyl, ozl]; 4]; let opacity = 1.0 - img.fade as f32 / 100.0; let (pixels, width, height) = load_pixels(&img.file_path)?; @@ -69,6 +84,7 @@ impl ImageModel { height, opacity, corners, + corners_low, vp_scissor: None, draw_depth: 0.0, }) diff --git a/src/scene/pipeline/image_gpu.rs b/src/scene/pipeline/image_gpu.rs index fbdddfa66..a5a0f9f49 100644 --- a/src/scene/pipeline/image_gpu.rs +++ b/src/scene/pipeline/image_gpu.rs @@ -16,25 +16,32 @@ use iced::wgpu::util::DeviceExt; pub struct ImageVertex { pub pos: [f32; 3], pub uv: [f32; 2], + pub pos_low: [f32; 3], } impl ImageVertex { pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> { + const ATTRS: &[wgpu::VertexAttribute] = &[ + wgpu::VertexAttribute { + offset: std::mem::offset_of!(ImageVertex, pos) as u64, + shader_location: 0, + format: wgpu::VertexFormat::Float32x3, + }, + wgpu::VertexAttribute { + offset: std::mem::offset_of!(ImageVertex, uv) as u64, + shader_location: 1, + format: wgpu::VertexFormat::Float32x2, + }, + wgpu::VertexAttribute { + offset: std::mem::offset_of!(ImageVertex, pos_low) as u64, + shader_location: 2, + format: wgpu::VertexFormat::Float32x3, + }, + ]; wgpu::VertexBufferLayout { array_stride: std::mem::size_of::() as u64, step_mode: wgpu::VertexStepMode::Vertex, - attributes: &[ - wgpu::VertexAttribute { - offset: 0, - shader_location: 0, - format: wgpu::VertexFormat::Float32x3, - }, - wgpu::VertexAttribute { - offset: 12, - shader_location: 1, - format: wgpu::VertexFormat::Float32x2, - }, - ], + attributes: ATTRS, } } } @@ -154,31 +161,14 @@ impl ImageGpu { // corners: [p0=BL, p1=BR, p2=TR, p3=TL] // UV: BL=(0,1), BR=(1,1), TR=(1,0), TL=(0,0) let [p0, p1, p2, p3] = model.corners; + let [l0, l1, l2, l3] = model.corners_low; let verts = [ - ImageVertex { - pos: p0, - uv: [0.0, 1.0], - }, - ImageVertex { - pos: p1, - uv: [1.0, 1.0], - }, - ImageVertex { - pos: p2, - uv: [1.0, 0.0], - }, - ImageVertex { - pos: p0, - uv: [0.0, 1.0], - }, - ImageVertex { - pos: p2, - uv: [1.0, 0.0], - }, - ImageVertex { - pos: p3, - uv: [0.0, 0.0], - }, + ImageVertex { pos: p0, uv: [0.0, 1.0], pos_low: l0 }, + ImageVertex { pos: p1, uv: [1.0, 1.0], pos_low: l1 }, + ImageVertex { pos: p2, uv: [1.0, 0.0], pos_low: l2 }, + ImageVertex { pos: p0, uv: [0.0, 1.0], pos_low: l0 }, + ImageVertex { pos: p2, uv: [1.0, 0.0], pos_low: l2 }, + ImageVertex { pos: p3, uv: [0.0, 0.0], pos_low: l3 }, ]; let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("image.vbuf"), diff --git a/src/shaders/image.wgsl b/src/shaders/image.wgsl index e2dc8c182..d4356335a 100644 --- a/src/shaders/image.wgsl +++ b/src/shaders/image.wgsl @@ -40,8 +40,9 @@ const DRAW_ORDER_BIAS: f32 = 0.001; // ── Vertex stage ────────────────────────────────────────────────────────────── struct VertIn { - @location(0) pos: vec3, - @location(1) uv: vec2, + @location(0) pos: vec3, + @location(1) uv: vec2, + @location(2) pos_low: vec3, }; struct VertOut { @@ -52,7 +53,8 @@ struct VertOut { @vertex fn vs_main(in: VertIn) -> VertOut { var out: VertOut; - out.clip_pos = u.view_rot * vec4((in.pos - u.eye_high) - u.eye_low, 1.0); + let rel = (in.pos - u.eye_high) + (in.pos_low - u.eye_low); + out.clip_pos = u.view_rot * vec4(rel, 1.0); out.clip_pos.z = out.clip_pos.z - img_params.draw_depth * DRAW_ORDER_BIAS * out.clip_pos.w; out.uv = in.uv; return out;