From 0f7920dc2c749c6bd7df222e7f6a6922be4c79eb Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Tue, 23 Jun 2026 17:52:28 +0300 Subject: [PATCH] feat(render): double-single anchors for both hatch pipelines The hatch anchor (world_origin / pattern origin) was cast to a single f32, quantizing UTM-scale fills to ~0.5 m and reintroducing jitter the wire path no longer had. Both the batched and per-hatch pipelines now carry the anchor as a high/low f32 pair (HatchInstance.world_origin_low, HatchUniformData.origin_low) and reconstruct the eye-relative position in the shader as (origin_high - eye_high) + (local + origin_low - eye_low). Co-Authored-By: Claude Opus 4.7 --- src/scene/pipeline/hatch_batched_gpu.rs | 56 +++++++++++++++++-------- src/scene/pipeline/hatch_gpu.rs | 11 +++-- src/shaders/hatch.wgsl | 13 ++++-- src/shaders/hatch_batched.wgsl | 20 ++++++--- 4 files changed, 71 insertions(+), 29 deletions(-) diff --git a/src/scene/pipeline/hatch_batched_gpu.rs b/src/scene/pipeline/hatch_batched_gpu.rs index 51981b920..f87be2323 100644 --- a/src/scene/pipeline/hatch_batched_gpu.rs +++ b/src/scene/pipeline/hatch_batched_gpu.rs @@ -51,26 +51,38 @@ pub struct HatchInstance { pub color: [f32; 4], // 0 pub color2: [f32; 4], // 16 (gradient end) pub aabb: [f32; 4], // 32 (local-space xmin,ymin,xmax,ymax) - pub world_origin: [f32; 2], // 48 (anchor added back in VS) - pub angle_offset: f32, // 56 - pub scale: f32, // 60 - pub grad_cos: f32, // 64 - pub grad_sin: f32, // 68 - pub grad_min: f32, // 72 - pub grad_range: f32, // 76 - pub mode: u32, // 80 (0=pattern, 1=solid, 2=gradient) - pub visible: u32, // 84 (CPU sets to 0 to skip) - pub boundary_offset: u32, // 88 (first boundary vert index) - pub boundary_count: u32, // 92 - pub family_offset: u32, // 96 - pub family_count: u32, // 100 + pub world_origin: [f32; 2], // 48 (anchor high half, added back in VS) + pub world_origin_low: [f32; 2], // 56 (anchor low residual — double-single) + pub angle_offset: f32, // 64 + pub scale: f32, // 68 + pub grad_cos: f32, // 72 + pub grad_sin: f32, // 76 + pub grad_min: f32, // 80 + pub grad_range: f32, // 84 + pub mode: u32, // 88 (0=pattern, 1=solid, 2=gradient) + pub visible: u32, // 92 (CPU sets to 0 to skip) + pub boundary_offset: u32, // 96 (first boundary vert index) + pub boundary_count: u32, // 100 + pub family_offset: u32, // 104 + pub family_count: u32, // 108 /// Signed draw-order depth (-1,1); 0.0 = neutral. Applied as a clip-z /// bias in the vertex shader so this fill orders against other types. - pub draw_depth: f32, // 104 - pub _pad0: u32, // 108 (pad to 112 = 16-byte stride) + pub draw_depth: f32, // 112 + pub _pad0: u32, // 116 (pad to 128 = 16-byte stride) + pub _pad1: u32, // 120 + pub _pad2: u32, // 124 } -const _: () = assert!(std::mem::size_of::() == 112); +const _: () = assert!(std::mem::size_of::() == 128); + +/// Split a hatch's f64 world-origin anchor into double-single (high, low) f32 +/// pairs so the GPU keeps sub-unit precision at UTM-scale coordinates. +#[inline] +fn split_origin_ds(o: [f64; 2]) -> ([f32; 2], [f32; 2]) { + let hx = o[0] as f32; + let hy = o[1] as f32; + ([hx, hy], [(o[0] - hx as f64) as f32, (o[1] - hy as f64) as f32]) +} /// Mirrors the per-family struct used by the existing per-hatch shader, /// but the dash slice lives in a separate concatenated DashBuffer (the @@ -261,11 +273,13 @@ impl HatchBatchedGpu { if !min_x.is_finite() { // Empty / all-NaN — skip but keep the slot so indices // stay in lockstep with the input list (visibility=0). + let (wo_hi, wo_lo) = split_origin_ds(h.world_origin); instances.push(HatchInstance { color: h.color, color2, aabb: [0.0, 0.0, 0.0, 0.0], - world_origin: h.world_origin.map(|v| v as f32), + world_origin: wo_hi, + world_origin_low: wo_lo, angle_offset: h.angle_offset, scale: h.scale.max(1e-6), grad_cos, @@ -280,6 +294,8 @@ impl HatchBatchedGpu { family_count, draw_depth: h.draw_depth, _pad0: 0, + _pad1: 0, + _pad2: 0, }); continue; } @@ -300,11 +316,13 @@ impl HatchBatchedGpu { }; let pad = (diag * 0.8 + max_spacing * 2.0 * h.scale).max(1.0); + let (wo_hi, wo_lo) = split_origin_ds(h.world_origin); instances.push(HatchInstance { color: h.color, color2, aabb: [min_x - pad, min_y - pad, max_x + pad, max_y + pad], - world_origin: h.world_origin.map(|v| v as f32), + world_origin: wo_hi, + world_origin_low: wo_lo, angle_offset: h.angle_offset, scale: h.scale.max(1e-6), grad_cos, @@ -319,6 +337,8 @@ impl HatchBatchedGpu { family_count, draw_depth: h.draw_depth, _pad0: 0, + _pad1: 0, + _pad2: 0, }); } diff --git a/src/scene/pipeline/hatch_gpu.rs b/src/scene/pipeline/hatch_gpu.rs index 4f788ed96..4a2265eb0 100644 --- a/src/scene/pipeline/hatch_gpu.rs +++ b/src/scene/pipeline/hatch_gpu.rs @@ -58,13 +58,15 @@ pub struct HatchUniformData { pub grad_sin: f32, // 52: gradient direction sin pub grad_min: f32, // 56: gradient proj_min pub grad_range: f32, // 60: gradient proj_range - pub origin: [f32; 2], // 64: hatch-local origin (boundary AABB centre). + pub origin: [f32; 2], // 64: hatch-local origin (boundary AABB centre), + // // high half of the double-single anchor. // // The pattern fragment shader subtracts this // // from xz before the perp/perp_step quotient // // so f32 doesn't catastrophically cancel when // // world coords are large and pattern spacing // // is small (e.g. UTM drawing + sub-mm hatch). - pub _pad: [f32; 2], // 72: 16-byte alignment + pub origin_low: [f32; 2], // 72: anchor low residual (double-single) so the + // // vertex clip position stays precise at UTM scale. } // total 80 bytes /// Boundary polygon (binding 1). Matches WGSL `Boundary`. @@ -288,7 +290,10 @@ impl HatchGpu { grad_min, grad_range, origin: [origin[0] as f32, origin[1] as f32], - _pad: [0.0; 2], + origin_low: [ + (origin[0] - origin[0] as f32 as f64) as f32, + (origin[1] - origin[1] as f32 as f64) as f32, + ], }; // ── BoundaryData (in snapped-local space) ──────────────────────── diff --git a/src/shaders/hatch.wgsl b/src/shaders/hatch.wgsl index 6ec6f3de5..fbebc1ddf 100644 --- a/src/shaders/hatch.wgsl +++ b/src/shaders/hatch.wgsl @@ -53,7 +53,7 @@ struct HatchUniforms { // // `xz - origin` so the f32 modulo doesn't // // catastrophically cancel when world coords // // are large and pattern spacing is small. - _pad: vec2, // 72: 16-byte alignment + origin_low: vec2, // 72: anchor low residual (double-single) } @group(1) @binding(0) var h: HatchUniforms; @@ -104,8 +104,15 @@ struct VOut { // jitter is invisible. The local-space `xz` we export to the // fragment shader stays at full f32 precision so the in_polygon / // pattern math doesn't catastrophically cancel. - let world = vec3(v.pos.x + h.origin.x, v.pos.y + h.origin.y, v.pos.z); - o.clip = u.view_rot * vec4((world - u.eye_high) - u.eye_low, 1.0); + // Double-single relative-to-eye: anchor high cancels eye_high (Sterbenz); + // boundary-local v.pos + anchor low + (−eye_low) carry the residual. + let hi = vec3(h.origin.x - u.eye_high.x, + h.origin.y - u.eye_high.y, + -u.eye_high.z); + let lo = vec3(v.pos.x + h.origin_low.x - u.eye_low.x, + v.pos.y + h.origin_low.y - u.eye_low.y, + v.pos.z - u.eye_low.z); + o.clip = u.view_rot * vec4(hi + lo, 1.0); o.xz = vec2(v.pos.x, v.pos.y); return o; } diff --git a/src/shaders/hatch_batched.wgsl b/src/shaders/hatch_batched.wgsl index c80223bc3..f0a1880c3 100644 --- a/src/shaders/hatch_batched.wgsl +++ b/src/shaders/hatch_batched.wgsl @@ -41,7 +41,8 @@ struct HatchInstance { color: vec4, color2: vec4, aabb: vec4, // (xmin, ymin, xmax, ymax) — local space - world_origin: vec2, + world_origin: vec2, // anchor high half + world_origin_low: vec2, // anchor low residual (double-single) angle_offset: f32, scale: f32, grad_cos: f32, @@ -56,6 +57,8 @@ struct HatchInstance { family_count: u32, draw_depth: f32, // signed (-1,1) draw-order bias; 0 = neutral _pad0: u32, + _pad1: u32, + _pad2: u32, } // Draw-order depth bias (see wire.wgsl). Higher draw_depth → smaller z → @@ -132,10 +135,17 @@ fn corner_xy(c: u32, aabb: vec4) -> vec2 { } let local = corner_xy(v.corner, inst.aabb); - let world = vec3(local.x + inst.world_origin.x, - local.y + inst.world_origin.y, - 0.0); - o.clip = u.view_rot * vec4((world - u.eye_high) - u.eye_low, 1.0); + // Double-single relative-to-eye: the anchor high half cancels exactly + // against eye_high (Sterbenz); local + anchor low + (−eye_low) carry the + // residual. `local` is small (boundary-relative), so adding it in the low + // term keeps full precision at UTM-scale anchors. + let hi = vec3(inst.world_origin.x - u.eye_high.x, + inst.world_origin.y - u.eye_high.y, + -u.eye_high.z); + let lo = vec3(local.x + inst.world_origin_low.x - u.eye_low.x, + local.y + inst.world_origin_low.y - u.eye_low.y, + -u.eye_low.z); + o.clip = u.view_rot * vec4(hi + lo, 1.0); o.clip.z = o.clip.z - inst.draw_depth * DRAW_ORDER_BIAS * o.clip.w; o.xz = local; o.instance_index = v.instance_index;