Skip to content

Commit 1829e0e

Browse files
Merge pull request AristurtleDev#4 from AristurtleDev/feature/dialation
Add dynamic glyph dilation to prevent thin strokes
2 parents 4cacea4 + d36d051 commit 1829e0e

7 files changed

Lines changed: 108 additions & 39 deletions

File tree

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
<!-- Version Configuration -->
1010
<PropertyGroup>
11-
<Version>0.0.1</Version>
11+
<Version>0.0.2</Version>
1212
<MonoGameVersion>3.8.4.1</MonoGameVersion>
1313
</PropertyGroup>
1414

src/Forme.MonoGame/FormeFontDevice.cs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ namespace Forme.MonoGame;
2323
/// <see cref="SurfaceFormat.Vector4"/> with RG32F data packed into XY and ZW set to zero;
2424
/// this is required because SM3/MojoShader cannot sample RG32F directly.
2525
/// </para>
26+
/// <para>
27+
/// The band LUT texture uses <see cref="SurfaceFormat.Vector4"/> (RGBA32F) and stores
28+
/// per-glyph band transform data (bandScaleX, bandScaleY, bandOffsetX, bandOffsetY) indexed
29+
/// by glyph.
30+
/// </para>
2631
/// </remarks>
2732
public sealed class FormeFontDevice : IDisposable
2833
{
@@ -41,11 +46,25 @@ public sealed class FormeFontDevice : IDisposable
4146
/// </summary>
4247
public Texture2D BandTexture { get; }
4348

49+
/// <summary>
50+
/// Gets the band LUT texture containing per-glyph band transform data (RGBA32F).
51+
/// </summary>
52+
/// <remarks>
53+
/// Each texel stores (bandScaleX, bandScaleY, bandOffsetX, bandOffsetY) for one glyph.
54+
/// The texture is at most 4096 texels wide; tall fonts use multiple rows.
55+
/// </remarks>
56+
public Texture2D BandLutTexture { get; }
57+
4458
/// <summary>
4559
/// Gets the glyph metadata dictionary, keyed by Unicode code point.
4660
/// </summary>
4761
public IReadOnlyDictionary<int, FormeGlyph> Glyphs { get; }
4862

63+
/// <summary>
64+
/// Gets the precomputed band LUT UV coordinates, keyed by Unicode code point.
65+
/// </summary>
66+
internal IReadOnlyDictionary<int, Vector2> GlyphLutUVs { get; }
67+
4968
/// <summary>
5069
/// Gets the font metrics (ascent, descent, units per em).
5170
/// </summary>
@@ -79,6 +98,9 @@ public FormeFontDevice(GraphicsDevice graphicsDevice, FormeFont font)
7998

8099
CurveTexture = UploadCurveTexture(graphicsDevice, font);
81100
BandTexture = UploadBandTexture(graphicsDevice, font);
101+
102+
BandLutTexture = BuildBandLut(graphicsDevice, font, out Dictionary<int, Vector2> glyphLutUVs);
103+
GlyphLutUVs = glyphLutUVs;
82104
}
83105

84106
private static Texture2D UploadCurveTexture(GraphicsDevice graphicsDevice, FormeFont font)
@@ -128,6 +150,40 @@ private static Texture2D UploadBandTexture(GraphicsDevice graphicsDevice, FormeF
128150
return texture;
129151
}
130152

153+
private static Texture2D BuildBandLut(
154+
GraphicsDevice graphicsDevice, FormeFont font, out Dictionary<int, Vector2> uvs)
155+
{
156+
int glyphCount = Math.Max(1, font.Glyphs.Count);
157+
int lutWidth = Math.Min(glyphCount, 4096);
158+
int lutHeight = (glyphCount + 4095) / 4096;
159+
160+
Vector4[] data = new Vector4[lutWidth * lutHeight];
161+
uvs = new Dictionary<int, Vector2>(glyphCount);
162+
163+
int index = 0;
164+
foreach (KeyValuePair<int, FormeGlyph> kvp in font.Glyphs)
165+
{
166+
FormeGlyph g = kvp.Value;
167+
float scaleX = 1.0f / Math.Max(1f, (float)g.BandInfo.DimX);
168+
float scaleY = 1.0f / Math.Max(1f, (float)g.BandInfo.DimY);
169+
data[index] = new Vector4(
170+
scaleX,
171+
scaleY,
172+
-(float)g.BoundingBox.X1 * scaleX,
173+
-(float)g.BoundingBox.Y1 * scaleY);
174+
175+
float u = (index % 4096 + 0.5f) / lutWidth;
176+
float v = (index / 4096 + 0.5f) / lutHeight;
177+
uvs[kvp.Key] = new Vector2(u, v);
178+
index++;
179+
}
180+
181+
Texture2D texture = new Texture2D(
182+
graphicsDevice, lutWidth, lutHeight, false, SurfaceFormat.Vector4);
183+
texture.SetData(data);
184+
return texture;
185+
}
186+
131187
/// <summary>
132188
/// Releases the GPU textures owned by this instance.
133189
/// </summary>
@@ -141,5 +197,6 @@ public void Dispose()
141197
IsDisposed = true;
142198
CurveTexture.Dispose();
143199
BandTexture.Dispose();
200+
BandLutTexture.Dispose();
144201
}
145202
}

src/Forme.MonoGame/FormeRenderer.cs

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ public sealed class FormeRenderer : IDisposable
4848
private RasterizerState? _savedRasterizerState;
4949
private SamplerState? _savedSamplerState0;
5050
private SamplerState? _savedSamplerState1;
51+
private SamplerState? _savedSamplerState2;
5152

5253
private Matrix _transformMatrix;
5354
private bool _hasCustomTransform;
@@ -148,6 +149,7 @@ public void Begin(Matrix? transformMatrix = null)
148149
_savedRasterizerState = _graphicsDevice.RasterizerState;
149150
_savedSamplerState0 = _graphicsDevice.SamplerStates[0];
150151
_savedSamplerState1 = _graphicsDevice.SamplerStates[1];
152+
_savedSamplerState2 = _graphicsDevice.SamplerStates[2];
151153

152154
if (transformMatrix.HasValue)
153155
{
@@ -205,7 +207,7 @@ public void DrawString(FormeFontDevice font, string text, Vector2 position, floa
205207

206208
if (font.Glyphs.TryGetValue(rune.Value, out FormeGlyph glyph))
207209
{
208-
_queue.Add(new QueuedDraw(font, glyph, new Vector2(cursorX, position.Y), sizePixels, color));
210+
_queue.Add(new QueuedDraw(font, glyph, rune.Value, new Vector2(cursorX, position.Y), sizePixels, color));
209211
cursorX += glyph.AdvanceWidth * scale;
210212
}
211213
}
@@ -254,7 +256,7 @@ public void DrawString(FormeFontDevice font, string text, Vector2 position, floa
254256
}
255257

256258
Vector2 glyphPos = new(position.X + placement.BaselineX, position.Y + placement.BaselineY);
257-
_queue.Add(new QueuedDraw(font, glyph, glyphPos, sizePixels, color));
259+
_queue.Add(new QueuedDraw(font, glyph, placement.CodePoint, glyphPos, sizePixels, color));
258260
}
259261
}
260262

@@ -289,7 +291,7 @@ public void DrawGlyph(FormeFontDevice font, int codepoint, Vector2 position, flo
289291

290292
if (font.Glyphs.TryGetValue(codepoint, out FormeGlyph glyph))
291293
{
292-
_queue.Add(new QueuedDraw(font, glyph, position, sizePixels, color));
294+
_queue.Add(new QueuedDraw(font, glyph, codepoint, position, sizePixels, color));
293295
}
294296
}
295297

@@ -318,6 +320,7 @@ public void End()
318320
_graphicsDevice.RasterizerState = RasterizerState.CullNone;
319321
_graphicsDevice.SamplerStates[0] = SamplerState.PointClamp;
320322
_graphicsDevice.SamplerStates[1] = SamplerState.PointClamp;
323+
_graphicsDevice.SamplerStates[2] = SamplerState.PointClamp;
321324

322325
FormeFontDevice? currentFont = null;
323326
_glyphCount = 0;
@@ -354,6 +357,7 @@ public void End()
354357
_graphicsDevice.RasterizerState = _savedRasterizerState!;
355358
_graphicsDevice.SamplerStates[0] = _savedSamplerState0!;
356359
_graphicsDevice.SamplerStates[1] = _savedSamplerState1!;
360+
_graphicsDevice.SamplerStates[2] = _savedSamplerState2!;
357361
}
358362

359363
private void FlushBatch(FormeFontDevice font)
@@ -379,6 +383,7 @@ private void FlushBatch(FormeFontDevice font)
379383
_effect.Parameters["forme_matrix"].SetValue(matrix);
380384
_effect.Parameters["curveTexture"].SetValue(font.CurveTexture);
381385
_effect.Parameters["bandTexture"].SetValue(font.BandTexture);
386+
_effect.Parameters["bandLUTTexture"].SetValue(font.BandLutTexture);
382387

383388
// curveTexSize and bandTexSize are used by the OpenGL shader for UV-based texel
384389
// sampling but are not present in the DirectX 11 shader, which uses Load() instead.
@@ -433,11 +438,6 @@ private bool AppendGlyphQuad(in QueuedDraw draw)
433438
float ex1 = g.BoundingBox.X2;
434439
float ey1 = g.BoundingBox.Y2;
435440

436-
float bandScaleX = 1.0f / Math.Max(1f, (float)g.BandInfo.DimX);
437-
float bandScaleY = 1.0f / Math.Max(1f, (float)g.BandInfo.DimY);
438-
float bandOffsetX = -(float)g.BoundingBox.X1 * bandScaleX;
439-
float bandOffsetY = -(float)g.BoundingBox.Y1 * bandScaleY;
440-
441441
// Pack band texture origin using the texture width as the row stride.
442442
// The shader unpacks this using the runtime uniform bandTexSize.x, avoiding
443443
// compile-time constant folding that the MGCB/MojoShader transpiler gets wrong.
@@ -456,7 +456,15 @@ private bool AppendGlyphQuad(in QueuedDraw draw)
456456
draw.Color.B / 255f,
457457
draw.Color.A / 255f);
458458

459-
Vector4 bnd = new Vector4(bandScaleX, bandScaleY, bandOffsetX, bandOffsetY);
459+
// Inverse Jacobian: maps a screen-space displacement to an em-space displacement.
460+
float invJxx = (ex1 - ex0) / (px1 - px0);
461+
float invJyy = (ey0 - ey1) / (py1 - py0); // negative; font Y up, screen Y down
462+
463+
Vector2 lutUV = draw.Font.GlyphLutUVs.TryGetValue(draw.CodePoint, out Vector2 uv)
464+
? uv
465+
: Vector2.Zero;
466+
467+
Vector4 dilation = new Vector4(lutUV.X, lutUV.Y, invJxx, invJyy);
460468

461469
int baseVertex = _glyphCount * 4;
462470

@@ -465,31 +473,31 @@ private bool AppendGlyphQuad(in QueuedDraw draw)
465473
Pos = new Vector4(px0, py0, -InvSqrt2, -InvSqrt2),
466474
Tex = new Vector4(ex0, ey1, packedBandTexLoc, packedBandCount),
467475
Color = color,
468-
Bnd = bnd
476+
Dilation = dilation
469477
};
470478

471479
_vertices[baseVertex + 1] = new FormeVertex
472480
{
473481
Pos = new Vector4(px1, py0, InvSqrt2, -InvSqrt2),
474482
Tex = new Vector4(ex1, ey1, packedBandTexLoc, packedBandCount),
475483
Color = color,
476-
Bnd = bnd
484+
Dilation = dilation
477485
};
478486

479487
_vertices[baseVertex + 2] = new FormeVertex
480488
{
481489
Pos = new Vector4(px1, py1, InvSqrt2, InvSqrt2),
482490
Tex = new Vector4(ex1, ey0, packedBandTexLoc, packedBandCount),
483491
Color = color,
484-
Bnd = bnd
492+
Dilation = dilation
485493
};
486494

487495
_vertices[baseVertex + 3] = new FormeVertex
488496
{
489497
Pos = new Vector4(px0, py1, -InvSqrt2, InvSqrt2),
490498
Tex = new Vector4(ex0, ey0, packedBandTexLoc, packedBandCount),
491499
Color = color,
492-
Bnd = bnd
500+
Dilation = dilation
493501
};
494502

495503
int baseIndex = _glyphCount * 6;
@@ -534,14 +542,16 @@ private readonly struct QueuedDraw
534542
{
535543
internal FormeFontDevice Font { get; }
536544
internal FormeGlyph Glyph { get; }
545+
internal int CodePoint { get; }
537546
internal Vector2 Position { get; }
538547
internal float SizePixels { get; }
539548
internal Color Color { get; }
540549

541-
internal QueuedDraw(FormeFontDevice font, FormeGlyph glyph, Vector2 position, float sizePixels, Color color)
550+
internal QueuedDraw(FormeFontDevice font, FormeGlyph glyph, int codePoint, Vector2 position, float sizePixels, Color color)
542551
{
543552
Font = font;
544553
Glyph = glyph;
554+
CodePoint = codePoint;
545555
Position = position;
546556
SizePixels = sizePixels;
547557
Color = color;

src/Forme.MonoGame/FormeVertex.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ internal struct FormeVertex
1616
// TEXCOORD1: xyzw = normalized RGBA color (R/255, G/255, B/255, A/255)
1717
internal Vector4 Color;
1818

19-
// TEXCOORD2: bandScaleX, bandScaleY, bandOffsetX, bandOffsetY
20-
internal Vector4 Bnd;
19+
// TEXCOORD2: x=band LUT U, y=band LUT V, z=invJxx, w=invJyy
20+
internal Vector4 Dilation;
2121

2222
internal const int SizeInBytes = 4 * 4 * 4;
2323

102 Bytes
Binary file not shown.

src/Forme.MonoGame/Shaders/FormeShader.fx

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
// POSITION0: pos.xy = object-space position, pos.zw = outward corner normal
1717
// TEXCOORD0: tex.xy = em-space sample coords, tex.z = packed band tex location, tex.w = band count
1818
// TEXCOORD1: color.xyzw = normalized RGBA (R/255, G/255, B/255, A/255)
19-
// TEXCOORD2: bnd = (bandScaleX, bandScaleY, bandOffsetX, bandOffsetY)
19+
// TEXCOORD2: dil = (lutU, lutV, invJxx, invJyy)
2020
//
2121
// This shader targets SM3 for OpenGL and SM4 for DirectX 11. No integer bitwise ops are used
2222
// so that the shader compiles cleanly at SM3 where they are unavailable.
@@ -54,6 +54,17 @@ sampler2D bandSampler = sampler_state
5454
AddressV = Clamp;
5555
};
5656

57+
texture2D bandLUTTexture;
58+
sampler2D bandLUTSampler = sampler_state
59+
{
60+
Texture = <bandLUTTexture>;
61+
MinFilter = Point;
62+
MagFilter = Point;
63+
MipFilter = None;
64+
AddressU = Clamp;
65+
AddressV = Clamp;
66+
};
67+
5768
float2 curveTexSize; // (width, height) in texels
5869
float2 bandTexSize; // (width, height) in texels
5970

@@ -62,37 +73,27 @@ struct VSInput
6273
float4 pos : POSITION0; // xy = position, zw = outward corner normal
6374
float4 tex : TEXCOORD0; // xy = em-space coords, z = packed band tex location, w = band count
6475
float4 color : TEXCOORD1; // normalized RGBA color
65-
float4 bnd : TEXCOORD2; // bandScaleX, bandScaleY, bandOffsetX, bandOffsetY
76+
float4 dil : TEXCOORD2; // x=lutU, y=lutV, z=invJxx, w=invJyy
6677
};
6778

6879
struct VSOutput
6980
{
7081
float4 position : SV_POSITION;
71-
float2 texcoord : TEXCOORD0; // em-space sample coords
72-
float4 banding : TEXCOORD1; // band transform
73-
float4 glyphLoc : TEXCOORD2; // packed band texture location and band count
74-
float4 color : TEXCOORD3; // RGBA color
82+
float2 texcoord : TEXCOORD0; // em-space sample coords (dilated)
83+
float4 glyphLoc : TEXCOORD1; // xy=packed band tex loc + band count, zw=band LUT UV
84+
float4 color : TEXCOORD2; // RGBA color
7585
};
7686

7787
VSOutput VS_Main(VSInput input)
7888
{
7989
VSOutput output;
8090

81-
// NOTE:
82-
// Dynamic dilation (Lengyel 2017, Section 4) is not implemented.
83-
// The math assumes a perspective projection to compute a half-pixel
84-
// outward displacement along the vertex normal, but with an orthographic
85-
// projection the intermediate values diverge and corrupt glyph geometry.
86-
//
87-
// Dilation is a cosmetic sub-pixel refinement and not required for
88-
// correct coverage rendering.
89-
//
90-
// If you can get it working with orthographic projection, please open
91-
// a pull request.
92-
output.position = mul(float4(input.pos.xy, 0, 1), forme_matrix);
93-
output.texcoord = input.tex.xy;
94-
output.glyphLoc = float4(input.tex.z, input.tex.w, 0.0, 0.0);
95-
output.banding = input.bnd;
91+
float2 n = input.pos.zw;
92+
float2 screenOffset = n * 0.5;
93+
94+
output.position = mul(float4(input.pos.xy + screenOffset, 0, 1), forme_matrix);
95+
output.texcoord = input.tex.xy + screenOffset * float2(input.dil.z, input.dil.w);
96+
output.glyphLoc = float4(input.tex.z, input.tex.w, input.dil.x, input.dil.y);
9697
output.color = input.color;
9798

9899
return output;
@@ -263,7 +264,8 @@ float FormeRender(float2 renderCoord, float4 bandTransform, float4 glyphTexInfo)
263264

264265
float4 PS_Main(VSOutput input) : COLOR0
265266
{
266-
float coverage = FormeRender(input.texcoord, input.banding, input.glyphLoc);
267+
float4 bandTransform = tex2D(bandLUTSampler, input.glyphLoc.zw);
268+
float coverage = FormeRender(input.texcoord, bandTransform, input.glyphLoc);
267269
return float4(input.color.rgb * coverage, coverage * input.color.a);
268270
}
269271

92 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)