Files
OneCAD-Draft/src/shaders/block_wire_storage.wgsl
T
Hakan SevenandClaude Opus 5.5 38d81bf838 feat(selection): highlight opacity and extra width (#1823)
Options > Selection gains "Selection highlight opacity" and "Selection
highlight extra width". Below full opacity the object's own colour
shows through the highlight. The selection overlay draws with its own
copy of the frame uniforms: transparency on, and every line widened by
the extra width. Drawings opened at startup now get the selection
colour settings too; they used to keep the defaults until a setting
changed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-16 01:07:23 +03:00

380 lines
14 KiB
WebGPU Shading Language

// Block-wire vertex shader, storage-buffer variant.
//
// `block_wire.wgsl` carries the segment endpoints as vertex attributes, and a
// segment must reach the shader once per corner of its quad, so every record
// is written to VRAM six times: 360 bytes a segment. On a block-heavy drawing
// that is the buffer that runs a 2 GB card out of memory — the OOM names
// `block_wire.vertices` directly.
//
// Here the segments live in a read-only storage buffer indexed by
// `vertex_index / 6`, so each record is stored once: 60 bytes a segment. The
// two shaders are otherwise identical, and must stay so.
//
// `Segment` is declared with scalar fields rather than `vec3<f32>`, which
// would align to 16 bytes in storage and reintroduce padding. All-f32 members
// give it align 4 and stride 60, matching `BlockWireVertex` byte for byte.
//
// Requires storage buffers in the vertex stage; callers select
// `block_wire.wgsl` when `DeviceCapabilities::supports_wire_storage` is false.
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,
view_rot: mat4x4<f32>,
eye_high: vec3<f32>,
highlight_widen: f32,
eye_low: vec3<f32>,
_pad_el: f32,
}
@group(0) @binding(0) var<uniform> u: Uniforms;
struct WireConst {
color: vec4<f32>,
pat0: vec4<f32>,
pat1: vec4<f32>,
half_width: f32,
pattern_length: f32,
draw_depth: f32,
align_end: f32,
align_total: f32,
world_half_width: f32,
is_tapered: f32,
_pad2: f32,
marker_origin_high: vec4<f32>,
marker_origin_low: vec4<f32>,
marker_normal_scale: vec4<f32>,
}
@group(1) @binding(0) var<uniform> wire_const: WireConst;
struct Segment {
pos_a_x: f32, pos_a_y: f32, pos_a_z: f32,
pos_a_low_x: f32, pos_a_low_y: f32, pos_a_low_z: f32,
pos_b_x: f32, pos_b_y: f32, pos_b_z: f32,
pos_b_low_x: f32, pos_b_low_y: f32, pos_b_low_z: f32,
distance_a: f32, distance_b: f32,
// Two `u16` unorms packed into one word, as `Unorm16x2` delivered them.
taper_ratio: u32,
}
@group(1) @binding(1) var<storage, read> segments: array<Segment>;
struct VertexIn {
@location(6) translation: vec3<f32>,
@location(7) translation_low: vec3<f32>,
@location(8) depth: vec2<f32>,
}
const MODEL_LINEWEIGHT_BOOST: f32 = 2.0;
const MODEL_LINEWEIGHT_MAX_PX: f32 = 10.0;
// Must match `MITER_LIMIT` in wire_gpu.rs.
const MITER_LIMIT: f32 = 8.0;
struct VertexOut {
@builtin(position) clip_pos: vec4<f32>,
@location(0) color: vec4<f32>,
@location(1) distance: f32,
@location(2) pattern_length: f32,
@location(3) pat0: vec4<f32>,
@location(4) pat1: vec4<f32>,
@location(5) @interpolate(flat) min_elem: f32,
@location(6) @interpolate(flat) align_end: f32,
@location(7) @interpolate(flat) align_total: f32,
@location(8) cap: vec2<f32>,
@location(9) @interpolate(flat) cap_ends: vec3<f32>,
}
// The selection pass widens highlighted lines by `highlight_widen` pixels;
// every other pass uploads zero.
fn resolve_hw(taper_ratio: f32, world_hw: f32, px_hw: f32) -> f32 {
return resolve_hw_base(taper_ratio, world_hw, px_hw) + u.highlight_widen * 0.5;
}
fn resolve_hw_base(taper_ratio: f32, world_hw: f32, px_hw: f32) -> f32 {
if taper_ratio > 0.0 {
return max((taper_ratio * world_hw) / u.world_per_pixel, 0.5);
}
if world_hw > 0.0 {
return max(world_hw / u.world_per_pixel, 0.5);
}
if world_hw < 0.0 { return max(-world_hw, 0.5); }
var display_hw = max(px_hw * u.lineweight_scale, 0.5);
if u.lineweight_scale < 0.0 {
let scale = -u.lineweight_scale;
let base_hw = select(
min(px_hw * MODEL_LINEWEIGHT_BOOST, MODEL_LINEWEIGHT_MAX_PX * 0.5),
0.5,
px_hw <= 0.5,
);
display_hw = max(base_hw * scale, 0.5);
}
return select(0.5, display_hw, u.lwdisplay_enable > 0.5);
}
fn marker_relative(
position_high: vec3<f32>,
position_low: vec3<f32>,
translation: vec3<f32>,
translation_low: vec3<f32>,
) -> vec3<f32> {
if wire_const.marker_normal_scale.w <= 0.0 {
return (position_high + translation - u.eye_high)
+ (position_low + translation_low - u.eye_low);
}
let origin_high = wire_const.marker_origin_high.xyz;
let origin_low = wire_const.marker_origin_low.xyz;
let origin_relative = (origin_high + translation - u.eye_high)
+ (origin_low + translation_low - u.eye_low);
let delta = (position_high - origin_high) + (position_low - origin_low);
let normal = normalize(wire_const.marker_normal_scale.xyz);
let axial = normal * dot(delta, normal);
let planar = delta - axial;
let origin_clip = u.view_rot * vec4<f32>(origin_relative, 1.0);
let projection_scale = max(length(vec3<f32>(
u.view_rot[0].y,
u.view_rot[1].y,
u.view_rot[2].y,
)), 1e-12);
let view_height = 2.0 * max(abs(origin_clip.w), 1e-6) / projection_scale;
let world_size = wire_const.marker_normal_scale.w * 0.01 * view_height;
return origin_relative + axial + planar * world_size;
}
@vertex
fn vs_main(@builtin(vertex_index) vertex_index: u32, in: VertexIn) -> VertexOut {
let corner = vertex_index % 6u;
let seg = segments[vertex_index / 6u];
let seg_pos_a = vec3<f32>(seg.pos_a_x, seg.pos_a_y, seg.pos_a_z);
let seg_pos_a_low = vec3<f32>(seg.pos_a_low_x, seg.pos_a_low_y, seg.pos_a_low_z);
let seg_pos_b = vec3<f32>(seg.pos_b_x, seg.pos_b_y, seg.pos_b_z);
let seg_pos_b_low = vec3<f32>(seg.pos_b_low_x, seg.pos_b_low_y, seg.pos_b_low_z);
let seg_distances = vec2<f32>(seg.distance_a, seg.distance_b);
let seg_taper_ratio = unpack2x16snorm(seg.taper_ratio);
let which_end_arr = array<f32, 6>(0.0, 1.0, 1.0, 0.0, 1.0, 0.0);
let side_arr = array<f32, 6>(-1.0, -1.0, 1.0, -1.0, 1.0, 1.0);
let which_end = which_end_arr[corner];
let side = side_arr[corner];
let rel_a = marker_relative(seg_pos_a, seg_pos_a_low, in.translation, in.translation_low);
let rel_b = marker_relative(seg_pos_b, seg_pos_b_low, in.translation, in.translation_low);
let is_tapered = wire_const.is_tapered > 0.5;
let is_world = wire_const.world_half_width > 0.0;
var final_clip: vec4<f32>;
var out_dist: f32;
var out_cap: vec2<f32>;
var out_cap_ends: vec3<f32>;
if is_world {
// Wide polylines have physical world-unit width (world_half_width or taper > 0).
// Expand the quad in 3D world space on the entity's plane (perpendicular to
// the segment and the plane normal), so the rectangle remains hosted rigidly
// on the 3D plane when the camera rotates, orbits, or tilts, matching CAD
// behavior and circle.wgsl planar arcs.
let world_hw_a = select(wire_const.world_half_width, seg_taper_ratio.x * wire_const.world_half_width, is_tapered);
let world_hw_b = select(wire_const.world_half_width, seg_taper_ratio.y * wire_const.world_half_width, is_tapered);
let cur_world_hw = mix(world_hw_a, world_hw_b, which_end);
let eff_hw = max(cur_world_hw, 0.5 * u.world_per_pixel);
let world_delta = rel_b - rel_a;
let world_len = length(world_delta);
var world_dir = vec3<f32>(1.0, 0.0, 0.0);
if world_len > 1e-6 {
world_dir = world_delta / world_len;
}
var norm = vec3<f32>(0.0, 0.0, 1.0);
if length(wire_const.marker_normal_scale.xyz) > 1e-4 {
norm = normalize(wire_const.marker_normal_scale.xyz);
}
var perp_world = cross(norm, world_dir);
if length(perp_world) < 1e-4 {
perp_world = cross(vec3<f32>(0.0, 1.0, 0.0), world_dir);
if length(perp_world) < 1e-4 {
perp_world = cross(vec3<f32>(1.0, 0.0, 0.0), world_dir);
}
}
perp_world = normalize(perp_world);
// A constant-width band slides each corner along the segment onto
// its joint's bisector (miter), so neighbours meet without a notch.
let miter = select(seg_taper_ratio, vec2<f32>(0.0), is_tapered) * MITER_LIMIT;
let along = mix(miter.x, miter.y, which_end) * eff_hw * side;
let pos_rel = mix(rel_a, rel_b, which_end);
let world_pos = pos_rel + perp_world * (eff_hw * side) + world_dir * along;
var clip_pos = u.view_rot * vec4<f32>(world_pos, 1.0);
clip_pos = apply_draw_order(clip_pos, in.depth.x);
final_clip = clip_pos;
out_dist = mix(seg_distances.x, seg_distances.y, which_end) + along * (seg_distances.y - seg_distances.x) / max(world_len, 1e-6);
out_cap = vec2<f32>(which_end * world_len, eff_hw * side);
out_cap_ends = vec3<f32>(world_len, world_hw_a, world_hw_b);
} else {
// Thin wires expand in screen pixels facing the camera with rounded end caps / joints.
let clip_a = u.view_rot * vec4<f32>(rel_a, 1.0);
let clip_b = u.view_rot * vec4<f32>(rel_b, 1.0);
let screen_a = clip_a.xy / clip_a.w * u.viewport_size * 0.5;
let screen_b = clip_b.xy / clip_b.w * u.viewport_size * 0.5;
let segment = screen_b - screen_a;
let segment_length = length(segment);
var direction = vec2<f32>(1.0, 0.0);
if segment_length > 1e-4 {
direction = segment / segment_length;
}
let perpendicular = vec2<f32>(-direction.y, direction.x);
let clip_position = mix(clip_a, clip_b, which_end);
let half_width = resolve_hw(0.0, wire_const.world_half_width, wire_const.half_width);
let extension = which_end * 2.0 - 1.0;
let offset_px = perpendicular * half_width * side
+ direction * half_width * extension;
let ndc_offset = offset_px / (u.viewport_size * 0.5);
var clip_pos_out = clip_position + vec4<f32>(ndc_offset * clip_position.w, 0.0, 0.0);
clip_pos_out = apply_draw_order(clip_pos_out, in.depth.x);
final_clip = clip_pos_out;
out_dist = mix(seg_distances.x, seg_distances.y, which_end)
+ extension * half_width * u.world_per_pixel;
out_cap = vec2<f32>(which_end * segment_length + extension * half_width, half_width * side);
out_cap_ends = vec3<f32>(segment_length, half_width, half_width);
}
let scale = u.linetype_scale;
var min_element = wire_const.pattern_length * scale;
let elements = array<f32, 8>(
wire_const.pat0.x * scale,
wire_const.pat0.y * scale,
wire_const.pat0.z * scale,
wire_const.pat0.w * scale,
wire_const.pat1.x * scale,
wire_const.pat1.y * scale,
wire_const.pat1.z * scale,
wire_const.pat1.w * scale,
);
for (var i = 0u; i < 8u; i++) {
let value = abs(elements[i]);
if value > 0.0 && value < min_element {
min_element = value;
}
}
var out: VertexOut;
out.clip_pos = final_clip;
out.color = wire_const.color;
out.distance = out_dist;
out.cap = out_cap;
out.cap_ends = out_cap_ends;
out.pattern_length = wire_const.pattern_length * scale;
out.pat0 = wire_const.pat0 * scale;
out.pat1 = wire_const.pat1 * scale;
out.min_elem = min_element;
out.align_end = wire_const.align_end * scale;
out.align_total = wire_const.align_total;
return out;
}
fn in_dash(
distance: f32,
pattern_length: f32,
pat0: vec4<f32>,
pat1: vec4<f32>,
align_end: f32,
align_total: f32,
) -> bool {
let elements = array<f32, 8>(
pat0.x, pat0.y, pat0.z, pat0.w,
pat1.x, pat1.y, pat1.z, pat1.w,
);
var count = 0u;
for (var i = 0u; i < 8u; i++) {
if elements[i] != 0.0 {
count = i + 1u;
}
}
var value: f32;
if align_total > 0.0 {
if distance <= align_end || distance >= align_total - align_end {
return true;
}
var first_dash = 0.0;
for (var i = 0u; i < count; i++) {
if elements[i] > 0.0 {
first_dash = elements[i];
break;
}
}
value = ((distance - align_end + first_dash) % pattern_length
+ pattern_length) % pattern_length;
} else {
value = ((distance % pattern_length) + pattern_length) % pattern_length;
}
var position = 0.0;
let dot_half = u.world_per_pixel * 0.75;
for (var i = 0u; i < count; i++) {
let element = elements[i];
if element == 0.0 {
let delta = abs(value - position);
if min(delta, pattern_length - delta) <= dot_half {
return true;
}
} else if element > 0.0 {
if value >= position && value < position + element {
return true;
}
position += element;
} else {
position -= element;
}
}
return false;
}
fn clipped_cap(cap: vec2<f32>, ends: vec3<f32>) -> bool {
if cap.x < 0.0 {
return length(cap) > ends.y;
}
if cap.x > ends.x {
return length(vec2<f32>(cap.x - ends.x, cap.y)) > ends.z;
}
return false;
}
fn visible_fragment(in: VertexOut) -> bool {
if clipped_cap(in.cap, in.cap_ends) {
return false;
}
if in.pattern_length > 0.0 && in.min_elem >= u.world_per_pixel {
return in_dash(
in.distance,
in.pattern_length,
in.pat0,
in.pat1,
in.align_end,
in.align_total,
);
}
return true;
}
@fragment
fn fs_main(in: VertexOut) -> @location(0) vec4<f32> {
if !visible_fragment(in) {
discard;
}
let alpha = select(1.0, in.color.a, u.transparency_enable > 0.5);
return vec4<f32>(in.color.rgb, alpha);
}
@fragment
fn fs_black(in: VertexOut) -> @location(0) vec4<f32> {
if !visible_fragment(in) {
discard;
}
let alpha = select(1.0, in.color.a, u.transparency_enable > 0.5);
return vec4<f32>(0.0, 0.0, 0.0, alpha);
}