WPF Tutorials Table of Contents | Return to www.kindohm.com
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
- Triangle indeces
- Triangle normals
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.
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:
-
GeometryModel3D - a model described by a Mesh (
MeshGeometry3D) and a Material (DiffuseMaterial). -
MeshGeometry3D - a mesh. Has a
Positionscollection, aTriangleIndecescollection, and aNormalscollection. - Point3D - the most basic unit of mesh construction. Used to define positions and vectors.
- Vector3D - used in the calculation of normals.
- DiffuseMaterial - gives a model color and texture.
-
DirectionalLight - provides light so that objects in the
Viewport3Dcan be seen. Without it, nothing shows up.
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:
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:
- A cube is composed of 12 triangles instead of one (six sides, each with two triangles)
- The triangles are oriented in each cardinal direction of the Cartesian coordinate system, which may or may not be easy to visualize at times. It can be particularly tricky when remembering which way would be counter-clockwise for the right-hand-rule.
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:
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:
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:
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:
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:
With normals turned on:
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:
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
- Return to the WPF Article Table of Contents
- Mike's Inetium WPF Blog
- Karsten J's Blog
- Karsten's "3D Sandbox" app
- Tim Sneath's Blog
- Tim's Five Great WPF 3D Nuggets
- Windows SDK
- System.Windows.Media.Media3D Namespace documentation
- Twin Cities Metro in 3D
- Improving 3D Performance in WPF
- The North Face In-Store Explorer Proof-of-Concept: A White Paper (From 2005 PDC)
[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.







