Files
OneCAD-Draft/src/shaders/hatch.wgsl
T

299 lines
12 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.
// Batched hatch shader. Kernel mesh vertices carry their source instance.
//
// Layout — matches `hatch_gpu/storage.rs`:
// 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) ──────────────────
struct Uniforms {
viewport_size: vec2<f32>,
world_per_pixel: f32,
lwdisplay_enable: f32,
flat_shade: f32,
transparency_enable: f32,
_linetype_scale: f32,
lineweight_scale: f32,
// Relative-to-eye (double-single): see wire.wgsl.
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: batched hatch storage ───────────────────────────────────────
struct HatchInstance {
color: vec4<f32>,
color2: vec4<f32>,
aabb: vec4<f32>, // (xmin, ymin, xmax, ymax) — local space
world_origin: vec2<f32>, // anchor high half
world_origin_low: vec2<f32>, // anchor low residual (double-single)
angle_offset: f32,
scale: f32,
grad_cos: f32,
grad_sin: f32,
grad_min: f32,
grad_range: f32,
mode: u32, // 0=pattern, 1=solid, 2=gradient
visible: u32, // 0 = skip (CPU writes via compute_hatch_lod)
family_offset: u32,
family_count: u32,
draw_depth: f32, // signed (-1,1) draw-order bias; 0 = neutral
grad_kind: u32, // shape (0=linear,1=cyl,2=sph,3=hemi,4=curved), bit4=invert
}
// Draw-order depth bias (see wire.wgsl). Higher draw_depth → smaller z →
// drawn on top, ordering this fill against other entity types.
const DRAW_ORDER_BIAS: f32 = 0.001;
const MODEL_LINEWEIGHT_BOOST: f32 = 2.0;
const MODEL_LINEWEIGHT_MAX_PX: f32 = 10.0;
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_offset: u32,
}
@group(1) @binding(0) var<storage, read> instances: array<HatchInstance>;
@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(3) var<storage, read> visibility: array<u32>;
// ── Vertex shader ────────────────────────────────────────────────────────
struct VIn {
@location(0) local_xy: vec2<f32>,
@location(1) instance_index: u32,
@location(2) translation: vec2<f32>,
@location(3) translation_low: vec2<f32>,
@location(4) draw_depth: f32,
@location(5) visible: u32,
}
struct VOut {
@builtin(position) clip: vec4<f32>,
@location(0) xz: vec2<f32>,
@location(1) @interpolate(flat) instance_index: u32,
}
@vertex fn vs_main(v: VIn) -> VOut {
var o: VOut;
let inst = instances[v.instance_index];
// Per-frame visibility (CPU-driven sub-pixel + frustum skip).
// 0 → emit a clip position whose x/y exceed |w| so the GPU
// frustum-culls the primitive and no fragment runs. (WGSL
// forbids literal NaN so this out-of-NDC trick replaces the
// usual NaN-degenerate-triangle.)
if visibility[v.instance_index] == 0u || v.visible == 0u {
o.clip = vec4<f32>(2.0, 2.0, 2.0, 1.0);
o.xz = vec2<f32>(0.0, 0.0);
o.instance_index = v.instance_index;
return o;
}
let local = v.local_xy;
// 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<f32>(inst.world_origin.x + v.translation.x - u.eye_high.x,
inst.world_origin.y + v.translation.y - u.eye_high.y,
-u.eye_high.z);
let lo = vec3<f32>(local.x + inst.world_origin_low.x + v.translation_low.x - u.eye_low.x,
local.y + inst.world_origin_low.y + v.translation_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 + v.draw_depth) * DRAW_ORDER_BIAS * o.clip.w;
o.xz = local;
o.instance_index = v.instance_index;
return o;
}
// ── Per-family hatch test (same math as hatch.wgsl, dashes from
// global DashBuffer instead of per-hatch FamilyBatch) ────────────────────
// `ddx_xz`/`ddy_xz` are screen-space derivatives of `xz`, taken once in
// fs_main: derivative builtins must run in uniform control flow, and the
// per-family loop's early return makes later iterations non-uniform.
fn display_lineweight(width: f32) -> f32 {
if u.lineweight_scale < 0.0 {
let base = select(
min(width * MODEL_LINEWEIGHT_BOOST, MODEL_LINEWEIGHT_MAX_PX),
1.0,
width <= 1.0,
);
return max(base * -u.lineweight_scale, 1.0);
}
return max(width * u.lineweight_scale, 1.0);
}
fn check_family(
xz: vec2<f32>,
ddx_xz: vec2<f32>,
ddy_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);
// perp is linear in xz (offsets are constant), so its derivatives are the
// xz derivatives rotated into the family frame.
let half_px = length(vec2<f32>(
-ddx_xz.x * sin_a + ddx_xz.y * cos_a,
-ddy_xz.x * sin_a + ddy_xz.y * cos_a,
)) * 0.5;
let width_px = select(1.0, display_lineweight(fam.line_width), u.lwdisplay_enable > 0.5);
let half_line = half_px * width_px;
// World units per screen pixel on each axis — used to light exactly the
// one pixel that contains a dot's centre (pixel-snapped, so the dot stays
// a steady single pixel at any pattern angle instead of flickering).
let wpx = length(vec2<f32>(ddx_xz.x, ddy_xz.x));
let wpy = length(vec2<f32>(ddx_xz.y, ddy_xz.y));
// A fragment within ~1px of a line may be a dot; everything further out is
// empty fill. (A dot's pixel sits on a line, so its perp offset is < 1px.)
if d > max(half_line, half_px * 2.0) { return false; }
if fam.n_dashes == 0u { return d <= half_line; }
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 sv = dashes[fam.dash_offset + j] * scale;
if sv > 0.0 {
if d <= half_line && t_mod >= pos && t_mod < pos + sv { return true; }
pos = pos + sv;
} else if sv < 0.0 {
pos = pos - sv;
} else {
// Dot: signed distance to its lattice centre (along the line and
// across lines), rotated back to world, then snapped to the pixel
// grid. The dot grid rotates with the pattern; the lit pixel does
// not, so it never thins/flickers.
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;
let dot_half = width_px * 0.5;
if abs(owx / wpx) <= dot_half && abs(owy / wpy) <= dot_half { return true; }
}
}
return false;
}
// ── Fragment shader ──────────────────────────────────────────────────────
@fragment fn fs_main(v: VOut) -> @location(0) vec4<f32> {
// Taken here, in uniform control flow — see check_family.
let ddx_xz = dpdx(v.xz);
let ddy_xz = dpdy(v.xz);
let inst = instances[v.instance_index];
// Mode dispatch.
if inst.mode == 1u {
return inst.color;
} else if inst.mode == 2u {
let proj = v.xz.x * inst.grad_cos + v.xz.y * inst.grad_sin;
var t = clamp((proj - inst.grad_min) / inst.grad_range, 0.0, 1.0);
// Shape profile: cylinder mirrors around the middle, curved eases in.
let k = inst.grad_kind & 15u;
if k == 1u {
t = 1.0 - abs(2.0 * t - 1.0);
} else if k == 4u {
t = t * t;
}
if (inst.grad_kind & 16u) != 0u {
t = 1.0 - t;
}
return mix(inst.color, inst.color2, t);
} else if inst.mode == 3u {
// Radial gradient: centre is (grad_cos, grad_sin), radius is grad_range.
let d = length(v.xz - vec2<f32>(inst.grad_cos, inst.grad_sin));
var t = clamp(d / inst.grad_range, 0.0, 1.0);
// Hemispherical shades faster near the centre (dome profile).
if (inst.grad_kind & 15u) == 3u {
t = sqrt(t);
}
if (inst.grad_kind & 16u) != 0u {
t = 1.0 - t;
}
// Radial stops run OUTSIDE-IN: colour 1 at the rim, colour 2 at the
// centre; the INV bit above swaps them back.
return mix(inst.color2, inst.color, t);
}
// 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;
if all_families_subpixel {
for (var i = 0u; i < inst.family_count; i++) {
let spacing_world =
abs(families[inst.family_offset + i].perp_step) * inst.scale;
if spacing_world <= 0.0 {
all_families_subpixel = false;
} else if spacing_world / u.world_per_pixel >= 2.0 {
all_families_subpixel = false;
}
}
}
if all_families_subpixel {
return inst.color;
}
// 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++) {
let fam = families[inst.family_offset + i];
if check_family(v.xz, ddx_xz, ddy_xz, fam, cos_off, sin_off, inst.scale) {
return inst.color;
}
}
discard;
// Unreachable: `discard` kills the fragment before this runs, but
// DX12/FXC reports E_FAIL X3507 ("not all control paths return a
// value") without an explicit return after every terminal discard.
return vec4<f32>(0.0);
}