Foundations

3D text in WPF

Charles Petzold

Code download available at:Foundations2007_10.exe(171 KB)

Contents

FormattedText and BuildGeometry
Outlines and Meshes
The Text3D Hierarchy
RibbonText and SliverText
The SolidText Breakthrough
Plumping It Up

Outline font technologies such as TrueType primarily provide us with typographical flexibility and accuracy, but they can also serve as graphical playthings. Programmers can get access to the actual outlines that define each text character and treat them as vector graphics objects. The outlines can be stroked, filled, used for clipping, or subjected to transforms. A popular feature in Microsoft® Word known as WordArt is based on this concept.

It's important to recognize the nature and limitations of these character outlines: they are strictly geometrical and are missing the "hints" that the operating system normally uses to render fonts on the screen. These hints allow the characters to be rasterized intelligently based on the available pixel grid. Consequently, the unhinted character outlines look best in big font sizes or on high-resolution devices. They are usually not adequate for rendering text at normal font sizes on the screen. (However, as printer resolution gets higher and as antialiasing is used more for screen graphics, the value of hinting has decreased.)

Whenever I encounter a new Windows® API, I make a special effort to locate the provision for getting access to these character outlines. In Windows Forms, it's part of the GraphicsPath class. Four overloads of an AddString method lets you add character outlines to a path. Chapter 19 of my books Programming Microsoft Windows with C# (Microsoft Press, 2002) and Programming Microsoft Windows with Microsoft Visual Basic® .NET (Microsoft Press, 2003) show how this is done.

In Windows Presentation Foundation (WPF), the classes and methods that provide access to character outlines are better hidden, but they do exist. The FormattedText and GlyphRun classes from the System.Windows.Media namespace both have methods named BuildGeometry that return a Geometry object for a particular font and text string. In this article I'll use FormattedText exclusively because it's the easier of the two classes. Chapters 28 and 30 of my book Applications = Code + Markup (Microsoft Press, 2006) have some examples of using FormattedText and BuildGeometry with two-dimensional graphics.

When I began working with 3D graphics in WPF, I naturally pondered the possibility of turning these character outlines into blocks of three-dimensional text such as seen in printed media and flying logo effects on television (see the example given in Figure 1). I knew that the job would involve converting a two-dimensional outline into a three-dimensional triangle mesh, but beyond that, I was sure of only one thing: some non-trivial programming was going to be involved.

Figure 1 Solid 3D Text

Figure 1** Solid 3D Text **

FormattedText and BuildGeometry

I suspect that most WPF programmers haven't had much contact with the FormattedText class. As the documentation indicates, you use this class for "low-level control for drawing text."

The FormattedText constructors require a TypeFace object, which defines the font family, a style (such as italic), possible boldfacing, and any stretching or compression associated with the font. In addition, the FormattedText constructors require an em size (the font height), a brush for coloring the font characters, and the text string itself.

Once a FormattedText object is created, you can call various methods to set different fonts, styling, or formatting to subsets of the text string. Properties let you set line spacing and other characteristics of the text.

FormattedText objects are used most commonly with the DrawText method of the DrawingContext class. Generally you encounter the DrawingContext class when you override the OnRender method defined by UIElement. This is the lowest level of graphics output your application can do and still be considered a pure WPF application. The DrawText method requires only a FormattedText object and a coordinate point where the text is to begin.

In the context of everything else in FormattedText, the BuildGeometry method seems like an oddity. The method has one argument—an origin of type Point—and returns a Geometry object.

Geometry is an important class in WPF, and it's clearly related to the traditional graphics path. A Geometry object is a collection of straight lines and curves specified as coordinate points. Some of these lines and curves might be connected; some connected lines and curves might be closed to describe enclosed areas. No concept of rendering is included in a Geometry object. In two-dimensional graphics programming, you can render a Geometry object by passing it to the handy Path class, which is part of the high-level Shapes library. An alternative is GeometryDrawing, which is based on a Geometry object, a Brush, and a Pen.

Generally when programming two-dimensional graphics, there's little reason to go digging inside a particular Geometry object. But converting a Geometry object into three-dimensional text is quite a different job. So what exactly is the Geometry object that FormattedText.BuildGeometry returns?

Geometry is an abstract class from which seven other classes derive. My experience is this: the Geometry object returned from FormattedText.BuildGeometry is actually one or more nested GeometryGroup objects containing multiple PathGeometry objects, one for each character in the text string. Each PathGeometry object consists of one or more PathFigure objects defining a closed path. Some characters (such as l, t, or x) require only one PathFigure. Other characters require two. A lowercase i, for example, requires a second PathFigure for the dot. An O requires one PathFigure for the outline of the outside circle and another for the inner circle. An uppercase B requires three PathFigure objects. A percent sign requires five.

Each PathFigure is a collection of objects of type PathSegment, which is another abstract class. My experience reveals that the PathFigure objects that are associated with text outlines contain multiple objects of type LineSegment, PolyLineSegment, BezierSegment, and PolyBezierSegment that contribute to the definition of a single closed path.

WPF 3D has no concept of polylines or Bezier curves, but triangle meshes are based on collections of points, so a first step requires converting a Geometry object from FormattedText.BuildGeometry into a series of enclosed polylines. That would be the easy part. But it soon became obvious to me that coming up with an algorithm to convert those polylines into a triangle mesh was clearly a complex mathematical exercise, and very likely beyond my abilities.

Outlines and Meshes

In the April 2007 issue of this magazine, I discussed the mechanics of generating MeshGeometry3D objects for use with the WPF 3D graphics (see msdn.microsoft.com/msdnmag/issues/07/04/Foundations). At the very least, you need to set the Positions and TriangleIndices properties of a MeshGeometry3D object. The Positions property is a collection of points in 3D space. The TriangleIndices collection describes how to construct triangles from these points. Every three integers in TriangleIndices reference three points in the Positions collection to form a triangle.

I figured that the polylines I generated from the text outlines would form members of the Positions collection of MeshGeometry3D. Figure 2 shows a capital A from a sans-serif font consisting of two enclosed polylines with a total of eleven points. The second image shows how the letter might be divided into seven triangles. For an easy character like this, it's trivial to do it by hand, but describing the process in code wasn't clear to me at all.

Figure 2 Letter Defined by Points and Triangles

Figure 2** Letter Defined by Points and Triangles **

Obviously you want to make sure that the sides of each triangle don't stray beyond the confines of the character, and you need to make sure the entire face of the character is accounted for by the triangle collection. And keep in mind that the example I used here is a simple character from an easy sans-serif font. Consider a capital S from a serifed font, and the job becomes truly baffling. A technique known as Delaunay Triangulation exists that might be of help, but the mathematics are rather complex.

Fortunately, all was not lost. While contemplating this problem, I saw some text in an advertisement that took a different approach to 3D text involving only the outlines and leaving the body of each character hollow. This, I realized, was something I knew I could do. First I needed a class name and I came up with RibbonText.

The Text3D Hierarchy

The downloadable code for this article contains a single Visual Studio® solution named Text3D. It consists of a DLL project named Petzold.Text3D and several small demo programs in XAML that use the DLL. Note that the files in the DLL have the namespace Petzold.Text3D.

The class hierarchy in the Petzold.Text3D.dll library begins with ModelVisualBase, an abstract class that derives from the WPF 3D class ModelVisual3D, a technique I discussed in the April 2007 issue of this magazine. This ModelVisualBase class, however, is not exactly the same ModelVisualBase from the previous column because it needed a little more flexibility. But it's important to note that the concept is the same: ModelVisualBase internally stores a GeometryModel3D object and a MeshGeometry3D object, and defines public Material and BackMaterial properties that it transfers to the GeometryModel3D object.

Most of the properties in the Petzold.Text3D classes are backed by dependency properties. The ModelVisualBase class defines two PropertyChanged event handlers (one static, one instance) that descendent classes can use when defining dependency properties. The instance version properly prepares the various properties of the internal MeshGeometry3D object for changes (a process I discussed in my April column), and then calls an abstract method named Triangulate. Descendent classes can override Triangulate to define the various collections of the MeshGeometry3D.

In the Petzold.Text3D library, the abstract Text3DBase class derives from ModelVisualBase. This class defines a bunch of text-related properties named Text, FontFamily, FontStyle, FontWeight, FontStretch, and FontSize, all required by the FormattedText constructor. The class also defines an Origin property that the BuildGeometry method requires. Whenever any of these properties change, the class creates a new FormattedText object and sets an eighth property of Text3DBase named TextGeometry from the Geometry object returned from BuildGeometry:

FormattedText formtxt = new FormattedText(Text, CultureInfo.CurrentCulture, 
  FlowDirection.LeftToRight, new Typeface(FontFamily, FontStyle, FontWeight, FontStretch), 
  FontSize, Brushes.Transparent); TextGeometry = formtxt.BuildGeometry(Origin);

This TextGeometry object is available to all descendent classes.

The Text3DBase class cannot create a new TextGeometry object without heap allocations taking place. The class needs to allocate a new FormattedText object, and BuildGeometry undoubtedly makes a lot of its own memory allocations. These heap allocations imply that properties defined by the Text3DBase class should probably not be animated. I've used the IsAnimationProhibited property defined by UIPropertyMetadata to flag those properties that might otherwise be prime candidates for animation.

Throughout the classes I've written that create 3D text, the Origin property defined by Text3DBase is the only property that indicates where the text is positioned in 3D space, and the property is of type Point, which indicates a location in two dimensions. I considered defining an extensive set of properties to precisely position text in 3D space. These properties must include not only a 3D text origin, but also a 3D vector indicating the direction of the baseline, and another 3D vector indicating the upright direction. I instead decided to use the Origin property to position the text on the XY plane in 3D space, and then perform all other positioning with transforms. The big advantage of this approach is that it simplifies the math.

The FontSize property defined by the Text3DBase indicates the em size of the font, which is roughly related to the total height of the font characters. In three dimensions, fonts also have a depth. The DeepTextBase class derives from Text3DBase and exists solely to define a Depth property. (You'll eventually see the reason for this extended class hierarchy; a considerable amount of refactoring took place during the programming of these classes and led to the current structure.) Because the text is positioned on the XY plane, I conceived the Depth property as describing how deep the text extends along the negative Z axis.

So far none of these classes have done any real work. The abstract GeometryTextBase class shown in Figure 3 derives from DeepTextBase and overrides the Triangulate method defined by ModelVisualBase. With the assistance of the GetFlattenedPathGeometry method in the Geometry class, the GeometryTextBase class converts the Geometry object available as the TextGeometry property into multiple connected polylines corresponding to the closed figures that make up the text outlines. For each closed figure, the GeometryTextBase class makes a call to an abstract method, like so:

Figure 3 GeometryTExtBase Class

public abstract class GeometryTextBase: DeepTextBase {
    // Field prevent re-allocations during mesh generation. 
    CircularList < Point > list = new CircularList < Point > ();
    protected override void Triangulate(DependencyPropertyChangedEventArgs args, 
      Point3DCollection vertices, Vector3DCollection normals, 
      Int32Collection indices, PointCollection textures) {
        // Clear all four collections. 
        vertices.Clear();
        normals.Clear();
        indices.Clear();
        textures.Clear();
        // Convert TextGeometry to series of closed polylines. 
        PathGeometry path = TextGeometry.GetFlattenedPathGeometry(0.001, 
          ToleranceType.Relative);
        foreach(PathFigure fig in path.Figures) {
            list.Clear();
            list.Add(fig.StartPoint);
            foreach(PathSegment seg in fig.Segments) {
                if (seg is LineSegment) {
                    LineSegment lineseg = seg as LineSegment;
                    list.Add(lineseg.Point);
                } else if (seg is PolyLineSegment) {
                    PolyLineSegment polyline = seg as PolyLineSegment;
                    for (int i = 0; i < polyline.Points.Count; i++) list.Add(polyline.Points[i]);
                }
            }
            // Figure is complete. Post-processing follows. 
            if (list.Count > 0) {
                // Remove last point if it's the same as the first. 
                if (list[0] == list[list.Count - 1]) list.RemoveAt(list.Count - 1);
                // Convert points to Y increasing up. 
                for (int i = 0; i < list.Count; i++) {
                    Point pt = list[i];
                    pt.Y = 2 * Origin.Y - pt.Y;
                    list[i] = pt;
                }
                // For each figure, process the points. 
                ProcessFigure(list, vertices, normals, indices, textures);
            }
        }
    }
    // Abstract method to convert figure to mesh geometry. 
    protected abstract void ProcessFigure(CircularList < Point > list, Point3DCollection vertices, 
                 Vector3DCollection normals, Int32Collection indices, PointCollection textures);
}

protected abstract void ProcessFigure( CircularList<Point> list, Point3DCollection vertices, 
  Vector3DCollection normals, Int32Collection indices, PointCollection textures);

The first argument is a collection of the two-dimensional points in the figure. CircularList is a collection class I defined that functions like a circular buffer. Whenever a member is accessed by an index, the object normalizes the index to the proper range. In other words, an index of -1 accesses the last member of the collection and an index of list.Count accesses the first member of the collection. The collection contains all the points in the closed polyline.

The other arguments to ProcessFigure correspond to the Positions, Normals, TriangleIndices, and TextureCoordinates collections of the MeshGeometry3D object. A class that implements the ProcessFigure method is required to fill at least the vertices and indices collections based on the points in the CircularList collection.

RibbonText and SliverText

The first class I wrote that actually generates three-dimensional text is called RibbonText and is shown in Figure 4. As you can see, the class derives from GeometryTextBase and consists solely of an implementation of the ProcessFigure method. The Point3D objects it generates are based solely on the 2D points in the CircularList collection and the Depth property. The points alternate between those on the XY plane (where Z equals zero) and those Depth units behind the XY plane. The indices collection connects these two sets of points to define a series of triangles.

Figure 4 RibbonText Class

public class RibbonText: GeometryTextBase {
    protected override void ProcessFigure(CircularList < Point > list, 
      Point3DCollection vertices, Vector3DCollection normals, 
      Int32Collection indices, PointCollection textures) {
        int offset = vertices.Count;
        for (int i = 0; i <= list.Count; i++) {
            Point pt = list[i];
            // Set vertices. 
            vertices.Add(new Point3D(pt.X, pt.Y, 0));
            vertices.Add(new Point3D(pt.X, pt.Y, -Depth));
            // Set texture coordinates. 
            textures.Add(new Point((double) i / list.Count, 0));
            textures.Add(new Point((double) i / list.Count, 1));
            // Set triangle indices. 
            if (i < list.Count) {
                indices.Add(offset + i * 2 + 0);
                indices.Add(offset + i * 2 + 2);
                indices.Add(offset + i * 2 + 1);
                indices.Add(offset + i * 2 + 1);
                indices.Add(offset + i * 2 + 2);
                indices.Add(offset + i * 2 + 3);
            }
        }
    }
}

Keep in mind that the ProcessFigure method is called multiple times for a particular text string. The Triangulate method in GeometryTextBase is responsible for initially clearing the MeshGeometry3D collections; the ProcessFigure class uses an integer named offset for determining the indices of new points it adds to the vertices collection.

Figure 5 shows a small XAML file that makes use of the RibbonText class, and Figure 6 shows what it looks like. In a sense, it looks somewhat "fancier" than normal 3D block text, perhaps because it's rather unusual. But algorithmically speaking, this is about the simplest form of 3D text conceivable.

Figure 5 RibbonTextDemo.xaml

<!-- RibbonTextDemo.xaml by Charles Petzold, June 2007 -->
<Page
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:src="clr-namespace:Petzold.Text3D;assembly=Petzold.Text3D" 
      WindowTitle="RibbonText Demo" Title="RibbonText Demo">
    <Viewport3D>
        <src:RibbonText Text="Ribbon" FontFamily="Times New Roman" Depth="2">
            <src:RibbonText.Material>
                <DiffuseMaterial Brush="Cyan" />
            </src:RibbonText.Material>
            <src:RibbonText.BackMaterial>
                <DiffuseMaterial Brush="Pink" />
            </src:RibbonText.BackMaterial>
        </src:RibbonText>
        <!-- Lights. -->
        <ModelVisual3D>
            <ModelVisual3D.Content>
                <Model3DGroup>
                    <AmbientLight Color="#404040" />
                    <DirectionalLight Color="#C0C0C0" Direction="2 -3 -1" />
                </Model3DGroup>
            </ModelVisual3D.Content>
        </ModelVisual3D>
        <!-- Camera. -->
        <Viewport3D.Camera>
            <PerspectiveCamera Position="-3 0 8" UpDirection="0 1 0" 
              LookDirection="1 0 -2" FieldOfView="45" />
        </Viewport3D.Camera>
    </Viewport3D>
</Page>

Figure 6 RibbonTextDemo Display

Figure 6** RibbonTextDemo Display **

The characters are actually hollow. If you viewed the text head on, you'd see right through it. Only because it's viewed at an angle can you see that the outside and inside are colored differently. The Material brush (set to Cyan in RibbonTextDemo.xaml) colors the outside of the characters and the BackMaterial brush (set to Pink) colors the inside. For some fonts, however, these colors could be reversed. It depends on the direction of the points defining the character outlines. Conceivably, this direction could even be different for different characters within the same font.

The RibbonText class defines points for the TextureCoordinates collection, so you're not restricted to solid color brushes. The points in the TextureCoordinates collection have Y coordinates of 0 for the foreground part of the ribbon and 1 for the background part, and X coordinates based on the index of the point in the CircularList collection. Any non-solid brush you use with this text will apply to each ribbon separately. The most predictable results occur when you use a LinearGradientBrush with starting and ending coordinates of (0, 0) and (0, 1). Other brushes might reveal a visible seam at unpredictable places in the text characters.

Rather more difficult than RibbonText was a class I called SliverText. This class also focuses solely on the outlines of the text characters, but it gives those outlines a non-zero width that I defined as the SliverWidth property. Figure 7 shows the SliverTextDemo program running with a default FontSize of 1, a default Depth of 1, and a default SliverWidth of 0.05. I defined the TextureCoordinates of SliverText based on the X and Y coordinates of the points, allowing a brush to be varied over the front of the text. The example shows a gradient brush. The tops of the characters appear a little rounded because the coordinates at the edges are shared between the top triangles and the side triangles. WPF 3D calculates normals (which govern the reflection of light) based on the average.

Figure 7 SliverTextDemo Display

Figure 7** SliverTextDemo Display **

If you set the SliverWidth to too high a value relative to FontSize, characters will begin to merge into each other. If the text is then animated by a transform, there may be some ugly chatter as surfaces fight for foreground dominance. (The technical term for this effect is "Z fighting.")

The SliverText class involved a little excursion into some two-dimensional analytic geometry. Each character outline is a two-dimensional polyline, but SliverText requires two sets of parallel polylines. I was tempted to use the GetWidenedPathGeometry method defined by Geometry, but my experience with widened paths is that they often produce artifacts I was eager to avoid.

Instead, I widened the path myself. This job requires calculating lines that are parallel to each of the individual lines of the polylines. However, these parallel lines often need to be shortened or lengthened to achieve the same continuity and shape as the underlying path. Helping me out was a little structure named Line2D that I wrote. This structure defines the addition of a Line2D object and a vector as a shift of the line. The multiplication of two Line2D objects returns a Point object indicating the intersection.

The SolidText Breakthrough

Many months passed after I originally wrote RibbonText and SliverText, and I truly feared that was as far as I could go with 3D text. Occasionally I returned to the problem of triangulating the face of the text characters, but I never saw a way to do it without descending into some truly scary mathematics.

Of course, I knew perfectly well how to put a text string on a flat surface in 3D. All you need is a VisualBrush based on a TextBlock element. It certainly occurred to me that I could overlay that flat surface on top of the RibbonText object and create the illusion of solid text. But I knew from experience the problems involved in mixing rasterized text (which is what TextBlock displays) and text constructed from geometrical outlines. The two different algorithms just don't match visually.

The breakthrough came when I realized I could instead cover a flat surface in 3D space with a DrawingBrush based on the same geometry used in RibbonText. The concept was clear; the implementation was not.

The class that generates this DrawingBrush I named PlanarText, which derives from Text3DBase. PlanarText needs the TextGeometry property that Text3DBase generates, but it doesn't need the Depth property defined by DeepTextBase because (as the class name indicates) PlanarText displays text on a plane. However, the PlanarText class does define a property named Z that indicates where the plane of text is located parallel to the XY plane.

PlanarText implements the Triangulate method by simply defining a flat rectangle based on two triangles. The coordinates of the four corners of this rectangle come from the Bounds property associated with the Geometry object obtained from the TextGeometry property.

Where PlanarText is obviously different from the other classes that derive from ModelVisualBase is in its treatment of the Material and BackMaterial properties.

What I find really funny is that when ModelVisualBase was originally conceived, these Material and BackMaterial properties were considered a nuisance more than anything else. ModelVisualBase derives from ModelVisual3D, but ModelVisual3D does not define Material and BackMaterial properties. The WPF 3D class that defines the Material and BackMaterial properties is actually GeometryModel3D, an instance of which ModelVisualBase stores internally as its Content property.

ModelVisualBase needs to define Material and BackMaterial properties so you can set them in derived classes in markup, like so:

<src:RibbonText Text="Ribbon">
    <src:RibbonText.Material>
        <DiffuseMaterial Brush="Cyan" />
    </src:RibbonText.Material>
    <src:RibbonText.BackMaterial>
        <DiffuseMaterial Brush="Pink" />
    </src:RibbonText.BackMaterial>
</src:RibbonText>

Associated with the Material and BackMaterial properties that ModelVisualBase defines is a property-changed handler called MaterialPropertyChanged that simply transfers any objects assigned to these properties to the identical properties of the internal GeometryModel3D object.

But PlanarText needs to do something different. When the Material property of PlanarText is set to a DiffuseMaterial object (for example), PlanarText needs to extract the Brush object associated with that DiffuseMaterial and then create a new DrawingBrush object based on this existing brush and the TextGeometry property:

new DrawingBrush(new GeometryDrawing(brush, null, TextGeometry))

This DrawingBrush becomes the basis of a new DiffuseMaterial object that PlanarText then sets to the Material property of the internal GeometryModel3D object.

This process sounds reasonably easy if the object being assigned to the Material property of PlanarText is a DiffuseMaterial. But the Material property could be assigned a MaterialGroup object, and this MaterialGroup object could have children of type DiffuseMaterial, SpecularMaterial, EmissiveMaterial, or even another nested MaterialGroup. In the general case, the PlanarText class needs to construct a parallel structure of Material objects to assign to its internal GeometryModel3D object, and each DrawingBrush associated with these Material objects needs to be calculated from the Brush object from the Material objects associated with PlanarText.

Simply because of this Material transfer logic, PlanarText.cs turned out to be the longest file in the Text3D library, even though the result doesn't look like much, as Figure 8 demonstrates.

Figure 8 PlanarTextDemo Display

Figure 8** PlanarTextDemo Display **

I'm still not entirely happy with the PlanarText class. If the size of the text changes, for example, PlanarText needs to recreate the brushes. The class avoids recreating a bunch of Material objects if the structure of its public Material and BackMaterial properties is the same as the structure of the same properties of the internal GeometryModel3D object. But if one of the brushes associated with these Material objects is animated—perhaps through a ColorAnimation—then PlanarText is forced to recreate GeometryDrawing and DrawingBrush objects on each pass, and that could be costly.

PlanarText is not very important on its own, but it's instrumental in making the SolidText class possible. SolidText, shown in Figure 9, derives from DeepTextBase, which means that it has all the text-related properties defined by Text3DBase as well as the Depth property. But SolidText isn't interested in generating any triangle meshes on its own. It overrides the Triangulate method, but just returns from it.

Figure 9 SolidText Class

public class SolidText: DeepTextBase {
    public SolidText() {
        // Create RibbonText and two PlanarText children. 
        RibbonText ribbon = new RibbonText();
        ribbon.Depth = Depth;
        Children.Add(ribbon);
        PlanarText planar = new PlanarText();
        Children.Add(planar);
        planar = new PlanarText();
        planar.Z = -Depth;
        Children.Add(planar);
    }
    // SideMaterial dependency property and property. 
    public static readonly DependencyProperty SideMaterialProperty = 
      DependencyProperty.Register("SideMaterial", typeof(Material), 
      typeof(SolidText), new PropertyMetadata(SideMaterialChanged));
    public Material SideMaterial {
        set {
            SetValue(SideMaterialProperty, value);
        }
        get {
            return (Material) GetValue(SideMaterialProperty);
        }
    }
    // SideMaterialChanged handlers. 
    static void SideMaterialChanged(DependencyObject obj, 
      DependencyPropertyChangedEventArgs args) {
        ((SolidText) obj).SideMaterialChanged(args);
    }
    void SideMaterialChanged(DependencyPropertyChangedEventArgs args) {
        // Transfer SideMaterial to RibbonText. 
        Text3DBase txtbase = Children[0] as Text3DBase;
        txtbase.Material = args.NewValue as Material;
        txtbase.BackMaterial = args.NewValue as Material;
    }
    // MaterialChanged override. 
    protected override void MaterialPropertyChanged(DependencyPropertyChangedEventArgs args) {
        // Transfer Material and BackMaterial properties to PlanarText. 
        if (args.Property == MaterialProperty)((PlanarText) Children[1]).Material = 
          args.NewValue as Material;
        else if (args.Property == BackMaterialProperty)((PlanarText) Children[2]).BackMaterial = 
          args.NewValue as Material;
    }
    // TextPropertyChanged override. 
    protected override void TextPropertyChanged(DependencyPropertyChangedEventArgs args) {
        base.TextPropertyChanged(args);
        // Transfer text-related property to all three children. 
        for (int i = 0; i < 3; i++) {
            Text3DBase txtbase = Children[i] as Text3DBase;
            txtbase.SetValue(args.Property, args.NewValue);
        }
    }
    // PropertyChanged override.
    protected override void PropertyChanged(DependencyPropertyChangedEventArgs args) {
        if (args.Property == DepthProperty) {
            // Set Depth property to RibbonText and PlanarText children. 
            double depth = (double) args.NewValue;
            ((DeepTextBase) Children[0]).Depth = depth;
            ((PlanarText) Children[2]).Z = -depth;
        }
        base.PropertyChanged(args);
    }
    // Move on, move on. Nothing to see here.
    protected override void Triangulate(DependencyPropertyChangedEventArgs args, 
      Point3DCollection vertices, Vector3DCollection normals, 
      Int32Collection indices, PointCollection textures) {}
}

Instead, SolidText takes advantage of a property it inherits from ModelVisual3D, the Children property. SolidText can have children of type ModelVisual3D and hence of type RibbonText and PlanarText. Any transform applied to SolidText will be applied to all its children. In addition, SolidText defines a SideMaterial property to be applied to the RibbonText child. The primary job of SolidText is distributing the properties set on itself to all its children.

The SolidTextDemo program rotates a SolidText object, which you saw back in Figure 1. To test out the Material-transfer logic in PlanarText, I gave the Figure a gradient brush and specular material that catches the light during its rotation.

Plumping It Up

Once I solved the basic problem of combining the RibbonText and PlanarText figures into one unified block of text, I decided to try to round out the surfaces a bit. The EllipticalText class is similar to SliverText except that each point on the outline of the text character becomes an ellipse rather than a rectangle. The EllipticalText class actually turned out to be a bit shorter than SliverText.

RoundedText derives from SolidText and uses its constructor to replace the RibbonText child that SolidText created with an EllipticalText object. This combination gives the text a somewhat rounded appearance at the edges of the PlanarText object, and also bulks up the middle of the block a bit, as you can clearly see in Figure 10.

Figure 10 Solid 3D Text

Figure 10** Solid 3D Text **

The degree of rounding is controlled by a property of both EllipticalText and RoundedText named EllipseWidth, which has a default value of 0.05. Setting this property too large as a fraction of the TextSize property will cause characters to merge with one another and result in Z fighting occuring during animation.

Someday I hope to crack that algorithm that lets me triangulate outline fonts with more flexibility. Meanwhile, these classes will have to suffice. I know that the world is suffering from a dearth of flying logos that incorporate 3D text effects, so I can only hope my modest contribution here will help relieve that dreadful shortage.

Send your questions and comments for Charles to cp@charlespetzold.com.

Charles Petzold is a Contributing Editor to MSDN Magazine. His most recent book is 3D Programming for Windows (Microsoft Press, 2007).