3D Text
TextMesh builds solid lettering from a real font: each glyph's outline is read from the font file,
triangulated, holes and all, and extruded along Z into a mesh with a front cap, a back cap and
flat-shaded walls. The result is geometry: it is lit and shadowed, takes any material, casts a
shadow, and can carry a physics body. For labels and HUDs, Entity Text and
World Text are far cheaper; this is for lettering that is a thing in the world: a
title, a sign, a logo, a word that falls.
MeshFont, TextMesh, TextMeshOptions and GlyphOutline live in the
Stride.CommunityToolkit.Rendering.Text namespace; Primitive3D.Text in
Stride.CommunityToolkit.Rendering.ProceduralModels. The stages between the font file and
the mesh, and the files behind each, are on How 3D text is built.
var entity = new Entity("Title")
{
new ModelComponent { Model = TextMesh.CreateModel(game.GraphicsDevice, game.CreateMaterial(Color.Gold), MeshFont.Default, "Hello") },
};
Fonts
A MeshFont is a font file opened for its outlines, through the FreeType library the engine ships on
every platform, so TrueType, OpenType (CFF) and font collections all work.
MeshFont.Defaultis an Open Sans Regular embedded in the toolkit (Apache 2.0): lettering works with no font file at hand, and gives the same shapes on every machine.MeshFont.Load(path)andMeshFont.Load(bytes)open any file; a collection (.ttc) holds several faces,FaceCountsays how many andLoad(path, faceIndex)picks one. Dispose the font when its outlines are no longer needed; glyphs are read once and cached, a font is safe to share between threads, and disposing it while another thread reads makes that reader throw rather than crash.MeshFont.FromFamily("Segoe UI", FontStyle.Bold)finds an installed family throughSystemFonts, and isnullwhen the machine has no such font, so a fallback reads?? MeshFont.Default. It opens the file's first face; a style kept as a later face of a collection needsLoadwith the index.
Outlines are in EM units: a capital letter is about 0.7 high, the baseline is y = 0, and
MeshFont.LineHeight, Ascent and Descent are the font's own. font.GetGlyph('A') returns the
GlyphOutline: contours as curves, the advance, the bounds; Flatten() turns them into polygons
with their holes sorted out.
Kerning comes from the font's legacy kern table only. Modern fonts keep it in GPOS, which FreeType
does not read without a shaping engine, so GetKerning is 0 for most fonts, including the default.
TextMeshOptions.LetterSpacing tunes the fit by hand.
Options
| Option | Default | What it does |
|---|---|---|
Size |
1 | World units per EM. A capital comes out about 0.7 of this tall. |
Depth |
0.25 | How far the lettering extends along Z, centred on zero. |
Bevel |
null |
A TextBevel(size, thickness, segments) rounds the edge between the caps and the walls, front and back; see below. |
CurveQuality |
32 | Points a full turn of curve gets: a quarter-turn bowl gets a quarter of them. 16 is still smooth at a distance; 64 for a logo the camera gets close to. |
Anchor |
null |
Which point of the text block sits at the origin. null is the typographic origin: the first baseline at y = 0, the pen at x = 0. The nine TextAnchor points use the font's ascent-to-descent block over every line, so the anchor does not move with the letters used. |
LetterSpacing |
0 | Extra space after every glyph, in EM units; negative tightens. |
LineSpacing |
1 | The distance between lines as a multiple of the font's line height. |
Alignment |
Left |
How lines of different widths line up: Left, Center, Right. |
Kerning |
true |
Apply the font's pair kerning where the font has a kern table. |
\n starts a new line. TextMesh.Measure(font, text, options) gives the block as a BoundingBox
without building the mesh.
var options = new TextMeshOptions
{
Size = 0.4f,
Depth = 0.1f,
Alignment = TextAlignment.Center,
LineSpacing = 0.9f,
Anchor = TextAnchor.TopCenter,
};
var draw = TextMesh.CreateMeshDraw(game.GraphicsDevice, MeshFont.Default, "two lines\nof text", options);
Bevel
TextBevel rounds the edge between the caps and the walls on both sides: a quarter-ellipse fillet
that reaches Size in from the outline across the cap and Thickness from the cap towards the
middle, in Segments steps (1 is a chamfer, 3 reads as round, 6 is smooth up close; the default is
3). The walls stay flat; across the steps the normals turn from the wall to the cap.
var options = new TextMeshOptions
{
Size = 0.6f,
Depth = 0.2f,
Bevel = new TextBevel(size: 0.015f, thickness: 0.03f, segments: 4),
};
A bevel wider than a glyph's thinnest stem would fold the cap over itself. TextMesh checks every
step of the pulled-in outline (same way round, smaller, every edge still running its way, not
crossing itself or another) and halves the size for the whole glyph until every piece of it fits, so
a wide bevel on a thin font rounds less rather than breaking; the dot and the stem of an i get the
same rounding. After six halvings the build fails with the font and the code point named. The
thickness is capped at half the depth; at that value the two bevels meet and there is no wall.
The mesh
TextMesh.Create returns GeometricMeshData<VertexPositionNormalTexture>, CPU data. ToMesh(device)
uploads it as a Mesh with its bounds, which the renderer needs to cull lettering that is off screen,
and TextMesh.CreateMesh and CreateModel do both in one call; ToMeshDraw and CreateMeshDraw
give the bare draw. Text with nothing to draw (empty, spaces, line breaks) gives empty CPU data, and
the upload methods throw ArgumentException, since a GPU buffer cannot be empty: when a value
clears, remove the model. Vertices carry a position, a normal and texture coordinates: the caps are
mapped across the text's block (0..1 left to right, 0..1 top to bottom) and each wall runs 0..1 along
its contour and 0..1 front to back, so a texture or a shader can follow the lettering.
The caller owns the buffers of a draw: text that changes must dispose the old buffers before the new
mesh goes in, or it leaks a buffer pair per rebuild. E03_3D_MeshText shows the pattern in its
FrameCounterScript.
A body per word, or per letter
Primitive3D.Text makes lettering a shape like any other, so game.CreatePrimitive gives it a material
and, with a physics engine chosen, a collider: one convex hull per glyph. A hull fills a glyph's holes
and bays, so an O collides as a disc, which is right for letters that tumble and pile up. A Mass on
the options is the word's, shared out among the hulls. The hulls of a string are kept, keyed by the
font, the text and the options, so a word dropped many times shares one set.
var word = game.CreatePrimitive(Primitive3D.Text("Drop", size: 0.7f, depth: 0.3f), new Bepu3DPhysicsOptions { Material = material });
word.Transform.Position = new Vector3(0, 4, 0);
word.Scene = scene;
TextMesh.CreateGlyphs returns one mesh per glyph, each centred on itself and told where it sits in
the text, for a body or an animation per letter; E20_3D_CubeCollapse drops its GAME OVER that way.
Lettering that must be walked into or shot through its holes wants a MeshCollider on a static, see
Physics extensions.
Limits
- A mesh is for words and titles, not pages: a paragraph is thousands of triangles per line. Sprite fonts are for text.
- Two filled contours that cross or overlap (composite glyphs such as Ç, where the cedilla runs into the C) are built as two solids through each other, which the font's nonzero fill would merge; they look right from outside, and each piece is closed on its own. A contour that crosses itself is reported by font and code point, not mangled. A contour of a single point, which some fonts carry, is skipped.
Size,Depth,CurveQualityand a bevel's size and thickness must be positive finite numbers, andLetterSpacingfinite; anything else is refused withArgumentOutOfRangeException.- Letters landing on letters are hull-against-hull pairs, and the Bepu build Stride 4.4 ships mishandles thin extruded hulls that overlap (reported upstream as bepuphysics2 issue 417; it can crash the narrow phase). A hull collider suits a word on its own; give a pile of letters a box per glyph instead, from the glyph meshes' bounds, as Cube Collapse does.
- No right-to-left, no wrapping, no text on a path.
Examples
E03_3D_MeshText: the embedded font, an installed font, lines that align, a frame counter, and words with physics.E03_3D_TextShaders: four materials on the same lettering: a gradient texture read through the texture coordinates, a rim light node, a vertex-stage wave and a pixel-stage dissolve, the last three shader classes in the example'sEffectsfolder.E07_3D_ProceduralGeometry: lettering among other meshes built in code.