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 <noreply@anthropic.com>
This commit is contained in:
parent
6a74f12a92
commit
0f7920dc2c
4 changed files with 71 additions and 29 deletions
|
|
@ -51,26 +51,38 @@ pub struct HatchInstance {
|
||||||
pub color: [f32; 4], // 0
|
pub color: [f32; 4], // 0
|
||||||
pub color2: [f32; 4], // 16 (gradient end)
|
pub color2: [f32; 4], // 16 (gradient end)
|
||||||
pub aabb: [f32; 4], // 32 (local-space xmin,ymin,xmax,ymax)
|
pub aabb: [f32; 4], // 32 (local-space xmin,ymin,xmax,ymax)
|
||||||
pub world_origin: [f32; 2], // 48 (anchor added back in VS)
|
pub world_origin: [f32; 2], // 48 (anchor high half, added back in VS)
|
||||||
pub angle_offset: f32, // 56
|
pub world_origin_low: [f32; 2], // 56 (anchor low residual — double-single)
|
||||||
pub scale: f32, // 60
|
pub angle_offset: f32, // 64
|
||||||
pub grad_cos: f32, // 64
|
pub scale: f32, // 68
|
||||||
pub grad_sin: f32, // 68
|
pub grad_cos: f32, // 72
|
||||||
pub grad_min: f32, // 72
|
pub grad_sin: f32, // 76
|
||||||
pub grad_range: f32, // 76
|
pub grad_min: f32, // 80
|
||||||
pub mode: u32, // 80 (0=pattern, 1=solid, 2=gradient)
|
pub grad_range: f32, // 84
|
||||||
pub visible: u32, // 84 (CPU sets to 0 to skip)
|
pub mode: u32, // 88 (0=pattern, 1=solid, 2=gradient)
|
||||||
pub boundary_offset: u32, // 88 (first boundary vert index)
|
pub visible: u32, // 92 (CPU sets to 0 to skip)
|
||||||
pub boundary_count: u32, // 92
|
pub boundary_offset: u32, // 96 (first boundary vert index)
|
||||||
pub family_offset: u32, // 96
|
pub boundary_count: u32, // 100
|
||||||
pub family_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
|
/// 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.
|
/// bias in the vertex shader so this fill orders against other types.
|
||||||
pub draw_depth: f32, // 104
|
pub draw_depth: f32, // 112
|
||||||
pub _pad0: u32, // 108 (pad to 112 = 16-byte stride)
|
pub _pad0: u32, // 116 (pad to 128 = 16-byte stride)
|
||||||
|
pub _pad1: u32, // 120
|
||||||
|
pub _pad2: u32, // 124
|
||||||
}
|
}
|
||||||
|
|
||||||
const _: () = assert!(std::mem::size_of::<HatchInstance>() == 112);
|
const _: () = assert!(std::mem::size_of::<HatchInstance>() == 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,
|
/// Mirrors the per-family struct used by the existing per-hatch shader,
|
||||||
/// but the dash slice lives in a separate concatenated DashBuffer (the
|
/// but the dash slice lives in a separate concatenated DashBuffer (the
|
||||||
|
|
@ -261,11 +273,13 @@ impl HatchBatchedGpu {
|
||||||
if !min_x.is_finite() {
|
if !min_x.is_finite() {
|
||||||
// Empty / all-NaN — skip but keep the slot so indices
|
// Empty / all-NaN — skip but keep the slot so indices
|
||||||
// stay in lockstep with the input list (visibility=0).
|
// stay in lockstep with the input list (visibility=0).
|
||||||
|
let (wo_hi, wo_lo) = split_origin_ds(h.world_origin);
|
||||||
instances.push(HatchInstance {
|
instances.push(HatchInstance {
|
||||||
color: h.color,
|
color: h.color,
|
||||||
color2,
|
color2,
|
||||||
aabb: [0.0, 0.0, 0.0, 0.0],
|
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,
|
angle_offset: h.angle_offset,
|
||||||
scale: h.scale.max(1e-6),
|
scale: h.scale.max(1e-6),
|
||||||
grad_cos,
|
grad_cos,
|
||||||
|
|
@ -280,6 +294,8 @@ impl HatchBatchedGpu {
|
||||||
family_count,
|
family_count,
|
||||||
draw_depth: h.draw_depth,
|
draw_depth: h.draw_depth,
|
||||||
_pad0: 0,
|
_pad0: 0,
|
||||||
|
_pad1: 0,
|
||||||
|
_pad2: 0,
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -300,11 +316,13 @@ impl HatchBatchedGpu {
|
||||||
};
|
};
|
||||||
let pad = (diag * 0.8 + max_spacing * 2.0 * h.scale).max(1.0);
|
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 {
|
instances.push(HatchInstance {
|
||||||
color: h.color,
|
color: h.color,
|
||||||
color2,
|
color2,
|
||||||
aabb: [min_x - pad, min_y - pad, max_x + pad, max_y + pad],
|
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,
|
angle_offset: h.angle_offset,
|
||||||
scale: h.scale.max(1e-6),
|
scale: h.scale.max(1e-6),
|
||||||
grad_cos,
|
grad_cos,
|
||||||
|
|
@ -319,6 +337,8 @@ impl HatchBatchedGpu {
|
||||||
family_count,
|
family_count,
|
||||||
draw_depth: h.draw_depth,
|
draw_depth: h.draw_depth,
|
||||||
_pad0: 0,
|
_pad0: 0,
|
||||||
|
_pad1: 0,
|
||||||
|
_pad2: 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -58,13 +58,15 @@ pub struct HatchUniformData {
|
||||||
pub grad_sin: f32, // 52: gradient direction sin
|
pub grad_sin: f32, // 52: gradient direction sin
|
||||||
pub grad_min: f32, // 56: gradient proj_min
|
pub grad_min: f32, // 56: gradient proj_min
|
||||||
pub grad_range: f32, // 60: gradient proj_range
|
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
|
// // The pattern fragment shader subtracts this
|
||||||
// // from xz before the perp/perp_step quotient
|
// // from xz before the perp/perp_step quotient
|
||||||
// // so f32 doesn't catastrophically cancel when
|
// // so f32 doesn't catastrophically cancel when
|
||||||
// // world coords are large and pattern spacing
|
// // world coords are large and pattern spacing
|
||||||
// // is small (e.g. UTM drawing + sub-mm hatch).
|
// // 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
|
} // total 80 bytes
|
||||||
|
|
||||||
/// Boundary polygon (binding 1). Matches WGSL `Boundary`.
|
/// Boundary polygon (binding 1). Matches WGSL `Boundary`.
|
||||||
|
|
@ -288,7 +290,10 @@ impl HatchGpu {
|
||||||
grad_min,
|
grad_min,
|
||||||
grad_range,
|
grad_range,
|
||||||
origin: [origin[0] as f32, origin[1] as f32],
|
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) ────────────────────────
|
// ── BoundaryData (in snapped-local space) ────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ struct HatchUniforms {
|
||||||
// // `xz - origin` so the f32 modulo doesn't
|
// // `xz - origin` so the f32 modulo doesn't
|
||||||
// // catastrophically cancel when world coords
|
// // catastrophically cancel when world coords
|
||||||
// // are large and pattern spacing is small.
|
// // are large and pattern spacing is small.
|
||||||
_pad: vec2<f32>, // 72: 16-byte alignment
|
origin_low: vec2<f32>, // 72: anchor low residual (double-single)
|
||||||
}
|
}
|
||||||
@group(1) @binding(0) var<uniform> h: HatchUniforms;
|
@group(1) @binding(0) var<uniform> h: HatchUniforms;
|
||||||
|
|
||||||
|
|
@ -104,8 +104,15 @@ struct VOut {
|
||||||
// jitter is invisible. The local-space `xz` we export to the
|
// jitter is invisible. The local-space `xz` we export to the
|
||||||
// fragment shader stays at full f32 precision so the in_polygon /
|
// fragment shader stays at full f32 precision so the in_polygon /
|
||||||
// pattern math doesn't catastrophically cancel.
|
// pattern math doesn't catastrophically cancel.
|
||||||
let world = vec3<f32>(v.pos.x + h.origin.x, v.pos.y + h.origin.y, v.pos.z);
|
// Double-single relative-to-eye: anchor high cancels eye_high (Sterbenz);
|
||||||
o.clip = u.view_rot * vec4<f32>((world - u.eye_high) - u.eye_low, 1.0);
|
// boundary-local v.pos + anchor low + (−eye_low) carry the residual.
|
||||||
|
let hi = vec3<f32>(h.origin.x - u.eye_high.x,
|
||||||
|
h.origin.y - u.eye_high.y,
|
||||||
|
-u.eye_high.z);
|
||||||
|
let lo = vec3<f32>(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<f32>(hi + lo, 1.0);
|
||||||
o.xz = vec2<f32>(v.pos.x, v.pos.y);
|
o.xz = vec2<f32>(v.pos.x, v.pos.y);
|
||||||
return o;
|
return o;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,8 @@ struct HatchInstance {
|
||||||
color: vec4<f32>,
|
color: vec4<f32>,
|
||||||
color2: vec4<f32>,
|
color2: vec4<f32>,
|
||||||
aabb: vec4<f32>, // (xmin, ymin, xmax, ymax) — local space
|
aabb: vec4<f32>, // (xmin, ymin, xmax, ymax) — local space
|
||||||
world_origin: vec2<f32>,
|
world_origin: vec2<f32>, // anchor high half
|
||||||
|
world_origin_low: vec2<f32>, // anchor low residual (double-single)
|
||||||
angle_offset: f32,
|
angle_offset: f32,
|
||||||
scale: f32,
|
scale: f32,
|
||||||
grad_cos: f32,
|
grad_cos: f32,
|
||||||
|
|
@ -56,6 +57,8 @@ struct HatchInstance {
|
||||||
family_count: u32,
|
family_count: u32,
|
||||||
draw_depth: f32, // signed (-1,1) draw-order bias; 0 = neutral
|
draw_depth: f32, // signed (-1,1) draw-order bias; 0 = neutral
|
||||||
_pad0: u32,
|
_pad0: u32,
|
||||||
|
_pad1: u32,
|
||||||
|
_pad2: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw-order depth bias (see wire.wgsl). Higher draw_depth → smaller z →
|
// Draw-order depth bias (see wire.wgsl). Higher draw_depth → smaller z →
|
||||||
|
|
@ -132,10 +135,17 @@ fn corner_xy(c: u32, aabb: vec4<f32>) -> vec2<f32> {
|
||||||
}
|
}
|
||||||
|
|
||||||
let local = corner_xy(v.corner, inst.aabb);
|
let local = corner_xy(v.corner, inst.aabb);
|
||||||
let world = vec3<f32>(local.x + inst.world_origin.x,
|
// Double-single relative-to-eye: the anchor high half cancels exactly
|
||||||
local.y + inst.world_origin.y,
|
// against eye_high (Sterbenz); local + anchor low + (−eye_low) carry the
|
||||||
0.0);
|
// residual. `local` is small (boundary-relative), so adding it in the low
|
||||||
o.clip = u.view_rot * vec4<f32>((world - u.eye_high) - u.eye_low, 1.0);
|
// term keeps full precision at UTM-scale anchors.
|
||||||
|
let hi = vec3<f32>(inst.world_origin.x - u.eye_high.x,
|
||||||
|
inst.world_origin.y - u.eye_high.y,
|
||||||
|
-u.eye_high.z);
|
||||||
|
let lo = vec3<f32>(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<f32>(hi + lo, 1.0);
|
||||||
o.clip.z = o.clip.z - inst.draw_depth * DRAW_ORDER_BIAS * o.clip.w;
|
o.clip.z = o.clip.z - inst.draw_depth * DRAW_ORDER_BIAS * o.clip.w;
|
||||||
o.xz = local;
|
o.xz = local;
|
||||||
o.instance_index = v.instance_index;
|
o.instance_index = v.instance_index;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue