WPF Tutorials Table of Contents | Return to www.kindohm.com

Download source code (58 KB)

Windows Presentation Foundation (WPF) 3D Tutorial

The purpose of this tutorial is to provide the simplest explanation and examples possible of how to create 3D graphics with Windows Presentation Foundation (WPF) (formerly known as "Avalon"). It is not a comprehensive guide to 3D modeling, but simply a primer for those who have no knowledge of or experience with 3D graphics. 3D or vector-based graphics is a different world from 2D GDI, and it took a long period of guessing, stumbling, and trial and error before I understood what was going on with it. Ultimately I hope that you are able to avoid some of the pains I went through by reading through this tutorial.

For your convenience, the tutorial contains inline links to the documentation of all of the .Net 3.0 classes, structures, properties, and methods used in the example code. Click away as you please.

Disclaimer

While I make my best effort to make sure the content and examples in this tutorial are accurate and bug-free, I'm not responsible for anything bad (e.g. crashes, loss of data, any damage) that may happen if you use the information or code in this tutorial. I'll do my best to make sure bugs are fixed and that the content is as accurate as possible. In short, have some common sense and use this at your own risk.

Mike's Version of 3D Graphics Theory

When I started 3D graphics in WPF (back when it was still called Avalon), I had no idea what I was doing. I didn't have a clue what the significance was of a mesh, triangle index, or normal. This was the most painful part about learning 3D modeling, because without a minimal understanding of these things, nothing will show up correctly in WPF (or you'll just get lucky). In short, all you really need to know is what a mesh is and what it is composed of [1].

What is a mesh?

A mesh is basically a representation of a surface. The mesh represents the surface through a system of points and lines. The points describe the high and low areas of the surface, and the lines connect the points to establish how you get from one point to the next.

At a minimum, a surface is a flat plane. A flat plane needs three points to define it. Thus, the simplest surface that can be described in a mesh is a single triangle. It turns out that meshes can only be described with triangles. That is because a triangle is the simplest, most granular way to define a surface. A large, complex surface obviously can't be accurately described by one triangle. Instead, it can be approximated by many smaller triangles. You could argue that you could use a rectangle to define a surface, but it's not as granular as a triangle. When you think about it, a rectangle can be broken into two triangles. Two triangles can much more accurately describe a surface than a single rectangle. Ok enough... the point is that a mesh represents a surface through many triangles.

A whole mesh is composed of:

Mesh Positions

A mesh position is the location of a single point on a surface. The more dense the points are, the more accurately the mesh describes the surface.

Triangle Indeces

A triangle index is a mesh position that defines one of the three points of a triangle in the mesh. The mesh positions alone cannot describe the mesh triangles. After the positions have been added, you need to define what positions make up which triangles.

In WPF, the order in which you add mesh positions is important. A position's index value in a mesh's position collection is used when adding triangle indeces. For example, let's say you have a surface composed of five positions {p0, p1, p2, p3, p4}. If you wanted to define a triangle from p1, p3, and p4, you would add triangle indeces with index values 1, 3, and 4. If the positions were added in a different order {p3, p4, p0, p2, p1} and you wanted a triangle made of the same positions, you would add triangle indeces with index values 4, 0, and 1.

The order in which you add triangle indeces is also important. When you define a triangle, you are basically defining the points in either a clockwise or counter-clockwise direction (depending on which side of the triangle you're on, of course). The reason this is important is because it affects which side of the triangle is visible. When I say "side", I don't mean which of the three triangle sides, but which side of the plane.

Let's say you're looking straight ahead at the surface of a triangle. If you define its indeces in a clockwise direction, the side you are looking at will be invisible and the opposite side will be visible. If you define its indeces in a counter-clockwise direction, the side you are looking at will be visible and the opposite side will be invisible. You can use the "right hand rule" to remember this. Take your right hand and make a "thumbs up" sign. The direction your fingers curl is counter-clockwise and your thumb points up (or out) in the direction the surface would be visible.

Exactly why a right-hand-rule applies to index ordering is beyond me. My best guess is that WPF doesn't think it is necessary or efficient to render both sides of the triangle, so you need to pick one of them.

Triangle Normals

After defining positions and triangle indeces, you need to add normals to each position. While the direction in which you add triangle indeces determines which side of the triangle is visible, a normal is used by WPF to know how the surface should be lit by a light source.

A normal is a vector that is perpendicular to the surface of the triangle. The normal vector is computed as the "cross product" of two vectors that make up the side of the triangle. If you have a triangle defined by points A, B, and C, you could create the normal by multiplying AB x AC, BC x BA, or CB x CA. All three ways will result in the same normal. However, the right-hand rule still applies. AB x AC will result in a normal in the opposite direction from AC x AB.

Normal 1
Normal 2

In general you'll want your normals to point in the same direction as the visible side of your triangle surface. However, if the normals are angled away from being perpendicular, the surface of the triangle will have a more interesting lighting effect.

Each position in the mesh should have a normal assigned to it, and a position can only have one normal. You'll add normals to the mesh in the same order that you added the positions. In other words, the index values of the normals collection in the mesh corresponds to the index values of the positions collection.

The more positions you have, the more normals you have. The more normals you have, the more pleasant the lighting and shading will be. In a mesh, a single point may be the index of more than one triangle. In this case, you may want to use multiple points with the same coordinates so that you can have multiple normals at that point. Take the corner of a cube, for example. The corner of a cube is the intersection of three different triangles on the cube. If you only used one position in the mesh to define the common index of those three triangles, you could only use one normal vector for that position. As a result, two of the triangles at that position won't be shaded as well as it could be. It would be better to define that cube corner with three unique positions. Each of the three triangles would use their own unique points, and you could use three normals at that corner instead of just one.

Getting Started With the Code

I'll assume you have a basic understanding of WPF and how to create a basic WPF user interface with XAML. So with that, let's jump in and create a new WPF application in VS.Net 2005. Add the following XAML to the app to create a simple layout with a panel for buttons and a Viewport3D for displaying our 3D stuff:

<Grid>
 <DockPanel 
  Width="Auto" 
  VerticalAlignment="Stretch" 
  Height="Auto" 
  HorizontalAlignment="Stretch" 
  Grid.ColumnSpan="1" 
  Grid.Column="0" 
  Grid.Row="0" 
  Margin="0,0,0,0" 
  Grid.RowSpan="1">
  <StackPanel>
   <StackPanel.Background>
    <LinearGradientBrush>
      <GradientStop Color="White" Offset="0"/>
      <GradientStop Color="DarkKhaki" Offset=".3"/>
      <GradientStop Color="DarkKhaki" Offset=".7"/>
      <GradientStop Color="White" Offset="1"/>
    </LinearGradientBrush>
   </StackPanel.Background>
   <StackPanel Margin="10">
    <Button 
     Name="simpleButton" 
     Click="simpleButtonClick">Simple</Button>
   </StackPanel>
  </StackPanel>
  <Viewport3D Name="mainViewport" ClipToBounds="True">
   <Viewport3D.Camera>
    <PerspectiveCamera 
	  FarPlaneDistance="100"
	  LookDirection="-11,-10,-9"
	  UpDirection="0,1,0"
	  NearPlaneDistance="1" 
	  Position="11,10,9" 
	  FieldOfView="70" />
    </Viewport3D.Camera>
    <ModelVisual3D>
     <ModelVisual3D.Content>
	   <DirectionalLight 
	    Color="White" 
	    Direction="-2,-3,-1" />
	  </ModelVisual3D.Content>
   </ModelVisual3D>
  </Viewport3D>
 </DockPanel>
</Grid>

Basically, all of the 3D stuff in WPF happens in Viewport3D controls. That's where we'll be adding our 3D models once we start writing some code. Notice that a PerspectiveCamera has been added to the Viewport3D. The camera is used to allow us to "view" what's in the model from the user interface. Note that the camera is looking at the point {0,0,0} in the model.

The model also contains a DirectionalLight light source so that we can view stuff in the model.

I encourage you to change the camera's LookDirection and Position while you go through the examples. The screenshots in this tutorial do not necessarily use the LookDirection and Position values in the XAML above.

The XAML above has one Button named simpleButton, so we need to hook up its click event to a method named simpleButtonClick in the window's code-behind file.

private void simpleButtonClick(object sender, RoutedEventArgs e)
{
}

Creating a Simple Mesh

When you click the simpleButton button, you're going to create a very simple mesh and add the model to the Viewport3D. But before you do that, you'll need to know a little bit about the types of 3D classes and structures you'll be working with:

Full documentation of all of the System.Windows.Media.Media3D classes can be found at msdn.microsoft.com

Add the code

Before we start digging into the nuts and bolts, make sure you import the using System.Windows.Media.Media3D namespace into the code-behind. That namespace has all of the classes we'll be working with:

using System.Windows.Media.Media3D;

Now let's create a simple mesh and add it to the Viewport3D. We'll start with the simplest mesh possible: a triangle. It'll be located near the origin of the model (remember where the PerspectiveCamera is pointing?) and will have one side along the X-axis (5 units long), one along the Z-axis (5 units long), and a hypotenuse connecting the first two sides.

First, create a new MeshGeometry3D:

MeshGeometry3D triangleMesh = new MeshGeometry3D();

Next, define the three points of the triangle:

Point3D point0 = new Point3D(0, 0, 0);
Point3D point1 = new Point3D(5, 0, 0);
Point3D point2 = new Point3D(0, 0, 5);

Next, add the three points as positions in the mesh:

triangleMesh.Positions.Add(point0);
triangleMesh.Positions.Add(point1);
triangleMesh.Positions.Add(point2);

Now we'll add the triangle indeces to define the triangle of the mesh. Since our entire mesh is a triangle, this might seem redundant, but WPF doesn't know how the points connect. Remember the right-hand-rule. We want the top surface of the triangle to be visible, so add the indeces in the appropriate order:

triangleMesh.TriangleIndices.Add(0);
triangleMesh.TriangleIndices.Add(2);
triangleMesh.TriangleIndices.Add(1);

Next we'll add the normal vectors for the mesh positions. Since this one's easy (the normal will point straight up in the Y direction), we'll just create a normal vector with known dimensions rather than computing a cross product:

Vector3D normal = new Vector3D(0, 1, 0);
triangleMesh.Normals.Add(normal);
triangleMesh.Normals.Add(normal);
triangleMesh.Normals.Add(normal);

Next we'll need to create a DiffuseMaterial for the surface, add the mesh to a model, and add the model to the Viewport3D:

Material material = new DiffuseMaterial(
    new SolidColorBrush(Colors.DarkKhaki));
GeometryModel3D triangleModel = new GeometryModel3D(
    triangleMesh, material);
ModelVisual3D model = new ModelVisual3D();
model.Content = triangleModel;
this.mainViewport.Children.Add(model);

When all is said and done, the simpleButtonClick event handler will look like this:

private void simpleButtonClick(object sender, RoutedEventArgs e)
{
    MeshGeometry3D triangleMesh = new MeshGeometry3D();
    Point3D point0 = new Point3D(0, 0, 0);
    Point3D point1 = new Point3D(5, 0, 0);
    Point3D point2 = new Point3D(0, 0, 5);
    triangleMesh.Positions.Add(point0);
    triangleMesh.Positions.Add(point1);
    triangleMesh.Positions.Add(point2);
    triangleMesh.TriangleIndices.Add(0);
    triangleMesh.TriangleIndices.Add(2);
    triangleMesh.TriangleIndices.Add(1);
    Vector3D normal = new Vector3D(0, 1, 0);
    triangleMesh.Normals.Add(normal);
    triangleMesh.Normals.Add(normal);
    triangleMesh.Normals.Add(normal);
    Material material = new DiffuseMaterial(
        new SolidColorBrush(Colors.DarkKhaki));
    GeometryModel3D triangleModel = new GeometryModel3D(
        triangleMesh, material);
    ModelVisual3D model = new ModelVisual3D();
    model.Content = triangleModel;
    this.mainViewport.Children.Add(model);
}

That's it! The code produces the following result:

Simple Result

Hardly exciting, I know, but just think - now you understand the building blocks of 3D meshes! Still not excited? Ok, then next we'll move on to a cube.

Creating a Cube

A cube is just an extension of creating a triangle. The differences are:

Let's continue by adding another button to the button panel in the XAML:

<Button Name="cubeButton" Click="cubeButtonClick">Cube</Button>

Then, hook up the cubeButtonClick event handler in the code-behind:

private void cubeButtonClick(object sender, RoutedEventArgs e)
{
}

We can now do a little refactoring of the code to make life easier. But before we do that we need to talk about the Model3DGroup class. A Model3DGroup is a collection of GeometryModel3D objects. In other words, a Model3DGroup can contain many meshes. On top of that, Model3DGroup objects can contain other Model3DGroup objects too, and so on. Last, a Model3DGroup can be added to a Viewport3D. What does all this mean? It provides a means to create small sets of 3D objects and make them a part of other models easily. Think of it as creating a Windows User Control that you can re-use in other User Controls.

So, let's start refactoring by abstracting out the creation of a single triangle mesh with normals at its triangle indeces. Add these two methods (CreateTriangleModel() and CalculateNormal() to the code-behind:

private Model3DGroup CreateTriangleModel(Point3D p0, Point3D p1, Point3D p2)
{
    MeshGeometry3D mesh = new MeshGeometry3D();
    mesh.Positions.Add(p0);
    mesh.Positions.Add(p1);
    mesh.Positions.Add(p2);
    mesh.TriangleIndices.Add(0);
    mesh.TriangleIndices.Add(1);
    mesh.TriangleIndices.Add(2);
    Vector3D normal = CalculateNormal(p0, p1, p2);
    mesh.Normals.Add(normal);
    mesh.Normals.Add(normal);
    mesh.Normals.Add(normal);
    Material material = new DiffuseMaterial(
        new SolidColorBrush(Colors.DarkKhaki));
    GeometryModel3D model = new GeometryModel3D(
        mesh, material);
    Model3DGroup group = new Model3DGroup();
    group.Children.Add(model);
    return group;
}
private Vector3D CalculateNormal(Point3D p0, Point3D p1, Point3D p2)
{
    Vector3D v0 = new Vector3D(
        p1.X - p0.X, p1.Y - p0.Y, p1.Z - p0.Z);
    Vector3D v1 = new Vector3D(
        p2.X - p1.X, p2.Y - p1.Y, p2.Z - p1.Z);
    return Vector3D.CrossProduct(v0, v1);
}

The CreateTriangleModel() method can be used anywhere to create a Model3DGroup that contains a mesh defined by three supplied points. Pretty nifty. The CalculateNormal() method is used by CreateTriangleModel() to get its normals for its triangle indeces. It does this easily using the CrossProduct method of the Vector3D structure. This is really cool because now all you have to do is know the mesh positions and these two methods do the rest for you.

Next, let's visualize what the cube will look like in Cartesian space. There are eight unique points, and we'll order them like so:

Cube 1

Now let's create a cube in code. Add this code to the cubeButtonClick() event handler:

private void cubeButtonClick(object sender, RoutedEventArgs e)
{
    Model3DGroup cube = new Model3DGroup();
    Point3D p0 = new Point3D(0, 0, 0);
    Point3D p1 =new Point3D(5, 0, 0);
    Point3D p2 =new Point3D(5, 0, 5);
    Point3D p3 =new Point3D(0, 0, 5);
    Point3D p4 =new Point3D(0, 5, 0);
    Point3D p5 =new Point3D(5, 5, 0);
    Point3D p6 =new Point3D(5, 5, 5);
    Point3D p7 = new Point3D(0, 5, 5);
    //front side triangles
    cube.Children.Add(CreateTriangleModel(p3, p2, p6));
    cube.Children.Add(CreateTriangleModel(p3, p6, p7));
    //right side triangles
    cube.Children.Add(CreateTriangleModel(p2, p1, p5));
    cube.Children.Add(CreateTriangleModel(p2, p5, p6));
    //back side triangles
    cube.Children.Add(CreateTriangleModel(p1, p0, p4));
    cube.Children.Add(CreateTriangleModel(p1, p4, p5));
    //left side triangles
    cube.Children.Add(CreateTriangleModel(p0, p3, p7));
    cube.Children.Add(CreateTriangleModel(p0, p7, p4));
    //top side triangles
    cube.Children.Add(CreateTriangleModel(p7, p6, p5));
    cube.Children.Add(CreateTriangleModel(p7, p5, p4));
    //bottom side triangles
    cube.Children.Add(CreateTriangleModel(p2, p3, p0));
    cube.Children.Add(CreateTriangleModel(p2, p0, p1));
    
    ModelVisual3D model = new ModelVisual3D();
    model.Content = cube;
    this.mainViewport.Children.Add(model);
}

Now when you run the code, you'll see a cube in the app:

Cube Rendered

Clearing the Viewport

If you've tried clicking both the "Simple" and "Cube" buttons one after another, you've essentially added a cube right on top of the original triangle (or vice-versa). Not a big deal, but kind of annoying if you like keeping things clean. Add this method to the code-behimd to clear out the Viewport3D except for the light source:

private void ClearViewport()
{
    ModelVisual3D m;
    for (int i = mainViewport.Children.Count - 1; i >= 0; i--)
    {
        m = (ModelVisual3D)mainViewport.Children[i];
        if (m.Content is DirectionalLight == false)
            mainViewport.Children.Remove(m);
    }
}

Call the ClearViewport() method whenever you want to clean things up (e.g. each time you click a button that adds some stuff).

Controlling the Camera

Let's make another small enhancement that makes it easier to move the camera and where it's looking without having to modify the XAML all the time. Add the following TextBlock and TextBox code to the XAML above the buttons we've already added:

<TextBlock Text="Camera X Position:"/>
<TextBox Name="cameraPositionXTextBox" MaxLength="5" 
   HorizontalAlignment="Left" Text="9"/>
<TextBlock Text="Camera Y Position:"/>
<TextBox Name="cameraPositionYTextBox" MaxLength="5" 
   HorizontalAlignment="Left" Text="8"/>
<TextBlock Text="Camera Z Position:"/>
<TextBox Name="cameraPositionZTextBox" MaxLength="5" 
   HorizontalAlignment="Left" Text="10"/>
<Separator/>
<TextBlock Text="Look Direction X:"/>
<TextBox Name="lookAtXTextBox" MaxLength="5" 
   HorizontalAlignment="Left" Text="-9"/>
<TextBlock Text="Look Direction Y:"/>
<TextBox Name="lookAtYTextBox" MaxLength="5" 
   HorizontalAlignment="Left" Text="-8"/>
<TextBlock Text="Look Direction Z:"/>
<TextBox Name="lookAtZTextBox" MaxLength="5" 
   HorizontalAlignment="Left" Text="-10"/>
<Separator/>

<!-- buttons -->
<Button Name="simpleButton" Click="simpleButtonClick">Simple</Button>
<Button Name="cubeButton" Click="cubeButtonClick">Cube</Button>

Now, add a new method to the code behind named SetCamera() with this code:

private void SetCamera()
{
    PerspectiveCamera camera = (PerspectiveCamera)mainViewport.Camera;
    Point3D position = new Point3D(
        Convert.ToDouble(cameraPositionXTextBox.Text),
        Convert.ToDouble(cameraPositionYTextBox.Text),
        Convert.ToDouble(cameraPositionZTextBox.Text)
    );
    Vector3D lookDirection = new Vector3D(
        Convert.ToDouble(lookAtXTextBox.Text),
        Convert.ToDouble(lookAtYTextBox.Text),
        Convert.ToDouble(lookAtZTextBox.Text)
    );
    camera.Position = position;
    camera.LookDirection = lookDirection;
}

The code simply grabs the points entered in the text boxes, creates Point3D objects from the values, and assigns the points to the Position and LookDirection properties of the camera. Go ahead and add a call to SetCamera() whenever you click one of the buttons or add something to the Viewport3D.

Now you can move the camera around and get a different view of things:

Cube Camera

The ScreenSpaceLines3D Class

Back in the beta days of WPF, it used to contain a class named ScreenSpaceLines3D. ScreenSpaceLines3D was used to draw a simple line in 3D space. Unfortunately, in the .Net 3.0 Framework, the ScreenSpaceLines3D class no longer exists, nor does another class exist that replaces its functionality.

Fortunately, Dan Lehenbauer has designed a new ScreenSpaceLines3D class for use with .Net 3.0 and WPF. The remainder of this tutorial will use that class, so you'll need to go and download his code from this 3DTools project on CodePlex.com. Extract the files, build the solution, and reference the 3DTools dll in your WPF project. Then add the following using statement in your code:

using _3DTools;

We'll be using ScreenSpaceLines3D to help do interesting things with normals and wireframes, but We won't use it just yet. We'll come back to it in a minute.

Add Normals to the Rendered Model

Wouldn't it be cool to see exactly where the normals are and what direction they're pointing in? Absolutely. Is it necessary? Well, it depends... it can be very handy when building more complex surfaces (like later in this tutorial). We can easily do this by drawing lines with the ScreenSpaceLines3D class. All we really want to do is draw a short little line from the triangle indeces out in the direction of the indeces' normal. To accomplish this, add the following code to the panel containing our controls in the XAML:

<Separator/>

<CheckBox Name="normalsCheckBox">Show Normals</CheckBox>
<TextBlock Text="Normal Size:"/>
<TextBox Name="normalSizeTextBox" Text="1"/>

The XAML above will allow us to use a check box to toggle whether the normals are shown or not and to set how big the normals are. Next, add this method to the code-behind:

private Model3DGroup BuildNormals(
    Point3D p0,
    Point3D p1,
    Point3D p2,
    Vector3D normal)
{
    Model3DGroup normalGroup = new Model3DGroup();
    Point3D p;
    ScreenSpaceLines3D normal0Wire = new ScreenSpaceLines3D();
    ScreenSpaceLines3D normal1Wire = new ScreenSpaceLines3D();
    ScreenSpaceLines3D normal2Wire = new ScreenSpaceLines3D();
    Color c = Colors.Blue;
    int width = 1;
    normal0Wire.Thickness = width;
    normal0Wire.Color = c;
    normal1Wire.Thickness = width;
    normal1Wire.Color = c;
    normal2Wire.Thickness = width;
    normal2Wire.Color = c;
    double num = 1;
    double mult = .01;
    double denom = mult * Convert.ToDouble(normalSizeTextBox.Text);
    double factor = num / denom;
    p = Vector3D.Add(Vector3D.Divide(normal, factor), p0);
    normal0Wire.Points.Add(p0);
    normal0Wire.Points.Add(p);
    p = Vector3D.Add(Vector3D.Divide(normal, factor), p1);
    normal1Wire.Points.Add(p1);
    normal1Wire.Points.Add(p);
    p = Vector3D.Add(Vector3D.Divide(normal, factor), p2);
    normal2Wire.Points.Add(p2);
    normal2Wire.Points.Add(p);

    //Normal wires are not models, so we can't
    //add them to the normal group.  Just add them
    //to the viewport for now...
    this.mainViewport.Children.Add(normal0Wire);
    this.mainViewport.Children.Add(normal1Wire);
    this.mainViewport.Children.Add(normal2Wire);

    return normalGroup;
}

The method above takes three points (the points of a triangle) and a given normal vector. It then draws some lines out from the points in the direction of the normal and adds them to a Model3DGroup. It also scales the normals using the Divide method of the Vector3D structure and the value entered in the normalSizeTextBox. Pretty simple.

Then add this code near the end of the CreateTriangleModel method:

if (normalsCheckBox.IsChecked == true)
                group.Children.Add(BuildNormals(p0, p1, p2, normal));

Now when you build the cube with normals, you'll see this:

Click to enlarge
Cube With Normals

Building a Topography

At this point, you know all of the basics of my loose 3D theory and how meshes work in WPF. Let's give it another shot though and create a surface that isn't so orthogonal. Imagine if you will a topography made up of peaks and valleys. A surface where the points are connected by vectors that majestically dance together through space, creating a scene so delightful that you drool on your keyboard. Or, just imagine a topography.

First, add another button into the control panel. This new button will be used to create and add the topography to the model:

<Button 
     Name="topographyButton" 
     Click="topographyButtonClick">
          Topography
     </Button>

Essentially, we're going to create a topography that stretches out in the X-Z plane and has peaks and valleys in the Y direction. We'll make it a 10x10 surface. From overhead, the mesh will essentially look like a checkerboard whose squares are divided in two by triangles. Using a nested loop (one loop for the X direction and one for the Z) and some random number generation, we can create the points of the topography:

private Point3D[] GetRandomTopographyPoints()
{
    //create a 10x10 topography.
    Point3D[] points = new Point3D[100];
    Random r = new Random();
    double y;
    double denom = 1000;
    int count = 0;
    for (int z = 0; z < 10; z++)
    {
        for (int x = 0; x < 10; x++)
        {
            System.Threading.Thread.Sleep(1);
            y = Convert.ToDouble(r.Next(1, 999)) / denom;
            points[count] = new Point3D(x, y, z);
            count += 1;
        }
    }
    return points;
}

Once you have the array of points, you can use another simple set of loops to load the points into triangles using our good old CreateTriangleModel() method. Add another button to the XAML in the button panel named topographyButton and hook up an event handler named topographyButtonClick in the code-behind:

private void topographyButtonClick(object sender, RoutedEventArgs e)
{
    ClearViewport();
    SetCamera();
    Model3DGroup topography = new Model3DGroup();
    Point3D[] points = GetRandomTopographyPoints();
    for (int z = 0; z <= 80; z = z + 10)
    {
        for (int x = 0; x < 9; x++)
        {
            topography.Children.Add(
                CreateTriangleModel(
                        points[x + z], 
                        points[x + z + 10], 
                        points[x + z + 1])
            );
            topography.Children.Add(
                CreateTriangleModel(
                        points[x + z + 1], 
                        points[x + z + 10], 
                        points[x + z + 11])
            );
        }
    }
    ModelVisual3D model = new ModelVisual3D();
    model.Content = topography;
    this.mainViewport.Children.Add(model);
}

Basically, the nested loops just zig-zag around the grid and adds points for the triangles. When you run the app, you'll see something like this:

Click to enlarge
Topography

With normals turned on:

Click to enlarge
Topography with normals

Add a Wireframe

Another helpful tool in visualizing a mesh is to see its "wireframe". A wireframe is just a visual representation of the mesh positions and sides of the mesh triangles. It gives a little more definition to the surface so that you can see all of the edges, peaks, and valleys. To draw the wireframe, we'll use the ScreenSpaceLines3D class again. All we need to do is enhance the CreateTriangleModel() method.

First, add some more XAML to the controls panel in the application so that we can have the option of viewing the wireframe when the model is rendered:

<Separator/>
<CheckBox Name="wireframeCheckBox">Show Wireframe</CheckBox>

Next, add the following code at the end of the CreateTriangleModel() method to add the wireframe if the wireframeCheckBox control is checked:

if (wireframeCheckBox.IsChecked == true)
{
 ScreenSpaceLines3D wireframe = new ScreenSpaceLines3D();
        wireframe.Points.Add(p0);
        wireframe.Points.Add(p1);
        wireframe.Points.Add(p2);
        wireframe.Points.Add(p0);
        wireframe.Color = Colors.LightBlue;
        wireframe.Thickness = 3;
        
        this.mainViewport.Children.Add(wireframe);
}

The code above just uses the existing points that have already been defined in the method and draws a path to connect them. The path is then added to the viewport. The final CreateTriangleModel() method will look like this:

private Model3DGroup CreateTriangleModel(Point3D p0, Point3D p1, Point3D p2)
{
   MeshGeometry3D mesh = new MeshGeometry3D();
   mesh.Positions.Add(p0);
   mesh.Positions.Add(p1);
   mesh.Positions.Add(p2);
   mesh.TriangleIndices.Add(0);
   mesh.TriangleIndices.Add(1);
   mesh.TriangleIndices.Add(2);
   Vector3D normal = CalculateNormal(p0, p1, p2);
   mesh.Normals.Add(normal);
   mesh.Normals.Add(normal);
   mesh.Normals.Add(normal);
   Material material = new DiffuseMaterial(
      new SolidColorBrush(Colors.DarkKhaki));
   GeometryModel3D model = new GeometryModel3D(mesh, material);
   Model3DGroup group = new Model3DGroup();
   group.Children.Add(model);

   if (normalsCheckBox.IsChecked == true)
         group.Children.Add(BuildNormals(p0, p1, p2, normal));

   if (wireframeCheckBox.IsChecked == true)
   {
         ScreenSpaceLines3D wireframe = new ScreenSpaceLines3D();
         wireframe.Points.Add(p0);
         wireframe.Points.Add(p1);
         wireframe.Points.Add(p2);
         wireframe.Points.Add(p0);
         wireframe.Color = Colors.LightBlue;
         wireframe.Thickness = 3;
         this.mainViewport.Children.Add(wireframe);
   }

   return group;
}

When you include the wireframe in the model, you'll see this:

Wireframe

Welcome to 3D Land!

That's it! You now have a foundation of the building blocks of meshes (positions, triangle indeces, and normals) and the classes used to create 3D objects in WPF. You should be able to apply these principles to more complex 3D shapes and problems in WPF.

Resources and Other Links


[1] (go back ^) there is more to a mesh than what I've written about in this tutorial, but just for the basics of 3D in WPF I'm keeping it limited to the bare minimum.

Valid XHTML 1.0 Transitional Valid CSS!