Text was point-sampled (`GL_NEAREST` plus `floor(v_uv * u_tex_size)`), which at a non-integer scale *deletes* texels instead of blending them. The library's own default -- a 12 px style over the 13 px embedded bake -- dropped one of every 8 columns and one of every 17 rows of every glyph, so whole strokes went missing and which ones depended on where the unsnapped pen fell. `kTextFS` now does a 2x2 bilinear fetch clamped to the instance's own cell: the clamp keeps the no-bleed guarantee the sampler mode protects (the atlas is a tight strip of cells with no padding), and at scale 1 with a whole pen the weights are exactly 0, so it stays as crisp as the point sample. Grid and axis lines were staged in data space and mapped in the vertex shader, so a 1 px line's centre landed on an arbitrary fraction -- x = 100.0 lights two columns at half coverage, x = 100.5 lights one crisply, and a frame mixed both. `u_snap` rounds an axis-aligned segment's ink *leading edge* onto a pixel boundary, which reduces to integer+0.5 for odd widths and to an integer for even ones with no case analysis. It is on for the chrome that frames a plot and off for anything drawn at a data position, since the rounding moves a line by up to half a pixel. Diagonals are untouched. `text_pixel`'s threshold drops to 160 because filtered glyphs legitimately peak at 0.844 coverage at 12/13; the 180 threshold found text only while the sampler was also deleting a column and a row of it. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
72 lines
3.8 KiB
C++
72 lines
3.8 KiB
C++
// SPDX-License-Identifier: MIT
|
|
// SPDX-FileCopyrightText: 2026 Martino Ferrari
|
|
|
|
#pragma once
|
|
|
|
namespace plot::render {
|
|
|
|
/// GLSL source of the instanced antialiased line pipeline.
|
|
///
|
|
/// The vertex shader takes one segment per instance (`a_p0` at location 0,
|
|
/// `a_p1` at location 1, both with a vertex attribute divisor of 1), expands it
|
|
/// in pixel space along its normal by `u_width * 0.5 + 1.0` pixels of AA fringe,
|
|
/// and emits a four-vertex `GL_TRIANGLE_STRIP` quad whose corners come from
|
|
/// `gl_VertexID` (0 and 1 are the two sides of `a_p0`, 2 and 3 of `a_p1`). It
|
|
/// outputs `v_dist`, the signed distance from the segment axis in pixels, and
|
|
/// `v_along`, the distance travelled from `a_p0` in pixels.
|
|
///
|
|
/// Uniforms: `u_scale` (vec2, data space to pixels), `u_offset` (vec2, pixels),
|
|
/// `u_viewport` (vec2, pixels), `u_width` (float, pixels), `u_aa` (float, the AA
|
|
/// fringe half-width in pixels -- the style's `line_aa_px` in device pixels,
|
|
/// clamped at 0.25), `u_color` (vec4).
|
|
/// `plot::Rect` is top-left origin while GL is bottom-left, so the y axis is
|
|
/// flipped exactly once, here.
|
|
///
|
|
/// A degenerate segment (`a_p0 == a_p1`) keeps a finite normal and is stretched
|
|
/// into a square mark one stroke width and fringe across, rather than
|
|
/// collapsing to zero area and rasterising nothing — the dense path stages
|
|
/// `min == max` buckets as exactly such segments.
|
|
///
|
|
/// The fragment shader discards outside the fringe and ramps alpha over the last
|
|
/// pixel of the half width with `smoothstep`, which is what keeps a 1px trace
|
|
/// from aliasing into dashes.
|
|
extern const char* const kLineVS;
|
|
extern const char* const kLineFS;
|
|
|
|
/// GLSL source of the instanced text pipeline.
|
|
///
|
|
/// One glyph cell per instance, every attribute with a vertex attribute divisor
|
|
/// of 1: `a_pos` at location 0 is the cell's top-left corner in Rect pixels,
|
|
/// `a_uv` at location 1 is the glyph rect's normalised top-left in the atlas and
|
|
/// `a_axis` at location 2 is the cell's local +x axis, a unit vector in pixels.
|
|
///
|
|
/// The vertex shader builds the cell from that basis and its perpendicular --
|
|
/// the cell's own +v direction -- so `a_axis` of `(0, -1)` draws a run that
|
|
/// advances up the screen, and it maps `a_uv` over the cell with the same
|
|
/// handedness, so a glyph's top row stays its top row however the cell is
|
|
/// turned. It also passes the cell's origin through `flat`, which is the bound
|
|
/// the fragment shader filters against.
|
|
///
|
|
/// The fragment shader converts back to texels and blends the four texels the
|
|
/// fragment sits between, **clamped to the instance's own cell**. Filtering is
|
|
/// the shader's, not the sampler's: every fetch is an explicit texel centre, so
|
|
/// `GL_NEAREST` stays correct, and the clamp keeps a footprint reaching past a
|
|
/// cell edge from blending in the neighbouring glyph of what is a tight,
|
|
/// unpadded strip of cells. A point sample would have been enough only at an
|
|
/// integer scale: at the library's own default of a 12 px style on the 13 px
|
|
/// embedded bake it *deleted* one of every 8 columns and one of every 17 rows of
|
|
/// every glyph. At scale 1 with a whole pen the two taps coincide and the result
|
|
/// is exactly as crisp as a point sample. The instance's own `a_colour` tints the
|
|
/// straight alpha -- per vertex, so one draw can carry runs of several colours.
|
|
///
|
|
/// Uniforms: `u_viewport` (vec2, pixels), `u_px_range` (vec2, the glyph cell's
|
|
/// size on screen in pixels), `u_glyph_uv` (vec2, the glyph cell's size in
|
|
/// normalised atlas coordinates -- one size for the whole draw, since every
|
|
/// glyph of a frame comes from one atlas; read in *both* stages, the fragment
|
|
/// one needing it for the clamp), `u_tex_size` (vec2, atlas size in texels),
|
|
/// `u_atlas` (sampler2D, unit 0).
|
|
extern const char* const kTextVS;
|
|
extern const char* const kTextFS;
|
|
|
|
} // namespace plot::render
|