In the first part of this series, we discussed multi-channel signed distance field fonts, how they work, and how to generate them as content.
Today, we’ll look at the object model types in our game or game framework needed to render text using our MSDF content. This article will cover the techniques employed to generate our vertex data as well as the various shaders we use to color the pixels.
At this article’s completion, we’ll have covered everything we need to have fully functional MSDF fonts in our games.
How MSDF Text Models are Generated
Before diving into our code for rendering MSDF font text, let’s take a step back and do a high-level overview of how we will generate the vertex data for the text.
Creating a model for a string of text will require the careful placement of consecutive quad primitives, each with vertices positioned in a manner specific to the glyph being mapped to it. The spacing between each consecutive glyph must be consistent with the font’s design, with any relevant kerning adjustments applied.
Luckily, all the necessary metrics are provided in the MSDF layout JSON file generated by msdf-atlas-gen
and have been packed into the content pipeline by our pipeline extension. To understand these metrics, however, we’ll need to first touch on some fundamentals of typography.
Basic Typographic Concepts
Let’s look at some basic typeface characteristics that require consideration if we wish to render text from our generated MSDF font content.
Have no illusions, there exist far better places on the net for learning about typography; nevertheless, I shall do my best to illuminate some basic terms and concepts using the above image of some rendered MSDF text as a reference.
Baseline
The baseline is precisely what it sounds like. It is the line upon which most letters rest. It is also where our pen rests when we’re between letters.
The baseline is important in that most of the metrics found in the MSDF layout file generated by msdf-atlas-gen
are relative to it.
X-height
The x-height is the height of the lowercase ‘x’ glyph and other lowercase characters with no ascenders or descenders in a typeface.
This metric isn’t all that relevant for us, as it is not present in the generated layout data; it’s just good to know about!
Ascender
Ascenders are upward strokes found in certain (and typically lowercase) letters that extend well beyond the x-height. The ascender line depicts the height of the highest ascender.
The ascender distance, represented by the ascender
JSON property in the generated layout data, is the vertical distance between the typeface’s baseline and the ascender line. This is a very important metric for us, as it is necessary for the initial positioning of our cursor/pen.
Descender
Descenders are the opposite of ascenders: downward strokes found in certain (and typically lowercase) letters extending beyond the baseline. The descender line depicts the height of the lowest descender.
The descender distance, represented by the descender
JSON property in the generated layout data, is the vertical distance between the typeface’s baseline and the descender line.
Writing with a Vertex Pen
Now that we have a rudimentary understanding of some of the metrics in our MSDF layout data, we can begin to devise a process for generating the model data needed to render strings of text.
We will (loosely speaking) write out our vertex data in much the same way we might write the letters with a pen on paper (OK…very loosely speaking).
We are given an initial position at the outset of model data generation. As is the case with most positional resources, this position specifies the top-left corner of the model.
However, as was stated before, layout metrics are relative to the baseline, which, as you can see in the image above, is nowhere near our initial position. In fact, because the initial position is at the top-left corner of our model, it follows that it rests on the ascender line.
In order to move our cursor or pen to the baseline, we perform a vertical translation of our initial position by multiplying it by the vector normal (perpendicular) to the baseline at a magnitude equal to that of the ascender distance metric.
All of this will shift our cursor’s location to the the proper cursor start point.
Modeling a Glyph
We can now begin modeling the first glyph in the string from our baseline cursor position. Two triangle primitives form a quadrilateral polygon, encompassing a glyph’s shape and texture.
Vertices are created and positioned based on the bounding rectangle found in the planeBounds
JSON property for the glyph in the MSDF layout data. They are made in the order shown in the image above (i.e., starting at 0 and ending at 3).
The vertices are then indexed as we index all flat quadrilateral polygons: in a clockwise winding order. We don’t want any part of our glyphs culled!
Advancing Onwards!
Once a glyph is modeled, it’s time to move ever forward to the next one. This is done by adding an advance width to the current position of our cursor.
The advance width is the distance between the current glyph’s origin and the next glyph’s origin; in other words, the distance between two successive cursor positions.
It is a character-specific value, stored in the advance
JSON property for the glyph. Adding it to the cursor location will position it such that the next glyph can be drawn. If a kerning adjustment is defined for the character pair in question, then that must also be added to the cursor position.
After advancement is complete, we basically repeat all the above steps until we reach the end of the string. And that’s all she wrote.
Domain Model
We now need to take everything we discussed above, and put it into action with our code.
The previous article in this series covered the classes in our content pipeline extension and how they were responsible for generating our font data and packaging it for later consumption. We’ll now look at our font domain model, which is the object model containing the objects we’ll be using to draw text on the screen.
Our MSDF font domain model comprises the following types:
DistanceFieldFont
: The primary object type that exposes text rendering methods, produced by the content pipeline.FontCharacteristics
: Metrics and statistics applicable to a font atlas as whole, sourced the frommsdf-atlas-gen
layout output.FontGlyph
: A typographic representation for a single unicode character.CharacterPair
/KerningPair
: Defines space adjustments between character pairings.FontModelData
: Generates the vertex data needed to render text using our font.
A number of these types do not warrant coverage in an article write-up; for further information, feel free to peruse the source code of any of the types not covered.
FontModelData – Generating Model Data for Text
This class will generate vertex data required to render a model of text using our MSDF font content. It is an implementation of the vertex generation process described in the previous sections above.
FontModelData.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 | /// <summary> /// Provides the vertex data required to render a 3D model of flat signed distance field font text. /// </summary> public sealed class FontModelData : QuadModelData < VertexPositionOutlinedColorTexture > { private readonly DistanceFieldFont _font; private readonly Color _fillColor; private readonly Color _strokeColor; /// <summary> /// Initializes a new instance of the <see cref="FontModelData"/> class. /// </summary> /// <param name="font">The multi-channel signed distance font to vertex data for.</param> /// <param name="color">The color of the text.</param> public FontModelData ( DistanceFieldFont font, Color color) : this (font, color, default ) { FillOnly = true ; } /// <summary> /// Initializes a new instance of the <see cref="FontModelData"/> class. /// </summary> /// <param name="font">The multi-channel signed distance font to vertex data for.</param> /// <param name="fillColor">The inner color of the text.</param> /// <param name="strokeColor">The outer color of the text.</param> public FontModelData ( DistanceFieldFont font, Color fillColor, Color strokeColor) : base ( VertexPositionOutlinedColorTexture .VertexDeclaration) { Require .NotNull(font, nameof (font)); _font = font; _fillColor = fillColor; _strokeColor = strokeColor; } /// <summary> /// Gets a value indicating if the coloring of the text consists of only a single fill color, /// without any outlining. /// </summary> public bool FillOnly { get ; } /// <summary> /// Adds 3D modeling data for quadrilateral surfaces that can be mapped to the specified text /// using glyphs found in a font atlas texture during rendering. /// </summary> /// <param name="text">The text to prepare modeling data for.</param> /// <param name="position">The position of the top-left corner of the text.</param> /// <param name="scale">The amount of scaling to apply to the text.</param> /// <remarks> /// <para> /// In order to model a glyph, we create <see cref="VertexPositionOutlinedColorTexture"/> values /// whose texture coordinates are based on the generated atlas coordinates for the glyph. /// These coordinates must be normalized such that they are in a range from 0 to 1 where (0, 0) /// is the top-left of the texture and (1, 1) is the bottom-right of the texture. /// </para> /// <para> /// Much like what we do with <see cref="QuadTextureModelData"/>, we divide the atlas (source) /// rectangle's individual vertex coordinates by the appropriate font atlas texture dimension, /// based on the axis that the particular vertex rests on. /// </para> /// <para> /// Unlike what we do with <see cref="QuadTextureModelData"/>, the source rectangle has no /// impact on the position of the vertices; rather, we make base the position off of the /// generated plane coordinates for the glyph. These coordinates are relative to the baseline /// cursor, and allow us to position the glyphs appropriately. /// </para> /// </remarks> public void AddText( string text, Vector2 position, float scale) { if ( string .IsNullOrEmpty(text)) return ; var advanceDirection = new Vector2 (1, 0); // The vertical direction is the vector that's perpendicular to our advance direction. var verticalDirection = -1 * new Vector2 (advanceDirection.Y, -advanceDirection.X); // The provided position vector value specifies where the top-left corner of the text // should be placed. The generated signed distance glyph plane coordinates, however, // are meant to be applied relative to the baseline cursor. Therefore, we subtract // the distance between the baseline and ascender line from our initial cursor, // giving us a cursor now positioned at the baseline. Vector2 cursorStart = position + verticalDirection * scale * _font.Characteristics.Ascender * -1; Vector2 cursor = cursorStart; Vector2 scaledAdvance = advanceDirection * scale; PointF ? offset = position; for ( int i = 0; i < text.Length; i++) { char character = text[i]; FontGlyph glyph = _font.FindGlyph(character); if (! char .IsWhiteSpace(character)) AddGlyph(glyph, _font.Characteristics, cursor, scaledAdvance, offset); char ? nextCharacter = i < text.Length - 1 ? text[i + 1] : null ; cursor += _font.GetNextAdvance(character, nextCharacter, advanceDirection, scale); // Only apply an offset the starting quad corner of the text. Another offset may be // needed if a line break is inserted. offset = null ; } } /// <inheritdoc/> protected override Vector3 GetVertexPosition( VertexPositionOutlinedColorTexture vertex) => vertex.Position; private void AddGlyph( FontGlyph glyph, FontCharacteristics characteristics, Vector2 cursor, Vector2 scaledAdvance, PointF ? offset) { Require .NotNull(glyph, nameof (glyph)); Require .NotNull(characteristics, nameof (characteristics)); RectangleF atlasBounds = glyph.AtlasBounds; float texelLeft = atlasBounds.X / characteristics.Width; float texelRight = (atlasBounds.X + atlasBounds.Width) / characteristics.Width; float texelTop = atlasBounds.Y / characteristics.Height; float texelBottom = (atlasBounds.Y + atlasBounds.Height) / characteristics.Height; Vector2 verticalAdvance = -1 * new Vector2 (scaledAdvance.Y, -scaledAdvance.X); RectangleF planeBounds = glyph.PlaneBounds; Vector2 positionLeft = scaledAdvance * planeBounds.Left; Vector2 positionRight = scaledAdvance * planeBounds.Right; Vector2 positionTop = verticalAdvance * planeBounds.Top; Vector2 positionBottom = verticalAdvance * planeBounds.Bottom; var vertexTopLeft = new VertexPositionOutlinedColorTexture ( new Vector3 (cursor + positionTop + positionLeft, 0), _fillColor, _strokeColor, new Vector2 (texelLeft, texelTop)); var vertexTopRight = new VertexPositionOutlinedColorTexture ( new Vector3 (cursor + positionTop + positionRight, 0), _fillColor, _strokeColor, new Vector2 (texelRight, texelTop)); var vertexBottomLeft = new VertexPositionOutlinedColorTexture ( new Vector3 (cursor + positionBottom + positionLeft, 0), _fillColor, _strokeColor, new Vector2 (texelLeft, texelBottom)); var vertexBottomRight = new VertexPositionOutlinedColorTexture ( new Vector3 (cursor + positionBottom + positionRight, 0), _fillColor, _strokeColor, new Vector2 (texelRight, texelBottom)); AddVertices(vertexTopLeft, vertexTopRight, vertexBottomLeft, vertexBottomRight); if (offset != null ) AddQuadCornerOffset(vertexTopLeft, offset.Value); } } |
The code above carries out the vertex data generation process detailed in the previous sections.
You can find the up-to-date source here in the (very likely) event that the code changes following the publishing of this article.
This type can be used on its own, but it is mainly used by the primary model type for MSDF fonts, which we’ll be covering next.
DistanceFieldFont – Provisioner of MSDF Goodness
Here’s the star player of the MSDF font gang. This type serves as the direct output of MSDF font content read from the content pipeline, and it provides the functionality needed to generate model data and then render said data onto the screen.
DistanceFieldFont.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 | /// <summary> /// Provides a multi-channel signed distance field font. /// </summary> public sealed class DistanceFieldFont { private readonly GraphicsDevice _device; private readonly Dictionary < char , FontGlyph > _glyphs; private readonly Dictionary < CharacterPair , KerningPair > _kernings; /// <summary> /// Initializes a new instance of the <see cref="DistanceFieldFont"/> class. /// </summary> /// <param name="device">The graphics device to use when rendering the font's models.</param> /// <param name="atlas">The texture of the font's atlas, generated with distance fields.</param> /// <param name="characteristics">The font's characteristics and metrics.</param> /// <param name="glyphs"> /// A mapping between unicode characters and their typographic representations. /// </param> /// <param name="kernings"> /// A mapping between unicode character pairs and the adjustments of space between them. /// </param> public DistanceFieldFont ( GraphicsDevice device, Texture2D atlas, FontCharacteristics characteristics, Dictionary < char , FontGlyph > glyphs, Dictionary < CharacterPair , KerningPair > kernings) { Require .NotNull(device, nameof (device)); Require .NotNull(atlas, nameof (atlas)); Require .NotNull(characteristics, nameof (characteristics)); Require .NotNull(glyphs, nameof (glyphs)); Require .NotNull(kernings, nameof (kernings)); _device = device; Atlas = atlas; Characteristics = characteristics; _glyphs = glyphs; _kernings = kernings; } /// <summary> /// Gets the texture of this font's atlas, generated with distance fields. /// </summary> public Texture2D Atlas { get ; } /// <summary> /// Gets this font's characteristics and metrics. /// </summary> public FontCharacteristics Characteristics { get ; } /// <summary> /// Generates a model required to render the specified text when it is being submitted for /// drawing. /// </summary> /// <param name="text">The text to prepare a model for.</param> /// <param name="position">The position of the top-left corner of the text.</param> /// <param name="color">The color of the text.</param> /// <param name="scale">The amount of scaling to apply to the text.</param> /// <returns>A <see cref="IModelRenderer"/> instance that will render the model.</returns> public IModelRenderer AddModel( string text, Vector2 position, Color color, float scale) => AddModel(text, position, color, scale, false ); /// <summary> /// Generates a model required to render the specified text when it is being submitted for /// drawing. /// </summary> /// <param name="text">The text to prepare a model for.</param> /// <param name="position">The position of the top-left corner of the text.</param> /// <param name="color">The color of the text.</param> /// <param name="scale">The amount of scaling to apply to the text.</param> /// <param name="optimizeForSmallText"> /// Value indicating if the text should be rendered using a shader optimized for smaller text, /// the use of which helps to avoid the artifacting normally observed when attempting to /// render signed distance fonts at small scale. /// </param> /// <returns>A <see cref="IModelRenderer"/> instance that will render the model.</returns> /// <remarks> /// If text being rendered with the default shader exhibit artifacts, it is recommended to pass /// <c>true</c> for <c>optimizeForSmallText</c>. The small-optimized shader is also able to /// render larger text, however the additional thickness applied in order to account for /// artifacting will also become more noticeable as the scale increases. /// </remarks> public IModelRenderer AddModel( string text, Vector2 position, Color color, float scale, bool optimizeForSmallText) { var fontData = new FontModelData ( this , color); return AddModel(fontData, text, position, scale, optimizeForSmallText); } /// <summary> /// Generates a model required to render the specified text when it is being submitted for /// drawing. /// </summary> /// <param name="text">The text to prepare a model for.</param> /// <param name="position">The position of the top-left corner of the text.</param> /// <param name="fillColor">The fill color of the text.</param> /// <param name="strokeColor">The stroke color of the text.</param> /// <param name="scale">The amount of scaling to apply to the text.</param> /// <returns>A <see cref="IModelRenderer"/> instance that will render the model.</returns> public IModelRenderer AddModel( string text, Vector2 position, Color fillColor, Color strokeColor, float scale) => AddModel(text, position, fillColor, strokeColor, scale, false ); /// <summary> /// Generates a model required to render the specified text when it is being submitted for /// drawing. /// </summary> /// <param name="text">The text to prepare a model for.</param> /// <param name="position">The position of the top-left corner of the text.</param> /// <param name="fillColor">The fill color of the text.</param> /// <param name="strokeColor">The stroke color of the text.</param> /// <param name="scale">The amount of scaling to apply to the text.</param> /// <param name="optimizeForSmallText"> /// Value indicating if the text should be rendered using a shader optimized for smaller text, /// the use of which helps to avoid the artifacting normally observed when attempting to /// render signed distance fonts at small scale. /// </param> /// <returns>A <see cref="IModelRenderer"/> instance that will render the model.</returns> /// <remarks> /// If text being rendered with the default shader exhibit artifacts, it is recommended to /// pass <c>true</c> for <c>optimizeForSmallText</c>. The small-optimized shader is also /// able to render larger text, however the additional thickness applied in order to account /// for artifacting will also become more noticeable as the scale increases. /// </remarks> public IModelRenderer AddModel( string text, Vector2 position, Color fillColor, Color strokeColor, float scale, bool optimizeForSmallText) { var fontData = new FontModelData ( this , fillColor, strokeColor); return AddModel(fontData, text, position, scale, optimizeForSmallText); } /// <summary> /// Retrieves the typographic representation of the specified unicode character from this font. /// </summary> /// <param name="character"> /// The unique character to retrieve the typographic representation for. /// </param> /// <returns>The <see cref="FontGlyph"/> representing <c>character</c>.</returns> public FontGlyph FindGlyph( char character) { if (!_glyphs.TryGetValue(character, out FontGlyph ? glyph)) { throw new ArgumentException ( Strings .GlyphNotInFont.InvariantFormat(character), nameof (character)); } return glyph; } /// <summary> /// Determines the next advance width to apply to the cursor given the specified character /// sequence and direction. /// </summary> /// <param name="current">The character the cursor is currently positioned at.</param> /// <param name="next"> /// The next character to advance the cursor to, or null if the current character was the /// last one. /// </param> /// <param name="advanceDirection">The direction to advance the cursor.</param> /// <param name="scale">The amount of scaling to apply to the text.</param> /// <returns>The next advance width to apply to the current cursor position.</returns> public Vector2 GetNextAdvance( char current, char ? next, Vector2 advanceDirection, float scale) { FontGlyph currentGlyph = FindGlyph(current); Vector2 advance = advanceDirection * currentGlyph.Advance * scale; if (next.HasValue && _kernings.TryGetValue( new CharacterPair (current, next.Value), out KerningPair ? kerning)) { advance += advanceDirection * kerning.Advance * scale; } return advance; } private IModelRenderer AddModel( FontModelData fontData, string text, Vector2 position, float scale, bool optimizeForSmallText) { if (! string .IsNullOrEmpty(text)) fontData.AddText(text, position, scale); var model = new StaticModel (_device, Atlas, fontData); return new DistanceFieldFontRenderer ( this , model, optimizeForSmallText, !fontData.FillOnly); } /// <summary> /// Provides a renderer of signed distance field font models using shaders appropriate to the /// size of the text. /// </summary> private sealed class DistanceFieldFontRenderer : IModelRenderer { private readonly DistanceFieldFont _font; private readonly StaticModel _model; private readonly bool _useSmallShader; private readonly bool _useStrokedShader; /// <summary> /// Initializes a new instance of the <see cref="DistanceFieldFontRenderer"/> class. /// </summary> /// <param name="font">The multi-channel signed distance font to render.</param> /// <param name="model">A generated model of static font data for rendering.</param> /// <param name="useSmallShader" /// >Value indicating whether a shader optimized for rendering small text is used. /// </param> /// <param name="useStrokedShader"> /// Value indicating whether a shader that applies an outline is used. /// </param> public DistanceFieldFontRenderer ( DistanceFieldFont font, StaticModel model, bool useSmallShader, bool useStrokedShader) { _font = font; _model = model; _useSmallShader = useSmallShader; _useStrokedShader = useStrokedShader; } /// <inheritdoc/> public void Draw() => Draw( Matrix .Identity); /// <inheritdoc/> public void Draw( Matrix view) { GraphicsDevice device = _font._device; var projection = Matrix .CreateOrthographicOffCenter(0, device. Viewport .Width, device. Viewport .Height, 0, 0, -1); var effect = new DistanceFieldFontEffect (device) { WorldViewProjection = projection, AtlasSize = new Vector2 (_font.Characteristics.Width, _font.Characteristics.Height), DistanceRange = _font.Characteristics.DistanceRange, Texture = _font.Atlas }; effect.CurrentTechnique = GetTechnique(effect); _model.Draw(effect); } private EffectTechnique GetTechnique( Effect effect) => (_useSmallShader, _useStrokedShader) switch { ( false , false ) => effect.Techniques[ DistanceFieldFontEffect .LargeTextTechnique], ( true , false ) => effect.Techniques[ DistanceFieldFontEffect .SmallTextTechnique], ( false , true ) => effect.Techniques[ DistanceFieldFontEffect .LargeStrokedTextTechnique], ( true , true ) => effect.Techniques[ DistanceFieldFontEffect .SmallStrokedTextTechnique] }; } } |
The DistanceFieldFont
class contains all information pertaining to the font and offers methods to generate models purposed for rendering text. We’ll have some sample usage code for this type at the end of the article.
I have no doubt that I’ll be adding improvements to this over time, but I do feel that the code as it is now is a rather suitable first incarnation. The up-to-date source is here, as always, on the Bad Echo repo.
As one can see by looking at the code, after we generate the model data, we return an IModelRenderer
instance. When asked to draw to the screen, these renderers use a custom effect class type: DistanceFieldFontEffect
.
And that’s the next stop on this MSDF tour: our custom shaders.
Shaders
We require the use of specialized shaders in order to translate the multi-channel signed distance fields into beautiful text. We will spend some time covering the various different shaders and the effects class that exposes them for use.
Font Effect Class
Because some of these shaders will need to be supplied with MSDF-related generation parameters, a custom effect class was created to simplify the process.
DistanceFieldFontEffect.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 | /// <summary> /// Provides parameters and the shaders that use them which are needed to render multi-channel /// signed distance field font text. /// </summary> public sealed class DistanceFieldFontEffect : Effect , ITextureEffect { private EffectParameter _worldViewProjectionParam; private EffectParameter _atlasSizeParam; private EffectParameter _distanceRangeParam; private EffectParameter _textureParam; /// <summary> /// Initializes a new instance of the <see cref="DistanceFieldFontEffect"/> class. /// </summary> /// <param name="device">The graphics device used for rendering.</param> public DistanceFieldFontEffect ( GraphicsDevice device) : base (device, Shaders. DistanceFieldFontEffect ) { CacheEffectParameters(); } /// <summary> /// Initializes a new instance of the <see cref="DistanceFieldFontEffect"/> class. /// </summary> /// <param name="cloneSource"> /// The <see cref="DistanceFieldFontEffect"/> instance to clone. /// </param> private DistanceFieldFontEffect ( DistanceFieldFontEffect cloneSource) : base (cloneSource) { CacheEffectParameters(); } /// <summary> /// Gets the name of the technique designed to render larger text optimally. /// </summary> public static string LargeTextTechnique => "DistanceFieldFont" ; /// <summary> /// Gets the name of the technique designed to render smaller text optimally. /// </summary> public static string SmallTextTechnique => "SmallDistanceFieldFont" ; /// <summary> /// Gets the name of the technique designed to render outlined larger text optimally. /// </summary> public static string LargeStrokedTextTechnique => "StrokedDistanceFieldFont" ; /// <summary> /// Gets the name of the technique designed to render outlined smaller text optimally. /// </summary> public static string SmallStrokedTextTechnique => "StrokedSmallDistanceFieldFont" ; /// <summary> /// Gets or sets the world, view, and projection transformation matrix. /// </summary> public Matrix WorldViewProjection { get => _worldViewProjectionParam.GetValueMatrix(); set => _worldViewProjectionParam.SetValue(value); } /// <summary> /// Gets or sets the width and height of the atlas texture. /// </summary> public Vector2 AtlasSize { get => _atlasSizeParam.GetValueVector2(); set => _atlasSizeParam.SetValue(value); } /// <summary> /// Gets or sets the distance field range in pixels. /// </summary> public float DistanceRange { get => _distanceRangeParam.GetValueSingle(); set => _distanceRangeParam.SetValue(value); } /// <summary> /// Gets or sets the font atlas texture. /// </summary> public Texture2D Texture { get => _textureParam.GetValueTexture2D(); set => _textureParam.SetValue(value); } /// <summary> /// Creates a clone of the current <see cref="DistanceFieldFontEffect"/> instance. /// </summary> /// <returns>A cloned <see cref="Effect"/> instance of this.</returns> public override Effect Clone() => new DistanceFieldFontEffect ( this ); [ MemberNotNull ( nameof (_worldViewProjectionParam), nameof (_atlasSizeParam), nameof (_distanceRangeParam), nameof (_textureParam))] private void CacheEffectParameters() { _worldViewProjectionParam = Parameters[ nameof (WorldViewProjection)]; _atlasSizeParam = Parameters[ nameof (AtlasSize)]; _distanceRangeParam = Parameters[ nameof (DistanceRange)]; _textureParam = Parameters[ nameof (Texture)]; } } |
Four custom parameters can be set here. The world-view-projection matrix and texture parameters are fairly standard; the other two are characteristics pertaining to our MSDF font atlas.
The projection matrix is a standard one geared for 2D rendering, set up by the IModelRenderer
instance in DistanceFieldFont
. An identity matrix for the world works just fine, and the view matrix will be whatever was passed to IModelRenderer.Draw
.
The other thing this effect class does is load the bytecode for our *.fx
file containing all our shader HLSL. Let us look at that now!
Shader for Normal-Sized Text
We use two varieties of pixel shaders to draw our MSDF text: a standard one for normal- and large-sized text and one optimized for small-sized text. The reason for this is due to the fact that MSDF struggles to render small-sized text 100% correctly, resulting in visible artifacts if the standard shader is used.
The standard shader is more or less based on the shader suggested by the author of msdfgen
in the project’s README. Still, I don’t like to type code blindly, so I attempted to understand the purpose behind it all while documenting my thoughts in the comments.
DistanceFontFieldEffect.fx – Normal-Sized Pixel Shader
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | // Returns the channel value that is in the middle of a data set consisting of the provided color's // RGB values. float Median( float3 color) { return max(min(color.r, color.g), min(max(color.r, color.g), color.b)); } // A pixel shader to use for normal- and large-sized text. float4 DistancePixelShader( VSOutput input) : COLOR { // Divide the distance field range by the atlas size to give us a distance range applicable to // texels. float2 texelDistanceRange = DistanceRange / AtlasSize; float3 sample = tex2D (AtlasSampler, input.TexCoord).rgb; // Vertex color values go from 0.0 to 1.0. The center of a pixel is located at (0, 0), however // the edges are located at +/- 0.5. We subtract 0.5 to align with the pixels. float signedDistance = Median(sample) - 0.5f; // Calculate the relation between the texel distance range and the changes across the input // coordinates, and apply this to our signed distance. This converts the distance to screen // pixels. signedDistance *= dot (texelDistanceRange, 0.5f / fwidth (input.TexCoord)); // Align the distance value back to a normal vertex color value range by adding 0.5 back to it. float opacity = clamp (signedDistance + 0.5f, 0.0f, 1.0f); return input.FillColor * opacity; } |
The magic here, which separates this shader from the single-channel signed distance field kind, is the computing of the median RGB value, giving us the encoded signed field distance.
Refer to the code comments for more information on what the shader is doing if it pleases you.
Shader for Small-Sized Text
In order to maintain glyph design fidelity, it is recommended that font atlases be generated at a resolution as high as practicable. The large size of the glyphs in the atlas will preserve the most minute details.
Unfortunately, the typical MSDF shader approach starts to run into trouble when we attempt to render tiny text using large glyph sources. Our once beautiful text now appears aliased and no longer true to the original design.
A different approach (and a bunch more math) is required to render small text properly. A separate shader was created for this very purpose. The relevant code for the shader will be provided below, with an attempt at explaining what it is doing following right after.
DistanceFontFieldEffect.fx – Small-Sized Pixel Shader
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | // Calculates the opacity to apply with respect to screen-space x- and y-coordinates given the // specified encoded signed distance. float GetOpacityFromDistance( float signedDistance, float2 Jdx, float2 Jdy) { // Continue reading this article for attempted explanation of this code. const float distanceLimit = sqrt (2.0f) / 2.0f; const float thickness = 1.0f / DistanceRange; float2 gradientDistance = SafeNormalize( float2 ( ddx (signedDistance), ddy (signedDistance))); float2 gradient = float2 (gradientDistance.x * Jdx.x + gradientDistance.y * Jdy.x, gradientDistance.x * Jdx.y + gradientDistance.y * Jdy.y); float scaledDistanceLimit = min(thickness * distanceLimit * length(gradient), 0.5f); return smoothstep(-scaledDistanceLimit, scaledDistanceLimit, signedDistance); } // A pixel shader to use for small- and normal-sized text. // This will render small-sized text more accurately without artifacts. // As the text size increases however, the text may appear 'phatter' than it should. // In that case, use the standard DistancePixelShader. float4 SmallDistancePixelShader( VSOutput input) : COLOR { float2 pixelCoord = input.TexCoord * AtlasSize; float2 Jdx = ddx (pixelCoord); float2 Jdy = ddy (pixelCoord); float3 sample = tex2D (AtlasSampler, input.TexCoord).rgb; float signedDistance = Median(sample) - 0.5f; float opacity = GetOpacityFromDistance(signedDistance, Jdx, Jdy); float4 color; // Correct for gamma, 2.2 is a valid gamma for most LCD monitors. color.a = pow ( abs (input.FillColor.a * opacity), 1.0f / 2.2f); color.rgb = input.FillColor.rgb * color.a; return color; } |
The above shader employs concepts discussed by James M. Lan Verth in his series of articles on the topic, and it is based on the implementation of said concepts by the Cinder-SdfText project.
Having mentioned that, I shall now attempt to spit out a quick explanation of what the code is doing.
How to Render Tiny Text Optimally
The distance from the center of a pixel to one of its corners is the square root of 2 divided by 2. This comes from the fact that the length of a diagonal is the length of a side (1) multiplied by the square root of 2; dividing this by 2 gives us the length from the center.
If we use this distance value, which we will refer to as the distance limit, to define the minimum and maximum range in a smoothstep
function, along with the signed distance value as the value to be interpolated, we will have found the appropriate amount of pixel coverage (the inverse of which is the opacity that needs to be applied).
This only works, however, if the size of the geometry we’re rendering is the same size as the distance field’s bounding rectangle in the source texture. This is clearly not the case with what we want to do, as our atlas texture will more than likely differ in size from our target rendering area.
Scaling Our Problems Away
Multiplying our distance limit by the partial derivative of non-normalized texture pixel coordinates will provide us with uniform scaling of the distance limit. In order to account for non-uniform scaling and perspective, however, we need to scale our distance limit based on how a vector normal to the outline curve is transformed.
We do this by creating a gradient vector that has been normalized to avoid approximation errors when near the edge of the shape. We take this gradient and then multiply it by a Jacobian matrix containing our coordinate value’s first-order partial derivatives.
This will give us our scaled distance limit, which we can then use in the smoothstep
function as described above. And that’s it!
Putting It All Together
Let’s see how we go about using new MSDF font classes to write some text.
Testing Our Distance Fonts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | // A one time setup of our text renderers... DistanceFieldFont lato = Content.Load< DistanceFieldFont >( "Fonts\\Lato" ); IModelRenderer smallText = lato.AddModel( "Hello" , new Vector2 (225, 100), Color .White, 11f, true ); IModelRenderer bigText = lato.AddModel( "Hello" , new Vector2 (220, 175), Color .White, 90f); IModelRenderer outlinedText = lato.AddModel( "Hello" , new Vector2 (220, 350), Color .White, 90); // And then wherever our drawing loop is... smallText.Draw(); bigText.Draw(); outlinedText.Draw(); |
The above code will produce the resulting output:
The outlined text is using a shader that wasn’t covered in our article — but it isn’t too different from the shaders we did cover. You can see how they work as well as the rest of the *.fx file’s contents by looking at the full source code.
The MSDF font source code in its entirety is part of the Bad Echo Game Framework.
Thanks for reading! Until next time.