struct VertexInput {
// Mesh-encoder buffer (slot 0)
@location(0) uvw: vec3f,
@location(1) screen_pos: vec2f,
@location(2) font_size_px: f32,
// Material-encoder buffer (slot 1)
@location(3) material_index: u32,
@location(4) gradient_uv: vec2f,
}
struct VertexOutput {
@builtin(position) @invariant clip_position: vec4f,
@location(0) uvw: vec3f,
@location(1) @interpolate(flat) font_size_px: f32,
@location(2) @interpolate(flat) material_index: u32,
@location(3) gradient_uv: vec2f,
}
struct Material {
color: vec4f,
gradient_color: vec4f,
gradient_direction: vec2f,
render_dist_range: vec2f,
max_alpha_dist_range: vec2f,
offset: vec2f,
roundness: f32,
}
struct Uniforms {
screen_size: vec2f,
}
@group(0) @binding(0) var atlas: texture_2d_array<f32>;
@group(0) @binding(1) var atlas_sampler: sampler;
@group(1) @binding(0) var<uniform> uniforms: Uniforms;
@group(2) @binding(0) var<storage, read> materials: array<Material>;
@vertex
fn vs_main(in: VertexInput) -> VertexOutput {
var out: VertexOutput;
let material = materials[in.material_index];
let pos = in.screen_pos + material.offset;
let ndc = (pos / uniforms.screen_size) * 2.0 - 1.0;
out.clip_position = vec4f(ndc.x, -ndc.y, 0.0, 1.0);
out.uvw = in.uvw;
out.font_size_px = in.font_size_px;
out.material_index = in.material_index;
out.gradient_uv = in.gradient_uv;
return out;
}
fn median(r: f32, g: f32, b: f32) -> f32 {
return max(min(r, g), min(max(r, g), b));
}
fn sample_atlas(uvw: vec3f) -> vec4f {
return textureSample(atlas, atlas_sampler, uvw.xy, i32(uvw.z));
}
// Convert sampled color (0-1) to signed distance in screen pixels.
fn color_to_dist(color: f32, font_size_px: f32) -> f32 {
return (color - 0.5) * font_size_px;
}
// Alpha for a fragment at signed screen-space distance `dist`.
//
// render_dist_range [rlo, rhi]:
// The material is non-zero only within this range (in screen pixels).
//
// max_alpha_dist_range [mlo, mhi]:
// Within [mlo, mhi] the alpha reaches its maximum value (1.0).
// Between rlo→mlo and mhi→rhi the alpha fades linearly to zero.
fn msdf_coverage(dist: f32, material: Material) -> f32 {
let rlo = material.render_dist_range.x;
let rhi = material.render_dist_range.y;
let mlo = material.max_alpha_dist_range.x;
let mhi = material.max_alpha_dist_range.y;
// Each edge: linear ramp between the render boundary and the max-alpha boundary.
// When rlo==mlo or mhi==rhi there is no ramp; 1e-6 avoids division by zero while
// keeping the result a step function at that boundary.
let lower = clamp((dist - rlo) / max(mlo - rlo, 1e-6), 0.0, 1.0);
let upper = clamp((rhi - dist) / max(rhi - mhi, 1e-6), 0.0, 1.0);
return min(lower, upper);
}
fn material_color(material: Material, gradient_uv: vec2f) -> vec4f {
let intensity = dot(gradient_uv, material.gradient_direction);
return mix(material.color, material.gradient_color, intensity);
}
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4f {
let material = materials[in.material_index];
let s = sample_atlas(in.uvw);
let msdf = mix(median(s.r, s.g, s.b), s.a, material.roundness);
let dist = color_to_dist(msdf, in.font_size_px);
let alpha = msdf_coverage(dist, material);
var col = material_color(material, in.gradient_uv);
col.a = col.a * alpha;
return col;
}