Foundations

3D Mesh Geometries

Charles Petzold

Code download available at:  Foundations 2007_04.exe(165 KB)

Contents

The 3D Viewport
Anatomy of a Mesh Geometry
Algorithmic Mesh Geometries
Accommodating the Consumer
Integrating with XAML
Dependency Properties
Proper Handling of Collections
Seams and Ends
Showcasing the Cylinder
Throw Out the First One

Among the classes that contribute to the Microsoft Windows Presentation Foundation, those in the System.Windows.Media.Media3D namespace stand out. These are the classes that are intended to bring three-dimensional graphics to mainstream Windows® applications. As with the Windows Presentation Foundation 2D graphics, 3D graphics are often most conveniently accessed in Extensible Application Markup Language (XAML), but there the similarities pretty much end. 3D graphics programming involves significantly different concepts and conventions. Where 3D and 2D overlap is in the area of brushes: you always cover the surface of a 3D visual with a 2D brush.

Figure 1 shows Hello3D, a 3D version of the traditional "Hello, World" program. If you're running Windows Vista™ or have the Microsoft® .NET Framework 3.0 runtime installed under Windows XP, you can simply use Internet Explorer® to launch the XAML code that produces it to see the image yourself (see Figure 2).

Figure 1 The Hello3D Image

Figure 1** The Hello3D Image **

The 3D Viewport

In 3D graphics programming, there are no lines, no Bezier spline curves, no rectangles or ellipses. Every 3D object is a collection of triangles in three-dimensional coordinate space. Triangles are intrinsic to 3D programming because each individual triangle always defines a flat surface, yet collections of triangles can mimic a solid object and even approximate a curved surface. As you get deeper into 3D programming, you'll start seeing everything in your life in terms of triangles.

As Hello3D.xaml shows, 3D views are composed within a Viewport3D element. A 3D scene requires one or more objects of type GeometryModel3D, one or more light sources, and a camera that governs how the 3D object is projected onto a 2D surface, and thus how the viewer sees the scene.

The GeometryModel3D element has three crucial properties: Geometry, Material, and BackMaterial. The Geometry property is set to a MeshGeometry3D element that describes the visual object in terms of coordinate points and triangles. The Material and BackMaterial properties indicate how the fronts and backs of the object are to be colored. In Hello3D.xaml, both properties are set to objects of type DiffuseMaterial. The Material property is a VisualBrush that consists of a TextBlock with the words "Hello, World." The BackMaterial property is simply a red brush. (If you want to see the back of the object, change the camera Position property to "0 0 -5" and LookDirection to "0 0 1.")

Anatomy of a Mesh Geometry

In this column, I want to focus on a particularly important part of this Viewport3D assemblage-the MeshGeometry3D class that defines the actual geometry of the 3D object. This class has four important properties: Positions, TriangleIndices, TextureCoordinates, and Normals. The Positions property is a collection of Point3D objects, which define locations in terms of X, Y, and Z coordinates. In this coordinate system, X coordinates increase to the right, Y coordinates increase going up the screen, and Z coordinates increase coming out of the screen. (This is known as a right-handed coordinate system; if you point the index finger of your right hand toward increasing X values, and your middle finger toward increasing Y values, your thumb points to increasing Z values. The Windows Presentation Foundation 3D system also implements the right-hand rule for rotations: if you point the thumb of your right hand to increasing values on any axis, the curve of your other fingers shows the direction of positive rotation around that axis.)

The Positions property in the Hello3D.xaml example has six Point3D objects separated by commas:

Positions="-2 1 -1, 0 2 0, 2 1 -1, -2 -1 -1, 0 -2 0, 2 -1 -1"

The first three are the coordinates of the top of the object from left to right, and the last three are the corresponding coordinates on the bottom. Notice that the Z coordinates in the center are 0, but -1 on the left and right edges, so the center is to the foreground of the left and right edges.

The Positions property indicates all the vertices of the object. These vertices are certainly instrumental in defining the object, but they don't tell the whole story. Any group of three vertices can be combined in a triangle, and that's what the TriangleIndices collection indicates. TriangleIndices is a collection of integers arranged in triplets. Each group of three integers defines a triangle. The values of the integers are indices into the Positions collection. For example, the first triplet is 0 3 1, which refers to the 3D points (-2, 1, -1), (-2, -1,-1), and (0, 2, 0). That's the triangle at the upper-left corner of the object.

TriangleIndices="0 3 1, 1 3 4, 1 5 2, 1 4 5"

The TriangleIndices collection actually drives the rendering of the object. Any Positions element not referenced by TriangleIndices is ignored (if there are no TriangleIndices, every Point3D triplet in the Positions collection is interpreted to be a triangle).

Each triangle is considered to have a front and a back. When looking at the front of a triangle, the triplet indexes the points in a counter-clockwise direction. If you change the first triplet to 0 1 3, you'll see the upper-left triangle colored red, since you're now looking at the back of the triangle instead of its front.

If a 3D object is to be colored with a solid brush, the Positions and TriangleIndices properties are sufficient. For other types of brushes (gradient brushes or tile brushes), you'll also need TextureCoordinates. Brushes are 2D surfaces that cover 3D objects like shrink wrap. The TextureCoordinates collection indicates a correspondence between the vertices of the 3D object and the coordinates of the 2D brush. This collection contains one 2D point for each 3D point in Positions. These 2D points are relative coordinates (between 0 and 1) with increasing values of Y going down. The point (0, 0) indicates the top-left corner of the brush, and (1, 1) is the bottom-right corner. In Hello3D.xaml, the six 2D points indicate the coordinates of the VisualBrush defined as the Material property of the GeometryModel3D element:

TextureCoordinates="0 0, 0.5 0, 1 0, 0 1, 0.5 1, 1 1"

In effect, each triangle of the 3D object is covered by a triangle of the brush, which might need to be stretched or shrunk to fit.

The Normals property is a collection of vectors in one-to-one correspondence with the Positions collection. Each vertex is considered to face a particular direction, which is indicated by the Normals vector for that vertex. Each point within each triangle reflects light differently based on an interpolation of the vectors at its three vertices. If you don't supply a Normals collection, one is calculated for you based on an average of the normals of the triangles that meet at each vertex shared in your mesh specification.

Algorithmic Mesh Geometries

The classes in the System.Windows.Media Media3D namespace offer no higher-level interfaces beyond MeshGeometry3D. (By the end of this article, you might understand why.) For simple objects with flat sides, such as pyramids and cubes and such, you can hand-code the MeshGeometry3D object. For more complex primitives, particularly those that have curved surfaces, you'll probably choose to generate the vertices and indices algorithmically using a .NET-compliant language. Indeed, if you're ambitious, you might conceive of a whole library of mesh geometries deriving from MeshGeometry3D.

Immediately you run into a brick wall. MeshGeometry3D is sealed; it cannot be inherited. Moreover, MeshGeometry3D itself derives from the abstract class Geometry3D, and you can't derive from Geometry3D because it has a solitary, internal constructor.

Rather than deriving a class from MeshGeometry3D, an alternative approach is to simply define a class that generates a MeshGeometry3D object and exposes that object as a property. You can then define this class as a resource in a XAML file and reference the MeshGeometry3D through a binding.

Figure 3 shows a class named SimpleCylinderGenerator written in C#. (I'll be restricting myself to cylinders here.) In the downloadable code for this column, SimpleCylinderGenerator contributes to a DLL named Petzold.MeshGeometries.

The SimpleCylinderGenerator class has two properties: you use the Slices properties to define how many triangles are used to approximate the curvature of the cylinder. (I appropriated the term "slices" from the documentation of the static Mesh.Cylinder method in the Direct3D class library.) The number of triangles running the length of the cylinder is actually double the Slices property. The top and bottom ends of the cylinder each also require a number of triangles equal to Slices.

The MeshGeometry property creates a MeshGeometry3D object based on the Slices property. The algorithm is hardcoded to create a cylinder that extends one unit along the positive Y axis with a radius of one unit. The idea here is that you can later size this object and move it wherever you want using transforms.

The SimpleCylinderDemo program shows how to use this SimpleCylinderGenerator class. The bulk of this program is the SimpleCylinderDemo.xaml file, which is shown in Figure 4. The root element contains an XML namespace declaration for the namespace and DLL in which the SimpleCylinderGenerator class is defined and associates with that a prefix of pmg (for "Petzold Mesh Geometries").

The Resources section includes an object of type SimpleCylinderGenerator with the key name of "cylinder" and a Slices value of 36. The GeometryModel3D element assigns its Geometry property to a binding of this resource and its MeshGeometry property. A transform changes the size of the cylinder. With some directional lighting and a SpecularMaterial object added to the DiffuseMaterial, the results are shown in Figure 5. You could animate this object by animating any transforms applied to the object.

Figure 5 A Cylinder

Figure 5** A Cylinder **

Obviously, the SimpleCylinderGenerator technique works, but I feel that it has a number of flaws and insufficiencies. Recognizing these problems and fixing them will provide several insights into not only mesh geometries in particular, but also some general aspects of Windows Presentation Foundation programming.

Accommodating the Consumer

When I first started defining mesh geometries for objects such as cylinders, I strived above all for symmetry, and SimpleCylinderGenerator reveals my biases: all the triangles are isosceles. But for the triangles running lengthwise along the cylinder, that's actually a problem.

It's often advantageous that a mesh-geometry algorithm produce something useful when the Slices property is small. Try setting the Slices property of SimpleCylinderGenerator to 4 and you get the object shown on the left in Figure 6. (Edges have been enhanced for illustrative purposes.) It's interesting but not quite as useful as the object on the right. The object on the right requires that the triangles running lengthwise be not isosceles triangles, but right triangles, so that each pair of right triangles forms a rectangle.

Figure 6 Slicing a Cylinder

Figure 6** Slicing a Cylinder  **

SimpleCylinderGenerator does not generate a TextureCoordinates property, which is fine if the cylinder is only going to be covered with a SolidColorBrush. On the other hand, if you want to use any type of GradientBrush, or any type of TileBrush, you'll need a TextureCoordinates property that has been intelligently generated along with the Positions property.

When a cylinder is covered with 2D brush, you probably want the brush to wrap around the cylinder so that the left and right edges of the brush meet at a line I'll call the "seam." This seam should run along the length of the cylinder. That's yet another reason to use right triangles rather than isosceles triangles for defining the mesh.

SimpleCylinderGenerator also reveals another of my biases, one of economy: all the Point3D objects in the Positions collection are unique. Within the for loop that generates the TriangleIndices collection, the modulus operator is used so that some later triangles use points early in the Positions collection. It somehow seemed to me that this might be necessary to "close up" the object, but that's just plain wrong. Again, if you want to cover the cylinder with a brush, you'll need to have duplicate Point3D objects at the seam so that the same location in 3D space maps to both the left side and the right side of the brush.

SimpleCylinderGenerator takes the easiest approach imaginable in defining the Positions collection by making the cylinder have a length and radius of one unit. It then relies on the consumer of the class (which might be you or might be another programmer) to come up with the transforms that will size and position the cylinder appropriately. It's fairly easy to translate the base of the cylinder to another location with a translation transform, but what about placing the top of the cylinder at a particular location as well? You'll need to calculate a rotation transform just for that job.

Give the consumer a break! It makes more sense for a cylinder generator to have properties of Point1 and Point2 that indicate the coordinate positions of the centers of the two ends of the cylinder. You might also want to consider Radius1 and Radius2 properties for independently setting the radii at the two ends. There's an extra bonus with having separate radii properties: if one radius is 0, the algorithm generates a cone. When defining mesh-geometry algorithms, "bonus" primitives are always welcome.

So far we have a nice list of improvements to SimpleGeometryGenerator, but I want to make that list even longer.

Integrating with XAML

To use SimpleCylinderGenerator, you must define the class as a resource and then access it with a binding. It would be much preferable to have a class that instantiates in XAML and integrates right into the Viewport3D markup. But how? It can't derive from MeshGeometry3D or Geometry3D.

Fortunately, there's another way, but it requires a familiarity with the often confusing Media3D namespace and some of the terminology involved in class names. Let's first talk about the difference between visuals and models.

A visual is something that can render itself on the screen. Within the Media3D namespace, that's the abstract Visual3D class. Visual3D objects are children of the Viewport3D.

A model, in contrast, is a description of something that a visual can render. In 3D programming, models include the 3D objects themselves and also the lights that illuminate them. The same models can be shared among multiple visuals. In the Media3D namespace, for example, the model is represented by the abstract Model3D class. Classes that derive from Model3D include GeometryModel3D, Light (which is abstract), and Model3DGroup.

The class that hooks a visual together with a model is ModelVisual3D. ModelVisual3D derives from Visual3D, so it's definitely a visual, but it has a Content property of type Model3D. A model is the content of a visual in much the same way that text or a bitmap is the content of a button.

Amazingly enough, ModelVisual3D is the only class in the entire Media3D namespace that is neither sealed nor abstract, which means that it's available for inheritance with no apparent problems (see Daniel Lehenbauer's blog entry at blogs.msdn.com/478923.aspx and Karsten Januszewski's blog entry at blogs.msdn.com/479924.aspx for more information).

Suppose you code a Cylinder class that derives from ModelVisual3D. You could then specify such an element as a child of a Viewport3D:

<Viewport3D>
    <pmg:Cylinder Point1="0 1 5" Point2="3 2 -4" 
                  Radius1="0.25" Radius2="0.125" /> 
    ...
</Viewport3D>

This seems quite convenient, but it's not quite as simple as this markup would imply. If the Cylinder class derives from ModelVisual3D, then it inherits a Content property of type Model3D (from which GeometryModel3D derives). The Cylinder would have to create an object of type GeometryModel3D to set to its inherited Content property. GeometryModel3D defines a Geometry property, so the Cylinder class would also create a MeshGeometry3D object to set to this Geometry property.

So far, it's not bad, but here's the kicker. GeometryModel3D also has Material and BackMaterial properties. This is how materials get associated with a mesh geometry. To make this scheme work correctly, the Cylinder class has to redefine the Material and BackMaterial properties, so the markup will look like this:

<Viewport3D>
    <pmg:Cylinder Point1="0 1 5" Point2="3 2 -4" 
                  Radius1="0.25" Radius2="0.125">
        <pmg:Cylinder.Material>
        ...
        </pmg:Cylinder.Material>
        <pmg:Cylinder.BackMaterial>
        ...
        </pmg:Cylinder.BackMaterial>
    </pmg:Cylinder>
    ...
</Viewport3D>

Now comes the big question: I've indicated that the Cylinder class has properties named Point1, Point2, Radius1, and Radius2. Do you want those properties potentially to be targets of data bindings? I suspect you do. Do you want them to be animatable? Oh, I am sure you do. In either case, these properties must be defined as dependency properties. As you might have heard, everything in the Windows Presentation Foundation is animatable (but only if it's a dependency property).

Dependency Properties

Dependency properties have emerged as one of the most important innovations of the Windows Presentation Foundation. Within Windows Presentation Foundation, properties can be set in a variety of ways. Properties can be set directly on an object in code or XAML. In addition, properties can be set through styles and through data bindings. Some properties can be inherited through a parent-child relationship, and properties can be animated. If a property isn't set at all, it has a default value. Dependency properties are an attempt to make all this variety work correctly with predictable levels of precedence.

Dependency properties require some scary-looking overhead in your classes that involves registering the properties with the system, but after that's finished, you pretty much don't have to worry about how the property is being set. The important thing is that your class gets notified when the property has changed so it can react to those changes.

The Cylinder class, which is among the downloadable code for this article, defines nine dependency properties. Rather than Cylinder deriving directly from ModelVisual3D, I created an intermediate class named ModelVisualBase that might be parent to other classes such as Sphere, Tube, and BunnyRabbit. ModelVisualBase defines Geometry, Material, and BackMaterial dependency properties that add an owner to the same properties defined in GeometryModel3D.

As an example of how dependency properties are implemented in your code, let's look at the Radius1 property of Cylinder. Registering the dependency property involves defining a public static read-only field of type DependencyProperty named Radius1Property, that is, the property name with the word Property appended:

public static readonly DependencyProperty Radius1Property =
    DependencyProperty.Register(
        "Radius1", typeof(double), typeof(Cylinder),
        new PropertyMetadata(1.0, PositionsChanged),
                             ValidateNonNegative);

Notice the arguments to the PropertyMetadata constructor, which indicate the default value, a method in the Cylinder class to call when the value changes, and a method to validate values.

You also include a traditional property definition (also called a "CLR property") that references this static read-only field:

public double Radius1
{
    get { return (double)GetValue(Radius1Property); }
    set { SetValue(Radius1Property, value); }
}

The GetValue and SetValue methods are inherited from DependencyObject. Any class that implements dependency properties must derive from DependencyObject.

It's important to realize that changes to the Radius1 property don't always go through the CLR property. When Radius1 is being animated, for example, the Radius1Property field is accessed rather than the Radius1 property. For that reason, you should avoid doing anything else in the Radius1 property except calling GetValue and SetValue.

The definition of Radius1Property includes references to two methods in the Cylinder class. Because these methods are referenced by a static field, the methods themselves must also be static. The ValidateNonNegative method simply checks that Radius1 isn't being set to a negative value:

static bool ValidateNonNegative(object value)
{
    return (double)value >= 0;
}

If some code does set Radius1 to a negative value, an exception will be thrown, but that's not the responsibility of your class.

The other method is a notification to the class that the property has been changed. Because the property can be changed without the CLR property being accessed, this notification method is your only opportunity to react to this property change. Here's the method I called PositionsChanged that's called when the Radius1 property has been changed:

static void PositionsChanged(DependencyObject obj, 
    DependencyPropertyChangedEventArgs args)
{
    Cylinder cyl = (Cylinder)obj;
    cyl.GeneratePositions();
}

The method is static, but the first argument is the actual Cylinder object whose property is being altered. The DependencyPropertyChangedEventArgs object has information about the particular property being changed, its old value, and its new value, but Cylinder ignores that stuff and just calls an instance method: GeneratePositions. This method is responsible for generating the Positions and Normals properties of the MeshGeometry3D object based on the new radius.

Proper Handling of Collections

The Cylinder class defines three separate methods for defining the four collections of the MeshGeometry3D object. GeneratePositions is responsible for the Positions and Normals collections; the two other methods are named GenerateTriangleIndices and GenerateTextureCoordinates. Cylinder's constructor calls all three methods to initialize the object; thereafter they are called in response to changes in the dependency properties. For most properties (Point1, Point2, Radius1, Radius2) only the Positions and Normals collections need to be recalculated. For the Slices property, however, all four collections need to be redone. Similar to the Slices property is another property named Stacks that governs the lengthwise subdivision of the cylinder. In many cases, you can set Stacks equal to 1, which is the default value. But if you're using PointLight or SpotLight to illuminate the cylinder, you'll need to generate much smaller triangles to get the light to work correctly.

When properties such as Point1 and Radius1 are animated, a method like GeneratePositions can be called very frequently, and as fast as possible. Here are some potential problems and solutions.

In particular, the method should avoid memory allocations. The last thing you want is for the CLR garbage collection truck to come rolling through your animation cleaning up the messes you've left behind. If at all possible, any new expressions should refer to structures rather than classes. In the Cylinder class, for example, I have the constructor create objects of type RotateTransform3D and AxisAngleRotation3D that are stored as fields and reused by each call to GeneratePositions.

The collections that you set to the MeshGeometry3D properties are of type Point3DCollection, Vector3DCollection, Int32Collection, and PointCollection. All collections derive from Freezable and include a Changed event that is fired whenever any element of the collection changes. This provides a powerful tool: for example, individual members of these collections can be animated to create deformations of 3D objects. But if you're entirely recalculating the whole collection, you only want a notification to occur when you're finished. For this reason, you should detach the collection from the MeshGeometry3D object and reattach it when you're finished. Here's how GeneratePositions begins and ends:

void GeneratePositions()
{
    MeshGeometry3D mesh = (MeshGeometry3D)Geometry;
    Point3DCollection points = mesh.Positions;
    mesh.Positions = null;
    points.Clear();

        ...

    mesh.Positions = points;
}

The first statement accesses the Geometry property defined by ModelVisualBase and inherited by Cylinder. Notice that the method reuses the Point3DCollection rather than reallocating a new one. The four collections are created by Cylinder's constructor using the number of elements they will contain based on the default Slices property. If Slices increases, the collection size might then be insufficient and a memory allocation will have to take place, but this shouldn't happen very often.

On the other hand, if you need to populate a collection with a large number of elements, then you should create that collection with a constructor indicating the number of elements. Allocating the necessary memory initially helps avoid frequent memory allocations while adding elements to the collection. Thanks to Tim Cahill's blog entry of August 31, 2006 for these tip.

Seams and Ends

Earlier in this column, I discussed the seam where the left side of the brush meets the right. Where should this seam go? I decided it should go "in the back" of the cylinder, which in the common case is towards the negative Z axis. The only case in which this logic breaks down is for a cylinder along the Z axis, in which case the Cylinder class puts it on the bottom.

You can wrap an ImageBrush around the cylinder, and if the length and circumference of the cylinder match the aspect ratio of the bitmap, there won't be any distortion. One problem, however, involves the two ends of the cylinder. What should happen there? I decided I definitely wanted part of the image to wrap around the top and bottom ends. The proportion of the image to be wrapped would be governed by two additional properties named Fold1 and Fold2, with default values of 0.1 and 0.9 respectively. These indicate the relative brush coordinates where the image folds from the length of the cylinder to the ends.

The ends of the cylinder are approximated by a ring of triangles that share a common point in the center. If you "unfold" the triangles that define the cylinder and spread them out over a rectangular brush, you can visualize the correspondences between the mesh geometry and brush. Figure 7 shows such a correspondence for a 12-slice cylinder and a gradient brush. This correspondence seems reasonable, and if the dimensions of the brush, cylinder, and the "fold" settings are set right, the brush might not be stretched at all.

Figure 7 Brush Mapped to Cylinder

Figure 7** Brush Mapped to Cylinder **

But for a bitmap, you might want as few seams as possible. You already have a seam where the two sides of the bitmap meet. You might avoid additional seams on the ends of the cylinder by using a layout shown in Figure 8. Of course, there will be some stretching involved. (In either case, not all of the brush gets on the cylinder, but that's unavoidable unless you create much finer triangles.)

Figure 8 Better Brush Mapping

Figure 8** Better Brush Mapping **

I couldn't decide which I like best, so I implemented both. The first option seems to be suitable for DrawingBrush, and the second for ImageBrush, so I defined a TextureType property and a TextureType enumeration with three members: Drawing, Image, and None. The None option suppresses the generation of TextureCoordinates entirely.

Showcasing the Cylinder

Now it's time to put the Cylinder class through its paces. The ImageOnCylinder project defines a cylinder object in XAML and puts a bitmap on its surface:

<pmg:Cylinder x:Name="cyl" Slices="32" 
              TextureType="Image" Fold1="0.25" Fold2="0.75">
  <pmg:Cylinder.Material>
    <DiffuseMaterial>
      <DiffuseMaterial.Brush>
        <ImageBrush
         ImageSource="https://www.charlespetzold.com/PetzoldTattoo.jpg" 
        />
      </DiffuseMaterial.Brush>
    </DiffuseMaterial>
  </pmg:Cylinder.Material>
  ...

The ImageOnCylinder.xaml file animates the Point1, Radius1, and Radius2 properties, and the cylinder is also rotated slowly around its axis. A snapshot is shown in Figure 9. The program also provides a scrollbar to rotate the camera around the X axis. (When running any of these samples, if you can't see the whole image, trying making the window narrower.) The PolkaDottedCylinder project defines a DrawingBrush with carefully calculated dimensions to avoid distortion, as shown in Figure 10.

Figure 9 ImageOnCylinder

Figure 9** ImageOnCylinder **

Figure 10 Polka Dots

Figure 10** Polka Dots **

The final example goes back to a SolidColorBrush, but has a total of 10 cylinders, and shows the convenience of precisely defining the locations of the two ends. When two 3D objects meet, they meld rather nicely, as shown in Figure 11. Two scrollbars are provided for viewing the chair from different angles (but don't try sitting on it just yet).

Figure 11 SteelChair

Figure 11** SteelChair **

Throw Out the First One

There's an old recommendation for writing software: after you're finished, throw out what you've done and start over, incorporating everything you learned when writing the first version. Of course, this approach isn't always practical, but it worked for me. The Cylinder class I wrote is my fifth or sixth stab at writing a class that generates a mesh geometry, and I now feel I'm ready to move on to the sphere.

Send your questions and comments to  mmnet30@microsoft.com.

Charles Petzold is a Contributing Editor to MSDN Magazine and the author of Applications=Code+Markup: A Guide to the Microsoft Windows Presentation Foundation (Microsoft Press, 2006).