Wpf Cube Three Dee
My Cube (3D graphics in Wpf)
At the end of this little tutorial you should be able to create an application that produces a number of cubes and lets the user rotate and zoom (rotate with sliders and zoom with mouse wheel). It will look a little something like the below screen shot. If you are very impatient you can scroll to the bottom of this page and download the source code.

Lights, Camera... Action!
3D-graphics in WPF is remarkably simple to set up - you just add a camera, some light and then some objects. In the tutorial from Wpf Tutorial (see [1]) a very simple example is shown with the following principle XAML:
<Viewport3D>
<Viewport3D.Camera>
<PerspectiveCamera ... />
</Viewport3D.Camera>
<ModelVisual3D>
<ModelVisual3D.Content>
<Model3DGroup>
<DirectionalLight ... />
<GeometryModel3D>
...
</GeometryModel3D>
</Model3DGroup>
</ModelVisual3D.Content>
</ModelVisual3D>
</Viewport3D>
As you can see in the XAML code above there is a Viewport3D with a camera and a model. The model has some light and a geometry model. This idea will follow in my little example. But I am building the model in a programmatic way instead of declaring everything in XAML.
The Triangles
The base of all 3D-graphics is the triangle. I will not explain much of the details (again see the tutorial at Wpf Tutorial or see the other links below) - I will instead try to explain the code behind how I make triangles in this example.
The code is borrowed from [2] and uses a helper-method to help build the triangles (this will later be used to build the cubes). As you can see in the code snippet below this helper creates a Model3DGroup - this is used in the WPF snippet in the above section. So here we are using code to generate the triangles, this is to me a lot more sensible since we can easily reproduce this many times. As you can see we are also adding a material using a brush.
public Model3DGroup CreateTriangle(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 = VectorHelper.CalcNormal(p0, p1, p2);
mesh.Normals.Add(normal);
mesh.Normals.Add(normal);
mesh.Normals.Add(normal);
Material material = new DiffuseMaterial(new SolidColorBrush(_color));
GeometryModel3D model = new GeometryModel3D(mesh, material);
Model3DGroup group = new Model3DGroup();
group.Children.Add(model);
return group;
}
The Cube
I want to be able to add a lot of cubes to my Viewport3D so I have a method (again I started off with code from Codegod) that generates a cube in the point (x, y, z). In short this method creates 8 points (the corners of the cube), 12 triangles (two for each side of the cube) and adds this to a 3D model (a Model3DGroup).
public ModelVisual3D Create(int x, int y, int z)
{
Model3DGroup cube = new Model3DGroup();
int A = 5;
int B = 5;
int C = 5;
Point3D p0 = new Point3D(0 + x, 0 + y, 0 + z);
Point3D p1 = new Point3D(A + x, 0 + y, 0 + z);
Point3D p2 = new Point3D(A + x, 0 + y, C + z);
Point3D p3 = new Point3D(0 + x, 0 + y, C + z);
Point3D p4 = new Point3D(0 + x, B + y, 0 + z);
Point3D p5 = new Point3D(A + x, B + y, 0 + z);
Point3D p6 = new Point3D(A + x, B + y, C + z);
Point3D p7 = new Point3D(0 + x, B + y, C + z);
//front
cube.Children.Add(CreateTriangle(p3, p2, p6));
cube.Children.Add(CreateTriangle(p3, p6, p7));
//right
cube.Children.Add(CreateTriangle(p2, p1, p5));
cube.Children.Add(CreateTriangle(p2, p5, p6));
//back
cube.Children.Add(CreateTriangle(p1, p0, p4));
cube.Children.Add(CreateTriangle(p1, p4, p5));
//left
cube.Children.Add(CreateTriangle(p0, p3, p7));
cube.Children.Add(CreateTriangle(p0, p7, p4));
//top
cube.Children.Add(CreateTriangle(p7, p6, p5));
cube.Children.Add(CreateTriangle(p7, p5, p4));
//bottom
cube.Children.Add(CreateTriangle(p2, p3, p0));
cube.Children.Add(CreateTriangle(p2, p0, p1));
ModelVisual3D model = new ModelVisual3D();
model.Content = cube;
return model;
}
The Cubes
As you could see in the code that generates a cube the side was hard-coded to 5. This is of course not very elegant - but as a first example I think it is ok. In the code below, where I make a lot of cubes I place them in a grid of size 6 - so that there will be a 20% space between each cube. I place them in a "castle" like layout.
public void Render()
{
CubeBuilder cubeBuilder = new CubeBuilder(CubeColor);
int A = 6;
int B = 6;
int C = 6;
// origin
mainViewport.Children.Add(cubeBuilder.Create(0, 0, 0));
//side 1
mainViewport.Children.Add(cubeBuilder.Create(1 * A, 0, 0));
mainViewport.Children.Add(cubeBuilder.Create(2 * A, 0, 0));
mainViewport.Children.Add(cubeBuilder.Create(3 * A, 0, 0));
mainViewport.Children.Add(cubeBuilder.Create(4 * A, 0, 0));
//side 2
mainViewport.Children.Add(cubeBuilder.Create(4 * A, 1 * B, 0));
mainViewport.Children.Add(cubeBuilder.Create(4 * A, 2 * B, 0));
//side 3
mainViewport.Children.Add(cubeBuilder.Create(4 * A, 3 * B, 0));
mainViewport.Children.Add(cubeBuilder.Create(3 * A, 3 * B, 0));
mainViewport.Children.Add(cubeBuilder.Create(2 * A, 3 * B, 0));
mainViewport.Children.Add(cubeBuilder.Create(1 * A, 3 * B, 0));
mainViewport.Children.Add(cubeBuilder.Create(0 * A, 3 * B, 0));
//side 4
mainViewport.Children.Add(cubeBuilder.Create(0, 2 * B, 0));
mainViewport.Children.Add(cubeBuilder.Create(0, 1 * B, 0));
//corner 1
mainViewport.Children.Add(cubeBuilder.Create(0, 0, 1 * C));
mainViewport.Children.Add(cubeBuilder.Create(0, 0, 2 * C));
//other corners
mainViewport.Children.Add(cubeBuilder.Create(4 * A, 0 * B, C));
mainViewport.Children.Add(cubeBuilder.Create(0 * A, 3 * B, C));
mainViewport.Children.Add(cubeBuilder.Create(4 * A, 3 * B, C));
}
Rotating and Zooming
Rotation
In order to rotate what we see we are adding a rotation transform on the camera (an into to transforms can be found at MSDN: [3]). In my example we insert this transform in the XAML code of the camera:
<Viewport3D.Camera>
<PerspectiveCamera ... >
<PerspectiveCamera.Transform>
<Transform3DGroup>
<RotateTransform3D>
<RotateTransform3D.Rotation>
<AxisAngleRotation3D x:Name="rotX" Axis="1 0 0" Angle="0" />
</RotateTransform3D.Rotation>
</RotateTransform3D>
< ... >
< ... >
< ... >
< ... >
This rotation is modified using a slider that we introduce in the XAML code:
<Slider Name="rotateX"
Minimum="0" Maximum="360"
Value="0" Width="120"
ValueChanged="rotateX_ValueChanged" />
The slider raises an event once modified that in turns call a rotate method. In that method we are modifying the angle of the rotation-transform:
private void rotateX_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
RotateX(e.NewValue);
}
public void RotateX(double angle)
{
rotX.Angle = angle;
}
The zoom
The zoom functionality was heavily inspired by the examples at this excellent Code Project article: [4] .
We want the mouse wheel event on the window and not just the Viewport3D to trigger a zoom code. we first add the event to the window block of the XAML code.
<Window ... MouseWheel="Window_MouseWheel">
In the code we update the position (z only to simplify things) by this simple code block. In principle we reposition the camera.
private void Window_MouseWheel(object sender, MouseWheelEventArgs e)
{
mCamera.Position = new System.Windows.Media.Media3D.Point3D(
mCamera.Position.X,
mCamera.Position.Y,
mCamera.Position.Z - e.Delta / 250D);
}
Download Visual Studio Solution
You can download the Visual Studio solution file (here [5]) and play around with it.
Acknowledgments
- A starting point at Wpf Tutorial: [6]
- Mouse wheel event and depth from the Code Project: [7]
- Major inspiration and a lot of code came from Codegod: [8]
This page belongs in Kategori Programmering