Skip to content

fix(element): render MSDF text at the true glyph edge#8935

Merged
willeastcott merged 6 commits into
mainfrom
fix/msdf-text-coverage
Jun 22, 2026
Merged

fix(element): render MSDF text at the true glyph edge#8935
willeastcott merged 6 commits into
mainfrom
fix/msdf-text-coverage

Conversation

@willeastcott

@willeastcott willeastcott commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Problem

Magnified MSDF text rendered via an ElementComponent (text) shows a hole in the f crossbar and notches in the x crossing — MSDF median sign-errors at thin junctions, not aliasing.

Roboto Light Italic, "fox" at fontSize 256–480 (≈ 4× the 64px atlas glyphs):

Before After

Cause

applyMsdf remapped the field with map(0.05, …), placing the 50%-coverage threshold at sigDist ≈ 0.525 instead of 0.5 — a fixed inward erosion of every glyph. Where the MSDF median plateaus just above 0.5 (thin crossbars / crossings) the erosion cuts through, producing the hole and notches. This is the 0.05 threshold @mvaligursky flagged in #2948.

Separately, the AA width came from the horizontal uv derivative only (fwidth(vUv0).x), so diagonal/italic edges were under-anti-aliased.

Fix

  • Threshold at the true glyph edge: edge = 0.5 - 0.5 * font_sdfIntensity — intensity 0 renders at the real edge; font_sdfIntensity still fattens with the same slope; this also removes a latent divide-by-zero map() had at high intensity.
  • Anti-alias from the gradient of the distance field (fwidth(sigDist) / abs(dpdx)+abs(dpdy)), correct for edges of any orientation.
  • Remove the now-unused map(), font_pxrange / font_textureWidth uniforms and the _getPxRange() helper.

GLSL and WGSL chunks kept in parity.

Compatibility

Removing the erosion means text now renders at its true (designer-intended) weight, which is marginally heavier than before. The shift is uniform and equals the erosion that was applied (≈ 0.2 atlas texels per edge). It is most visible on very thin fonts stored at low atlas resolution — the Roboto Light Italic above is near a worst case (≈ +25% stroke width, because its strokes are only ≈ 2 texels wide); most fonts will change far less. Deliberate weight adjustments should use the per-font font_sdfIntensity (or outlineThickness), not a global threshold bias.

Because the fix removes the font_pxrange and font_textureWidth uniforms from the msdfPS chunk, msdfPS is registered in the chunk-version table (API 2.20) so a custom override of that chunk is flagged by the chunk validator.

Testing

  • WebGL2 (repro project) and WebGPU (examples → User Interface ▸ Text): clean rendering; outline, drop shadow, markup colours and small text all verified.
  • npm test passes (incl. the text-element suite); lint clean.

Fixes #2948

Related to #830 (small-size MSDF readability): this removes the erosion and improves AA, but that issue's atlas-side asks — uniform glyph scale, sub-pixel quad placement, and a bitmap/MTSDF fallback — are out of scope here.

A fixed 0.05 bias in the coverage remap placed the edge at sigDist ~0.525
instead of 0.5, eroding every glyph inward; at thin junctions this carved a
hole in the 'f' crossbar and notches in the 'x' crossing. Render at the true
edge (font_sdfIntensity 0 -> 0.5, still fattening with the same slope) and
derive anti-aliasing from the distance-field gradient so edges of any
orientation - including italic diagonals - anti-alias correctly. Drop the
now-unused map(), font_pxrange and font_textureWidth uniforms and _getPxRange().

Fixes #2948

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jun 19, 2026

Copy link
Copy Markdown

Build size report

This PR changes the size of the minified bundles.

Bundle Minified Gzip Brotli
playcanvas.min.js 2280.7 KB (−1.6 KB, −0.07%) 584.8 KB (−0.4 KB, −0.06%) 454.8 KB (−0.0 KB, −0.01%)
playcanvas.min.mjs 2278.1 KB (−1.6 KB, −0.07%) 583.9 KB (−0.4 KB, −0.07%) 454.1 KB (−0.4 KB, −0.09%)

@willeastcott willeastcott self-assigned this Jun 19, 2026
@willeastcott willeastcott added enhancement Request for a new feature area: ui UI related issue labels Jun 19, 2026
@willeastcott

willeastcott commented Jun 19, 2026

Copy link
Copy Markdown
Contributor Author

Just to prove the point, here is the Roboto Light Italic font rendered by Windows on the left and the same font rendered in current PlayCanvas Engine on the right:

image

Again, Windows on left and PlayCanvas Engine with fixes applied on the right:

image

So yes, fonts will appear heavier than before but they will be correct and match the source font asset.

My expectation is that we might have some developers noticing but they can probably just set their font intensity back to 0 and their text should match the source font and no longer exhibit noticable artifacts. I propose we merge and see how developers respond. This fix is simply too good to pass up!

The coverage change removes the font_pxrange and font_textureWidth uniforms
(the engine no longer sets them), changing the msdfPS chunk contract. Register
msdfPS in chunkVersions so an older user override is flagged by the validator.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Drop the historical narrative from the coverage/anti-aliasing comments and
describe only what the code does.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@mvaligursky mvaligursky left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all good, the versioning is required as well, as old chunk will no longer work (missing uniforms).

@willeastcott willeastcott merged commit 105ee55 into main Jun 22, 2026
10 checks passed
@willeastcott willeastcott deleted the fix/msdf-text-coverage branch June 22, 2026 09:51
@mvaligursky

mvaligursky commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Docs follow-up: added an Engine v2.20 entry to the shader chunk migrations page (covering the msdfPS rework and the removed font_pxrange / font_textureWidth uniforms + map() helper) in both the English and Japanese versions — playcanvas/developer-site#1067.

willeastcott added a commit that referenced this pull request Jun 30, 2026
…e field gradient (#8990)

#8935 switched the coverage anti-aliasing to fwidth(median(sample)). On
minified (small) text the atlas is undersampled, so that gradient is noisy
and breathes as the text translates - small text shimmers and thin strokes
(l, i, 1) flicker on and off (#8984).

Derive the transition width from the uv magnification (both axes) and the
atlas spread instead - the msdfgen screenPxRange approach - which depends
only on geometry, so it is stable under motion and minification, floored at
1px. Keep the true-edge threshold (edge = 0.5 - 0.5*intensity) from #8935 so
the #2948 crossbar fix and the rendered weight are unchanged.

Re-adds the font_pxrange uniform removed by #8935 (atlas size comes from
textureSize/textureDimensions); bumps the msdfPS chunk version to 2.21.

Fixes #8984

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
willeastcott added a commit that referenced this pull request Jun 30, 2026
…e field gradient (#8990)

#8935 switched the coverage anti-aliasing to fwidth(median(sample)). On
minified (small) text the atlas is undersampled, so that gradient is noisy
and breathes as the text translates - small text shimmers and thin strokes
(l, i, 1) flicker on and off (#8984).

Derive the transition width from the uv magnification (both axes) and the
atlas spread instead - the msdfgen screenPxRange approach - which depends
only on geometry, so it is stable under motion and minification, floored at
1px. Keep the true-edge threshold (edge = 0.5 - 0.5*intensity) from #8935 so
the #2948 crossbar fix and the rendered weight are unchanged.

Re-adds the font_pxrange uniform removed by #8935 (atlas size comes from
textureSize/textureDimensions). The msdfPS chunk version is left at 2.20 on
this release line.

Fixes #8984

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: ui UI related issue enhancement Request for a new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Can we increase quality of font assets?

3 participants