forked from 1b-agent/opencadstudio-upstream
fix(hatch): clip fill with kernel mesh
Resolve boundary topology before rendering so corner-aligned pattern rows cannot flip shader parity. Chunk indexed geometry to device buffer limits and retain the texture compatibility transport. Refs #489
This commit is contained in:
1 parent
448869ac35
commit
5b9fffdfba
9 files changed
+259
-416
No files matched your search
+12
-91
@@ -1,23 +1,10 @@
|
||||
// Phase 4-B — batched hatch shader. All hatches in one draw call;
|
||||
// per-instance data fetched from storage buffers indexed by the
|
||||
// `instance_index` vertex attribute (passed from the per-vertex
|
||||
// (local_xy, instance_index) stream of tessellated boundary-triangle
|
||||
// positions so we don't depend on @builtin(instance_index) edge cases
|
||||
// across backends).
|
||||
// Batched hatch shader. Kernel mesh vertices carry their source instance.
|
||||
//
|
||||
// Layout — matches `hatch_gpu/storage.rs`:
|
||||
// group 1 binding 0 InstanceBuffer HatchInstance[] (128 B / inst)
|
||||
// group 1 binding 1 BoundaryBuffer vec4<f32>[] (xy in .xy)
|
||||
// group 1 binding 2 FamilyBuffer LineFamilyGpu[] (48 B / fam)
|
||||
// group 1 binding 3 DashBuffer f32[]
|
||||
//
|
||||
// The vertex shader emits tessellated boundary triangles in local space
|
||||
// (an instance whose boundary failed to tessellate falls back to an
|
||||
// AABB quad with `poly_test == 1`, so the fragment shader still runs
|
||||
// `in_polygon` to clip it to the real shape). A `visible == 0` instance
|
||||
// gets an out-of-NDC clip position so the fragment shader never runs
|
||||
// for it — that's the GPU-side cull (Phase 4-B equivalent of
|
||||
// `compute_hatch_lod` writing `hatch_skip_flags`).
|
||||
// group 1 binding 0 InstanceBuffer HatchInstance[]
|
||||
// group 1 binding 1 FamilyBuffer LineFamilyGpu[]
|
||||
// group 1 binding 2 DashBuffer f32[]
|
||||
// group 1 binding 3 Visibility u32[]
|
||||
|
||||
// ── Group 0: shared frame uniforms (matches hatch.wgsl) ──────────────────
|
||||
|
||||
@@ -53,14 +40,10 @@ struct HatchInstance {
|
||||
grad_range: f32,
|
||||
mode: u32, // 0=pattern, 1=solid, 2=gradient
|
||||
visible: u32, // 0 = skip (CPU writes via compute_hatch_lod)
|
||||
boundary_offset: u32,
|
||||
boundary_count: u32,
|
||||
family_offset: u32,
|
||||
family_count: u32,
|
||||
draw_depth: f32, // signed (-1,1) draw-order bias; 0 = neutral
|
||||
poly_test: u32, // 1 = run in_polygon (fallback), 0 = skip
|
||||
grad_kind: u32, // shape (0=linear,1=cyl,2=sph,3=hemi,4=curved), bit4=invert
|
||||
_pad2: u32,
|
||||
}
|
||||
|
||||
// Draw-order depth bias (see wire.wgsl). Higher draw_depth → smaller z →
|
||||
@@ -83,14 +66,13 @@ struct LineFamily {
|
||||
}
|
||||
|
||||
@group(1) @binding(0) var<storage, read> instances: array<HatchInstance>;
|
||||
@group(1) @binding(1) var<storage, read> boundary: array<vec4<f32>>;
|
||||
@group(1) @binding(2) var<storage, read> families: array<LineFamily>;
|
||||
@group(1) @binding(3) var<storage, read> dashes: array<f32>;
|
||||
@group(1) @binding(1) var<storage, read> families: array<LineFamily>;
|
||||
@group(1) @binding(2) var<storage, read> dashes: array<f32>;
|
||||
// Per-instance visibility (Phase 4-B sub-pixel + frustum skip).
|
||||
// CPU writes `1` to draw / `0` to skip every frame; vertex shader
|
||||
// emits an out-of-NDC clip position for 0-instances so the GPU
|
||||
// rasterizer culls the primitive before any fragment runs.
|
||||
@group(1) @binding(4) var<storage, read> visibility: array<u32>;
|
||||
@group(1) @binding(3) var<storage, read> visibility: array<u32>;
|
||||
|
||||
// ── Vertex shader ────────────────────────────────────────────────────────
|
||||
|
||||
@@ -144,58 +126,6 @@ struct VOut {
|
||||
return o;
|
||||
}
|
||||
|
||||
// ── Point-in-polygon (ray casting) over a sub-range of BoundaryBuffer ────
|
||||
|
||||
fn valid_vertex(p: vec2<f32>) -> bool {
|
||||
// Sub-loop separators are a huge finite sentinel (1e30, see
|
||||
// GPU_BOUNDARY_SEP), not NaN: `x == x` NaN detection gets folded to
|
||||
// `true` by some drivers (Intel Mesa fast math), which made separators
|
||||
// read as real vertices and bleed fills past their boundary (#386, #416).
|
||||
return abs(p.x) < 1.0e29 && abs(p.y) < 1.0e29;
|
||||
}
|
||||
|
||||
fn edge_crosses(p: vec2<f32>, a: vec2<f32>, c: vec2<f32>) -> bool {
|
||||
if (a.y > p.y) != (c.y > p.y) {
|
||||
let x_int = (c.x - a.x) * (p.y - a.y) / (c.y - a.y) + a.x;
|
||||
return p.x < x_int;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
fn in_polygon(p: vec2<f32>, offset: u32, count: u32) -> bool {
|
||||
var inside = false;
|
||||
var prev = vec2<f32>(0.0, 0.0);
|
||||
var first = vec2<f32>(0.0, 0.0);
|
||||
var have_prev = false;
|
||||
for (var i = 0u; i < count; i++) {
|
||||
let vi = boundary[offset + i].xy;
|
||||
if !valid_vertex(vi) {
|
||||
// Close the sub-loop that just ended (last → first edge). An
|
||||
// unclosed boundary — e.g. a SOLID's 4 corners, which are not
|
||||
// repeated — otherwise miscounts crossings and the fill bleeds
|
||||
// outside the shape. (#140)
|
||||
if have_prev && edge_crosses(p, prev, first) {
|
||||
inside = !inside;
|
||||
}
|
||||
have_prev = false;
|
||||
continue;
|
||||
}
|
||||
if have_prev {
|
||||
if edge_crosses(p, prev, vi) {
|
||||
inside = !inside;
|
||||
}
|
||||
} else {
|
||||
first = vi;
|
||||
}
|
||||
prev = vi;
|
||||
have_prev = true;
|
||||
}
|
||||
if have_prev && edge_crosses(p, prev, first) {
|
||||
inside = !inside;
|
||||
}
|
||||
return inside;
|
||||
}
|
||||
|
||||
// ── Per-family hatch test (same math as hatch.wgsl, dashes from
|
||||
// global DashBuffer instead of per-hatch FamilyBatch) ────────────────────
|
||||
|
||||
@@ -282,15 +212,7 @@ fn check_family(
|
||||
|
||||
let inst = instances[v.instance_index];
|
||||
|
||||
// 1. Boundary test — only on the fallback path (poly_test==1). On the
|
||||
// tessellated fast path the triangles already bound the fill.
|
||||
if inst.poly_test == 1u {
|
||||
if !in_polygon(v.xz, inst.boundary_offset, inst.boundary_count) {
|
||||
discard;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Mode dispatch.
|
||||
// Mode dispatch.
|
||||
if inst.mode == 1u {
|
||||
return inst.color;
|
||||
} else if inst.mode == 2u {
|
||||
@@ -319,12 +241,11 @@ fn check_family(
|
||||
t = 1.0 - t;
|
||||
}
|
||||
// Radial stops run OUTSIDE-IN: colour 1 at the rim, colour 2 at the
|
||||
// centre (AutoCAD's SPHERICAL/HEMISPHERICAL convention; the INV bit
|
||||
// above swaps them back).
|
||||
// centre; the INV bit above swaps them back.
|
||||
return mix(inst.color2, inst.color, t);
|
||||
}
|
||||
|
||||
// 3. Pattern LOD. Keep every family visible until all family spacings
|
||||
// Pattern LOD. Keep every family visible until all family spacings
|
||||
// project below 2 px, then substitute one solid fill. A single dense
|
||||
// family must neither hide itself nor turn a complex hatch solid.
|
||||
var all_families_subpixel = inst.family_count > 0u && u.world_per_pixel > 0.0;
|
||||
@@ -343,7 +264,7 @@ fn check_family(
|
||||
return inst.color;
|
||||
}
|
||||
|
||||
// 4. Pattern evaluation.
|
||||
// Pattern evaluation.
|
||||
let cos_off = cos(inst.angle_offset);
|
||||
let sin_off = sin(inst.angle_offset);
|
||||
for (var i = 0u; i < inst.family_count; i++) {
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
// Hatch texture shader — storage-free, UNCAPPED variant of wipeout.wgsl.
|
||||
//
|
||||
// Devices without vertex/fragment storage buffers cannot use the batched
|
||||
// storage-buffer renderer (hatch.wgsl). This shader keeps the exact
|
||||
// wipeout.wgsl hatch algorithm (in_polygon + per-family line/dash/dot + solid +
|
||||
// gradient) but reads the variable-length boundary / family / dash arrays from a
|
||||
// single RGBA32F data texture via textureLoad instead of fixed-size uniforms —
|
||||
// removing the MAX_FAMILIES (16) / MAX_HATCH_BOUNDARY_VERTS (1024) / MAX_DASHES
|
||||
// (128) caps of the uniform path.
|
||||
// Texture-backed hatch shader. The kernel mesh bounds the fill; this shader
|
||||
// evaluates only the pattern, solid, or gradient colour.
|
||||
//
|
||||
// Data texture layout (row-major, width = h.tex_width, one vec4 per texel):
|
||||
// [ 0 .. vcount ) boundary verts, .xy = local XY, NaN = break
|
||||
// [ fam_off .. fam_off+3*n_fam ) families, 3 texels each (see load_family)
|
||||
// [ dash_off .. ) dash values, 4 per texel (RGBA)
|
||||
|
||||
@@ -36,7 +28,7 @@ struct HatchUniforms {
|
||||
color: vec4<f32>, // 0
|
||||
color2: vec4<f32>, // 16
|
||||
mode: u32, // 32: 0=pattern, 1=solid, 2=gradient
|
||||
vcount: u32, // 36: boundary vertex count
|
||||
_reserved: u32, // 36
|
||||
angle_offset: f32, // 40
|
||||
scale: f32, // 44
|
||||
grad_cos: f32, // 48
|
||||
@@ -119,56 +111,6 @@ struct VOut {
|
||||
return o;
|
||||
}
|
||||
|
||||
// ── Point-in-polygon (ray casting) ────────────────────────────────────────
|
||||
|
||||
fn valid_vertex(p: vec2<f32>) -> bool {
|
||||
// Sub-loop separators are a huge finite sentinel (1e30, see
|
||||
// GPU_BOUNDARY_SEP), not NaN: `x == x` NaN detection gets folded to
|
||||
// `true` by some drivers (Intel Mesa fast math), which made separators
|
||||
// read as real vertices and bleed fills past their boundary (#386, #416).
|
||||
return abs(p.x) < 1.0e29 && abs(p.y) < 1.0e29;
|
||||
}
|
||||
|
||||
fn edge_crosses(p: vec2<f32>, a: vec2<f32>, c: vec2<f32>) -> bool {
|
||||
if (a.y > p.y) != (c.y > p.y) {
|
||||
let x_int = (c.x - a.x) * (p.y - a.y) / (c.y - a.y) + a.x;
|
||||
return p.x < x_int;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
fn in_polygon(p: vec2<f32>) -> bool {
|
||||
var inside = false;
|
||||
let n = h.vcount;
|
||||
var prev = vec2<f32>(0.0, 0.0);
|
||||
var first = vec2<f32>(0.0, 0.0);
|
||||
var have_prev = false;
|
||||
for (var i = 0u; i < n; i++) {
|
||||
let vi = texel(i).xy;
|
||||
if !valid_vertex(vi) {
|
||||
// NaN sentinel closes the current sub-loop (last → first edge). (#140)
|
||||
if have_prev && edge_crosses(p, prev, first) {
|
||||
inside = !inside;
|
||||
}
|
||||
have_prev = false;
|
||||
continue;
|
||||
}
|
||||
if have_prev {
|
||||
if edge_crosses(p, prev, vi) {
|
||||
inside = !inside;
|
||||
}
|
||||
} else {
|
||||
first = vi;
|
||||
}
|
||||
prev = vi;
|
||||
have_prev = true;
|
||||
}
|
||||
if have_prev && edge_crosses(p, prev, first) {
|
||||
inside = !inside;
|
||||
}
|
||||
return inside;
|
||||
}
|
||||
|
||||
// ── Per-family hatch test (identical math to wipeout.wgsl) ─────────────────
|
||||
|
||||
// `ddx_xz`/`ddy_xz` are screen-space derivatives of `xz`, taken once in
|
||||
@@ -244,8 +186,6 @@ fn check_family(
|
||||
let ddx_xz = dpdx(v.xz);
|
||||
let ddy_xz = dpdy(v.xz);
|
||||
|
||||
if !in_polygon(v.xz) { discard; }
|
||||
|
||||
let base_mode = h.mode & 0xFFu;
|
||||
let gk = (h.mode >> 8u) & 15u;
|
||||
let ginv = ((h.mode >> 8u) & 16u) != 0u;
|
||||
|
||||
Reference in new issue
Block a user