Skip to content

Commit 105ee55

Browse files
willeastcottclaude
andauthored
fix(element): render MSDF text at the true glyph edge (#8935)
* fix(element): render MSDF text at the true glyph edge 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> * fix(element): flag outdated msdfPS chunk overrides 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> * docs(element): keep msdf shader comments to current behaviour 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> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent cf3e3e5 commit 105ee55

4 files changed

Lines changed: 19 additions & 70 deletions

File tree

src/framework/components/element/text-element.js

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -684,8 +684,6 @@ class TextElement {
684684
mi.setParameter('material_emissive', this._colorUniform);
685685
mi.setParameter('material_opacity', this._color.a);
686686
mi.setParameter('font_sdfIntensity', this._font.intensity);
687-
mi.setParameter('font_pxrange', this._getPxRange(this._font));
688-
mi.setParameter('font_textureWidth', this._font.data.info.maps[i].width);
689687

690688
mi.setParameter('outline_color', this._outlineColorUniform);
691689
mi.setParameter('outline_thickness', this._outlineThicknessScale * this._outlineThickness);
@@ -1414,8 +1412,6 @@ class TextElement {
14141412
const mi = this._meshInfo[i].meshInstance;
14151413
if (mi) {
14161414
mi.setParameter('font_sdfIntensity', this._font.intensity);
1417-
mi.setParameter('font_pxrange', this._getPxRange(this._font));
1418-
mi.setParameter('font_textureWidth', this._font.data.info.maps[i].width);
14191415
}
14201416
}
14211417
}
@@ -1439,18 +1435,6 @@ class TextElement {
14391435
}
14401436
}
14411437

1442-
_getPxRange(font) {
1443-
// calculate pxrange from range and scale properties on a character
1444-
const keys = Object.keys(this._font.data.chars);
1445-
for (let i = 0; i < keys.length; i++) {
1446-
const char = this._font.data.chars[keys[i]];
1447-
if (char.range) {
1448-
return (char.scale || 1) * char.range;
1449-
}
1450-
}
1451-
return 2; // default
1452-
}
1453-
14541438
_getUv(char) {
14551439
const data = this._font.data;
14561440

@@ -1798,8 +1782,6 @@ class TextElement {
17981782
const mi = this._meshInfo[i].meshInstance;
17991783
if (mi) {
18001784
mi.setParameter('font_sdfIntensity', this._font.intensity);
1801-
mi.setParameter('font_pxrange', this._getPxRange(this._font));
1802-
mi.setParameter('font_textureWidth', this._font.data.info.maps[i].width);
18031785
this._setTextureParams(mi, this._font.textures[i]);
18041786
}
18051787
}

src/scene/shader-lib/glsl/chunks/chunk-validation.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,10 @@ const chunkVersions = {
5757
iridescenceDiffractionPS: '1.65',
5858
lightmapAddPS: '1.65',
5959
refractionCubePS: '1.70',
60-
refractionDynamicPS: '1.70'
60+
refractionDynamicPS: '1.70',
61+
62+
// text
63+
msdfPS: '2.20'
6164
};
6265

6366
// removed

src/scene/shader-lib/glsl/chunks/common/frag/msdf.js

Lines changed: 7 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,7 @@ float median(float r, float g, float b) {
55
return max(min(r, g), min(max(r, g), b));
66
}
77
8-
float map (float min, float max, float v) {
9-
return (v - min) / (max - min);
10-
}
11-
128
uniform float font_sdfIntensity; // intensity is used to boost the value read from the SDF, 0 is no boost, 1.0 is max boost
13-
uniform float font_pxrange; // the number of pixels between inside and outside the font in SDF
14-
uniform float font_textureWidth; // the width of the texture atlas
159
1610
#ifndef LIT_MSDF_TEXT_ATTRIBUTE
1711
uniform vec4 outline_color;
@@ -41,27 +35,15 @@ vec4 applyMsdf(vec4 color) {
4135
float sigDist = median(tsample.r, tsample.g, tsample.b);
4236
float sigDistShdw = median(ssample.r, ssample.g, ssample.b);
4337
44-
// smoothing limit - smaller value makes for sharper but more aliased text, especially on angles
45-
// too large value (0.5) creates a dark glow around the letters
46-
float smoothingMax = 0.2;
47-
48-
// smoothing depends on size of texture on screen
49-
vec2 w = fwidth(vUv0);
50-
float smoothing = clamp(w.x * font_textureWidth / font_pxrange, 0.0, smoothingMax);
51-
52-
float mapMin = 0.05;
53-
float mapMax = clamp(1.0 - font_sdfIntensity, mapMin, 1.0);
38+
// coverage threshold (0.5 = glyph edge); font_sdfIntensity fattens the glyph
39+
float edge = 0.5 - 0.5 * font_sdfIntensity;
5440
55-
// remap to a smaller range (used on smaller font sizes)
56-
float sigDistInner = map(mapMin, mapMax, sigDist);
57-
float sigDistOutline = map(mapMin, mapMax, sigDist + outline_thickness);
58-
sigDistShdw = map(mapMin, mapMax, sigDistShdw + outline_thickness);
41+
// anti-aliasing width: distance field change per pixel, clamped above zero
42+
float aa = max(fwidth(sigDist), 1e-4);
5943
60-
float center = 0.5;
61-
// calculate smoothing and use to generate opacity
62-
float inside = smoothstep(center-smoothing, center+smoothing, sigDistInner);
63-
float outline = smoothstep(center-smoothing, center+smoothing, sigDistOutline);
64-
float shadow = smoothstep(center-smoothing, center+smoothing, sigDistShdw);
44+
float inside = clamp((sigDist - edge) / aa + 0.5, 0.0, 1.0);
45+
float outline = clamp((sigDist + outline_thickness - edge) / aa + 0.5, 0.0, 1.0);
46+
float shadow = clamp((sigDistShdw + outline_thickness - edge) / aa + 0.5, 0.0, 1.0);
6547
6648
vec4 tcolor = (outline > inside) ? outline * vec4(outline_color.a * outline_color.rgb, outline_color.a) : vec4(0.0);
6749
tcolor = mix(tcolor, color, inside);

src/scene/shader-lib/wgsl/chunks/common/frag/msdf.js

Lines changed: 8 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,7 @@ fn median(r: f32, g: f32, b: f32) -> f32 {
66
return max(min(r, g), min(max(r, g), b));
77
}
88
9-
fn map(min: f32, max: f32, v: f32) -> f32 {
10-
return (v - min) / (max - min);
11-
}
12-
139
uniform font_sdfIntensity: f32; // intensity is used to boost the value read from the SDF, 0 is no boost, 1.0 is max boost
14-
uniform font_pxrange: f32; // the number of pixels between inside and outside the font in SDF
15-
uniform font_textureWidth: f32; // the width of the texture atlas
1610
1711
#ifndef LIT_MSDF_TEXT_ATTRIBUTE
1812
uniform outline_color: vec4f;
@@ -52,29 +46,17 @@ fn applyMsdf(color_in: vec4f) -> vec4f {
5246
5347
// get the signed distance value
5448
let sigDist: f32 = median(tsample.r, tsample.g, tsample.b);
55-
var sigDistShdw: f32 = median(ssample.r, ssample.g, ssample.b);
56-
57-
// smoothing limit - smaller value makes for sharper but more aliased text, especially on angles
58-
// too large value (0.5) creates a dark glow around the letters
59-
let smoothingMax: f32 = 0.2;
60-
61-
// smoothing depends on size of texture on screen
62-
let w: vec2f = abs(dpdx(vUv0)) + abs(dpdy(vUv0));
63-
let smoothing: f32 = clamp(w.x * uniform.font_textureWidth / uniform.font_pxrange, 0.0, smoothingMax);
49+
let sigDistShdw: f32 = median(ssample.r, ssample.g, ssample.b);
6450
65-
let mapMin: f32 = 0.05;
66-
let mapMax: f32 = clamp(1.0 - uniform.font_sdfIntensity, mapMin, 1.0);
51+
// coverage threshold (0.5 = glyph edge); font_sdfIntensity fattens the glyph
52+
let edge: f32 = 0.5 - 0.5 * uniform.font_sdfIntensity;
6753
68-
// remap to a smaller range (used on smaller font sizes)
69-
let sigDistInner: f32 = map(mapMin, mapMax, sigDist);
70-
let sigDistOutline: f32 = map(mapMin, mapMax, sigDist + outline_thicknessValue);
71-
sigDistShdw = map(mapMin, mapMax, sigDistShdw + outline_thicknessValue);
54+
// anti-aliasing width: distance field change per pixel, clamped above zero
55+
let aa: f32 = max(abs(dpdx(sigDist)) + abs(dpdy(sigDist)), 1e-4);
7256
73-
let center: f32 = 0.5;
74-
// calculate smoothing and use to generate opacity
75-
let inside: f32 = smoothstep(center - smoothing, center + smoothing, sigDistInner);
76-
let outline: f32 = smoothstep(center - smoothing, center + smoothing, sigDistOutline);
77-
let shadow: f32 = smoothstep(center - smoothing, center + smoothing, sigDistShdw);
57+
let inside: f32 = clamp((sigDist - edge) / aa + 0.5, 0.0, 1.0);
58+
let outline: f32 = clamp((sigDist + outline_thicknessValue - edge) / aa + 0.5, 0.0, 1.0);
59+
let shadow: f32 = clamp((sigDistShdw + outline_thicknessValue - edge) / aa + 0.5, 0.0, 1.0);
7860
7961
let tcolor_outline: vec4f = outline * vec4f(outline_colorValue.a * outline_colorValue.rgb, outline_colorValue.a);
8062
var tcolor: vec4f = select(vec4f(0.0), tcolor_outline, outline > inside);

0 commit comments

Comments
 (0)