// SDF symbol / text rendering shader.
//
// Renders glyphs from an SDF atlas texture. The signed distance field
// encodes the distance from the glyph edge: values > 0.5 are inside,
// values < 0.5 are outside. The fragment shader thresholds the SDF to
// produce crisp, resolution-independent text with optional halo.
struct Uniforms {
view_proj: mat4x4<f32>,
fog_color: vec4<f32>,
eye_pos: vec4<f32>,
fog_params: vec4<f32>,
};
struct SymbolVertexInput {
@location(0) position: vec3<f32>,
@location(1) glyph_offset: vec2<f32>,
@location(2) tex_coord: vec2<f32>,
@location(3) color: vec4<f32>,
@location(4) halo_color: vec4<f32>,
@location(5) params: vec4<f32>, // (halo_width, gamma_scale, 0, 0)
};
struct SymbolVertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) tex_coord: vec2<f32>,
@location(1) color: vec4<f32>,
@location(2) halo_color: vec4<f32>,
@location(3) params: vec4<f32>,
@location(4) world_pos: vec3<f32>,
};
@group(0) @binding(0) var<uniform> u: Uniforms;
@group(1) @binding(0) var sdf_atlas: texture_2d<f32>;
@group(1) @binding(1) var sdf_sampler: sampler;
@vertex
fn vs_main(in: SymbolVertexInput) -> SymbolVertexOutput {
let world = in.position + vec3<f32>(in.glyph_offset, 0.0);
var out: SymbolVertexOutput;
out.clip_position = u.view_proj * vec4<f32>(world, 1.0);
out.tex_coord = in.tex_coord;
out.color = in.color;
out.halo_color = in.halo_color;
out.params = in.params;
out.world_pos = in.position;
return out;
}
@fragment
fn fs_main(in: SymbolVertexOutput) -> @location(0) vec4<f32> {
let sdf = textureSample(sdf_atlas, sdf_sampler, in.tex_coord).r;
let halo_width = in.params.x;
let gamma = in.params.y;
// SDF thresholding: 0.5 is the glyph edge.
let edge = 0.5;
let aa = gamma * 0.707; // ~1/sqrt(2) pixel-width anti-aliasing.
// Halo: expand the glyph outline outward.
let halo_edge = edge - halo_width;
let halo_alpha = smoothstep(halo_edge - aa, halo_edge + aa, sdf);
let fill_alpha = smoothstep(edge - aa, edge + aa, sdf);
// Composite halo behind fill.
let halo_rgb = in.halo_color.rgb * in.halo_color.a * halo_alpha * (1.0 - fill_alpha);
let fill_rgb = in.color.rgb * in.color.a * fill_alpha;
let combined_rgb = fill_rgb + halo_rgb;
let combined_a = fill_alpha * in.color.a + halo_alpha * in.halo_color.a * (1.0 - fill_alpha);
if combined_a < 0.005 {
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(combined_rgb / max(combined_a, 0.001), u.fog_color.rgb, fog_mix);
return vec4<f32>(final_rgb, combined_a * (1.0 - fog_mix));
}