// Dedicated line shader with dash-pattern, line-cap/join SDF AA, and fog.
//
// Line caps and joins are generated as tessellation geometry by the engine.
// Round cap/join fan vertices are placed at a circumscribed radius with
// normals slightly > 1.0. The shader clips at SDF distance = 1.0 to
// produce pixel-perfect circular edges regardless of polygon segment count.
//
// The shader handles:
// - dash-pattern evaluation using per-vertex centreline distance
// - round-cap antialiasing at dash boundaries when cap_round = 1.0
// - SDF circle AA for round cap/join fan geometry (cap_join flag)
// - linear edge AA for ribbon body geometry
// - horizon fog blending
struct Uniforms {
view_proj: mat4x4<f32>,
fog_color: vec4<f32>,
eye_pos: vec4<f32>,
fog_params: vec4<f32>, // (start, end, density, 0)
// Line-specific uniforms packed into a spare vec4:
// line_style.x = dash_length (world-space meters)
// line_style.y = gap_length (world-space meters)
// line_style.z = cap_round (1.0 = round, 0.0 = butt)
// line_style.w = 0 (reserved)
line_style: vec4<f32>,
};
struct LineVertexInput {
@location(0) position: vec3<f32>,
@location(1) color: vec4<f32>,
@location(2) line_normal: vec2<f32>,
@location(3) line_distance: f32,
@location(4) cap_join: f32,
};
struct LineVertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) color: vec4<f32>,
@location(1) world_pos: vec3<f32>,
@location(2) line_distance: f32,
@location(3) line_normal: vec2<f32>,
@location(4) cap_join: f32,
};
@group(0) @binding(0)
var<uniform> u: Uniforms;
@vertex
fn vs_main(in: LineVertexInput) -> LineVertexOutput {
var out: LineVertexOutput;
out.clip_position = u.view_proj * vec4<f32>(in.position, 1.0);
out.color = in.color;
out.world_pos = in.position;
out.line_distance = in.line_distance;
out.line_normal = in.line_normal;
out.cap_join = in.cap_join;
return out;
}
@fragment
fn fs_main(in: LineVertexOutput) -> @location(0) vec4<f32> {
// --- Dash pattern ---
let dash_len = u.line_style.x;
let gap_len = u.line_style.y;
let cap_round = u.line_style.z;
let cycle = dash_len + gap_len;
var alpha = in.color.a;
// Only apply dash pattern when cycle > 0 (both dash and gap are positive).
if cycle > 0.0 {
let d = in.line_distance % cycle;
if d > dash_len {
// In the gap portion -- discard.
discard;
}
// Soft anti-aliased edge at dash boundaries.
let edge_aa = 0.5;
let dash_edge = smoothstep(0.0, edge_aa, d) *
smoothstep(0.0, edge_aa, dash_len - d);
alpha *= dash_edge;
}
// --- Edge antialiasing ---
let edge_dist = length(in.line_normal);
if in.cap_join > 0.5 {
// SDF circle AA for round cap/join regions.
// Fan perimeter vertices have normals with magnitude slightly > 1.0
// (circumscribed polygon). The SDF distance = length(normal) traces
// a perfect circle at magnitude 1.0. Discard fragments outside the
// circle and apply smooth AA at the boundary.
if edge_dist > 1.0 {
discard;
}
let sdf_aa = smoothstep(1.0, 0.92, edge_dist);
alpha *= sdf_aa;
} else {
// Linear edge AA for ribbon body.
// The normal magnitude approaches 0 at the centreline and 1.0 at the
// ribbon edge. Use it for a smooth alpha falloff.
let ribbon_aa = smoothstep(1.0, 0.95, edge_dist);
alpha *= ribbon_aa;
}
// --- Horizon fog (same as vector.wgsl) ---
let dx = in.world_pos.x - u.eye_pos.x;
let dy = in.world_pos.y - u.eye_pos.y;
let ground_dist = sqrt(dx * dx + dy * dy);
let fog_start = u.fog_params.x;
let fog_end = u.fog_params.y;
let density = u.fog_params.z;
let fog_t = clamp(
(ground_dist - fog_start) / max(fog_end - fog_start, 0.001),
0.0, 1.0,
) * density;
let fog_mix = fog_t * 0.7;
let blended_rgb = mix(in.color.rgb, u.fog_color.rgb, fog_mix);
return vec4<f32>(blended_rgb, alpha * (1.0 - fog_mix));
}