// SDF circle shader with stroke, blur, and fog support.
struct Uniforms {
view_proj: mat4x4<f32>,
fog_color: vec4<f32>,
eye_pos: vec4<f32>,
fog_params: vec4<f32>, // (start, end, density, 0)
};
struct CircleVertexInput {
@location(0) position: vec3<f32>,
@location(1) quad_offset: vec2<f32>,
@location(2) color: vec4<f32>,
@location(3) stroke_color: vec4<f32>,
@location(4) params: vec4<f32>, // (radius, stroke_width, blur, 0)
};
struct CircleVertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) color: vec4<f32>,
@location(1) stroke_color: vec4<f32>,
@location(2) quad_uv: vec2<f32>,
@location(3) params: vec4<f32>,
@location(4) world_pos: vec3<f32>,
};
@group(0) @binding(0)
var<uniform> u: Uniforms;
@vertex
fn vs_main(in: CircleVertexInput) -> CircleVertexOutput {
let radius = in.params.x;
let stroke_width = in.params.y;
let total_radius = radius + stroke_width;
// Expand the quad: position is the circle centre, quad_offset is ±1.
let expanded = in.position + vec3<f32>(
in.quad_offset.x * total_radius,
in.quad_offset.y * total_radius,
0.0,
);
var out: CircleVertexOutput;
out.clip_position = u.view_proj * vec4<f32>(expanded, 1.0);
out.color = in.color;
out.stroke_color = in.stroke_color;
out.quad_uv = in.quad_offset;
out.params = in.params;
out.world_pos = in.position;
return out;
}
@fragment
fn fs_main(in: CircleVertexOutput) -> @location(0) vec4<f32> {
let radius = in.params.x;
let stroke_width = in.params.y;
let blur = in.params.z;
let total_radius = radius + stroke_width;
// SDF: distance from quad centre in normalised space.
let dist = length(in.quad_uv) * total_radius;
// Anti-aliasing width (in world units, proportional to total radius).
let aa = max(blur * total_radius, 0.5);
// Outer edge: smoothstep fade.
let outer_alpha = 1.0 - smoothstep(total_radius - aa, total_radius, dist);
// Stroke ring: inside stroke zone.
let inner_edge = radius;
let stroke_alpha = smoothstep(inner_edge - aa, inner_edge, dist);
// Blend stroke and fill colours.
let fill_alpha = (1.0 - stroke_alpha) * outer_alpha;
let ring_alpha = stroke_alpha * outer_alpha;
let blended = in.color.rgb * fill_alpha * in.color.a +
in.stroke_color.rgb * ring_alpha * in.stroke_color.a;
let alpha = fill_alpha * in.color.a + ring_alpha * in.stroke_color.a;
if alpha < 0.001 {
discard;
}
// --- Horizon fog ---
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 final_rgb = mix(blended / max(alpha, 0.001), u.fog_color.rgb, fog_mix);
return vec4<f32>(final_rgb, alpha * (1.0 - fog_mix));
}