Files
OneCAD-Draft/src/shaders/hatch_web.wgsl
T
Hakan SevenandClaude Opus 4.8 fc29139056 fix(hatch): radial gradients and correct gradient stop colours
A gradient hatch was drawn linear from the entity colour to its second
stop, so a red->yellow spherical fill came out as a pale linear wash. Two
fixes:
- Colour: start from the gradient's first stop, not the entity colour.
  hatch_model_from_dxf already baked stop-0 in; stop synced_hatch_models
  from clobbering it with the entity's resolved colour for gradient fills.
- Shape: spherical / hemispherical / curved gradient names now render as a
  radial fill (new shader mode 3, centred on the boundary with radius to the
  farthest vertex) instead of linear. Native and web shaders both updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 22:01:22 +03:00

264 lines
9.3 KiB
WebGPU Shading Language
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Hatch shader (WebGL2) — texture-backed, UNCAPPED variant of wipeout.wgsl.
//
// WebGL2 has no vertex/fragment storage buffers, so the batched storage-buffer
// renderer (hatch.wgsl) is disabled on wasm. 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. Native (hatch.wgsl) is untouched.
//
// 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)
// ── Group 0: frame uniforms (shared) ──────────────────────────────────────
struct Uniforms {
viewport_size: vec2<f32>,
world_per_pixel: f32,
lwdisplay_enable: f32,
flat_shade: f32,
transparency_enable: f32,
_pad: vec2<f32>,
view_rot: mat4x4<f32>,
eye_high: vec3<f32>,
_pad_eh: f32,
eye_low: vec3<f32>,
_pad_el: f32,
}
@group(0) @binding(0) var<uniform> u: Uniforms;
// ── Group 1: per-hatch data ────────────────────────────────────────────────
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
angle_offset: f32, // 40
scale: f32, // 44
grad_cos: f32, // 48
grad_sin: f32, // 52
grad_min: f32, // 56
grad_range: f32, // 60
origin: vec2<f32>, // 64
origin_low: vec2<f32>, // 72
n_families: u32, // 80
fam_off: u32, // 84: texel offset of the family section
dash_off: u32, // 88: texel offset of the dash section
tex_width: u32, // 92: data texture width in texels
}
@group(1) @binding(0) var<uniform> h: HatchUniforms;
@group(1) @binding(1) var data_tex: texture_2d<f32>;
// One line family, reconstructed from 3 texels.
struct LineFamily {
cos_a: f32,
sin_a: f32,
x0: f32,
y0: f32,
dx: f32,
dy: f32,
perp_step: f32,
along_step: f32,
line_width: f32,
period: f32,
n_dashes: u32,
dash_off: u32,
}
fn texel(i: u32) -> vec4<f32> {
let w = h.tex_width;
return textureLoad(data_tex, vec2<u32>(i % w, i / w), 0);
}
fn load_family(fi: u32) -> LineFamily {
let base = h.fam_off + fi * 3u;
let t0 = texel(base);
let t1 = texel(base + 1u);
let t2 = texel(base + 2u);
var f: LineFamily;
f.cos_a = t0.x; f.sin_a = t0.y; f.x0 = t0.z; f.y0 = t0.w;
f.dx = t1.x; f.dy = t1.y; f.perp_step = t1.z; f.along_step = t1.w;
f.line_width = t2.x; f.period = t2.y;
// Counts stored as exact f32 (small integers, so no denormal/bitcast hazard
// through the float texture fetch).
f.n_dashes = u32(t2.z); f.dash_off = u32(t2.w);
return f;
}
// ── Vertex shader (identical to wipeout.wgsl) ──────────────────────────────
struct VIn { @location(0) pos: vec3<f32> }
struct VOut {
@builtin(position) clip: vec4<f32>,
@location(0) xz: vec2<f32>,
}
@vertex fn vs_main(v: VIn) -> VOut {
var o: VOut;
// 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<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);
return o;
}
// ── Point-in-polygon (ray casting) ────────────────────────────────────────
fn valid_vertex(p: vec2<f32>) -> bool {
return p.x == p.x && p.y == p.y;
}
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) ─────────────────
fn check_family(
xz: vec2<f32>,
fam: LineFamily,
cos_off: f32,
sin_off: f32,
scale: f32,
) -> bool {
let cos_a = fam.cos_a * cos_off - fam.sin_a * sin_off;
let sin_a = fam.sin_a * cos_off + fam.cos_a * sin_off;
let ox = (fam.x0 * cos_off - fam.y0 * sin_off) * scale;
let oz = (fam.x0 * sin_off + fam.y0 * cos_off) * scale;
let px = xz.x - ox;
let pz = xz.y - oz;
let perp_step = fam.perp_step * scale;
let perp = -px * sin_a + pz * cos_a;
let k = round(perp / perp_step);
let dperp = perp - k * perp_step;
let d = abs(dperp);
let half_px = length(vec2<f32>(dpdx(perp), dpdy(perp))) * 0.5;
let wpx = length(vec2<f32>(dpdx(xz.x), dpdy(xz.x)));
let wpy = length(vec2<f32>(dpdx(xz.y), dpdy(xz.y)));
if d > half_px * 2.0 { return false; }
if fam.n_dashes == 0u { return d <= half_px; }
let along_step = fam.along_step * scale;
let period = fam.period * scale;
let along = px * cos_a + pz * sin_a;
let t = along - k * along_step;
let t_mod = ((t % period) + period) % period;
var pos = 0.0;
for (var j = 0u; j < fam.n_dashes; j++) {
let idx = fam.dash_off + j;
let sv = texel(h.dash_off + idx / 4u)[idx % 4u] * scale;
if sv > 0.0 {
if d <= half_px && t_mod >= pos && t_mod < pos + sv { return true; }
pos = pos + sv;
} else if sv < 0.0 {
pos = pos - sv;
} else {
let dtv = (t - pos) - round((t - pos) / period) * period;
let owx = -dtv * cos_a + dperp * sin_a;
let owy = -dtv * sin_a - dperp * cos_a;
if abs(owx / wpx) <= 0.5 && abs(owy / wpy) <= 0.5 { return true; }
}
}
return false;
}
// ── Fragment shader ────────────────────────────────────────────────────────
@fragment fn fs_main(v: VOut) -> @location(0) vec4<f32> {
if !in_polygon(v.xz) { discard; }
if h.mode == 1u {
return h.color;
} else if h.mode == 2u {
let proj = v.xz.x * h.grad_cos + v.xz.y * h.grad_sin;
let t = clamp((proj - h.grad_min) / h.grad_range, 0.0, 1.0);
return mix(h.color, h.color2, t);
} else if h.mode == 3u {
// Radial gradient: centre is (grad_cos, grad_sin), radius is grad_range.
let d = length(v.xz - vec2<f32>(h.grad_cos, h.grad_sin));
let t = clamp(d / h.grad_range, 0.0, 1.0);
return mix(h.color, h.color2, t);
}
// Pattern LOD: when the densest family projects below 2 px, lines blur into
// a solid fill — return solid instead of looping every family.
if u.world_per_pixel > 0.0 && h.n_families > 0u {
var min_spacing_world: f32 = 1.0e30;
for (var i = 0u; i < h.n_families; i++) {
let s = abs(load_family(i).perp_step) * h.scale;
if s > 0.0 && s < min_spacing_world {
min_spacing_world = s;
}
}
if min_spacing_world / u.world_per_pixel < 2.0 {
return h.color;
}
}
let cos_off = cos(h.angle_offset);
let sin_off = sin(h.angle_offset);
for (var i = 0u; i < h.n_families; i++) {
if check_family(v.xz, load_family(i), cos_off, sin_off, h.scale) {
return h.color;
}
}
discard;
// Unreachable, but FXC/DX12 requires a return after every terminal discard.
return vec4<f32>(0.0);
}