Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Walking Robot Series In WPF -- Part 1: Triangles, Rectangles, And Cubes

0.00/5 (No votes)
3 Nov 2010 2  
Part one of a series on how to make an animated 3D robot in WPF using C# code
screen_shot_small.JPG

Introduction

Yes, this is another rotating cube example in WPF. But it's also the first part of my series that will present how to make an animated walking robot in WPF using C# code. Very little XAML is used in this project because we are modeling everything in C# code. In this first part, we will introduce some basic reusable classes for building shapes in WPF and set up a simple animated scene. In future instalments, we will talk about other kinds of shapes, back materials, smooth shading and flat shading, and storyboards. The classes we use to make the cube will be combined to make our robot, a simple character with its own movements. All the source is included for each instalment of the series.

We begin by starting a new project in Visual Studio 2010. Choose WPF Application as the project type. You will have to add this XAML to the MainWindow:

<ContentControl Name="contentControl2">

    <Viewport3D ClipToBounds="True" Width="Auto" Height="Auto">
    </Viewport3D>

</ContentControl>

You will need to add a couple of using statements to your MainWindow.xaml.cs:

using System.Windows.Media.Animation;
using System.Windows.Media.Media3D;

We will need some basic classes to do the work of modeling our cubes. You know that 3D geometry models in WPF are made of triangle meshes. So we will need a C# class to build a triangle.

Add a class to your project and start adding some variables. Obviously we need 3 points and a constructor:

private Point3D p1;
private Point3D p2;
private Point3D p3;

public WpfTriangle(Point3D P1, Point3D P2, Point3D P3)
{
    p1 = P1;
    p2 = P2;
    p3 = P3;
}

We will need some code to add our points to a GeometryMesh3D so they can be used in a model. Since we are going to be combining triangles from different shape classes together to make a single model, we will pass in the mesh variable and add our triangle points to it:

public static void addTriangleToMesh(Point3D p0, Point3D p1, Point3D p2,
    MeshGeometry3D mesh, bool combine_vertices)
{
    Vector3D normal = CalculateNormal(p0, p1, p2);

    if (combine_vertices)
    {
        addPointCombined(p0, mesh, normal);
        addPointCombined(p1, mesh, normal);
        addPointCombined(p2, mesh, normal);
    }
    else
    {
        mesh.Positions.Add(p0);
        mesh.Positions.Add(p1);
        mesh.Positions.Add(p2);
        mesh.TriangleIndices.Add(mesh.TriangleIndices.Count);
        mesh.TriangleIndices.Add(mesh.TriangleIndices.Count);
        mesh.TriangleIndices.Add(mesh.TriangleIndices.Count);
        mesh.Normals.Add(normal);
        mesh.Normals.Add(normal);
        mesh.Normals.Add(normal);
    }
}

We have made this a static function because we will usually want to add a triangle only as part of a larger form. Rarely would we ever construct a triangle model on its own. We will be using hundreds of triangles, so we don't want to instantiate a triangle class just to add a triangle to a mesh.

You will notice that we have included an argument called combine_vertices. This is very important because of the difference between flat shading and smooth shading in WPF. For our cube model, we want to use flat shading. If you combine vertices for your triangle mesh, it leads to smooth shading using the Gouraud method. Later on, we will show how this can apply to shapes such as a cylinder where we might want to use smooth shading. For now, we will add the capability for combing vertices to our triangle class, even though it won't be used for the cube example:

public static void addPointCombined(Point3D point, MeshGeometry3D mesh, Vector3D normal)
{
    bool found = false;

    int i = 0;

    foreach (Point3D p in mesh.Positions)
    {
        if (p.Equals(point))
        {
            found = true;
            mesh.TriangleIndices.Add(i);
            mesh.Positions.Add(point);
            mesh.Normals.Add(normal);
            break;
        }

        i++;
    }

    if (!found)
    {
        mesh.Positions.Add(point);
        mesh.TriangleIndices.Add(mesh.TriangleIndices.Count);
        mesh.Normals.Add(normal);
    }
}

When combining points in a triangle mesh, each unique point (Position) is only listed once in the mesh, even though it may belong to several triangles. A separate list (TriangleIndices) is used to store the indices for each point that is used to make each triangle. A third list is needed to provide the normals for each triangle. This is necessary for calculating the angle of incidence between a light source and the surface of each triangle. This is how WPF does flat shading. It uses Gouraud interpolation to smooth out the shading, but only if you combine the points that are the same. If you list each point separately, flat shading will be the result. This is what we want for our cube.

Normals are calculated using a simple cross product. WPF gives you a built in Vector member function for that. Here is our member function that calculates a normal for our triangle:

public static 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);
}

This allows us to fill the Normals list while we are building our mesh.

We add another member function to construct a triangle GeometryModel3D, just in case we wanted to do that. It won't be needed for this project, but I include it anyway. It takes a color as an argument and constructs the model using a DiffuseMaterial of that solid color:

public static GeometryModel3D CreateTriangleModel
    (Point3D P0, Point3D P1, Point3D P2, Color color)
{
    MeshGeometry3D mesh = new MeshGeometry3D();

    addTriangleToMesh(P0, P1, P2, mesh);

    Material material = new DiffuseMaterial(new SolidColorBrush(color));

    GeometryModel3D model = new GeometryModel3D(mesh, material);

    return model;
}

Now since we are making a cube, we need a rectangle class to use for each side of the cube. Conveniently, a rectangle can be made of just two triangles. We only need 4 points to specify our rectangle:

private Point3D p0;
private Point3D p1;
private Point3D p2;
private Point3D p3;

public WpfRectangle(Point3D P0, Point3D P1, Point3D P2, Point3D P3)
{
    p0 = P0;
    p1 = P1;
    p2 = P2;
    p3 = P3;
}

As with the triangle, we want to add the rectangle to a MeshGeometry3D that we are building. The member function to do that looks similar, and builds on what we created for our triangle:

public static void addRectangleToMesh
    (Point3D p0, Point3D p1, Point3D p2, Point3D p3, MeshGeometry3D mesh)
{
    WpfTriangle.addTriangleToMesh(p0, p1, p2, mesh);
    WpfTriangle.addTriangleToMesh(p2, p3, p0, mesh);
}

As with the triangle, we will not always want to instantiate a rectangle by itself, so we make this member function static and pass in the points each time. The order of the points when we call WpfTriangle.addTriangleToMesh will ensure that each triangle has the same winding direction. This is important because by default, shapes in WPF have only one side and are invisible from the other side. This is determined by the winding direction or order of the points in each triangle, so it is important to keep this consistent for all triangles in your model.

We add another member function for when we have an instance of a rectangle and want to add it to a mesh without specifying the points, letting it use its member points instead:

public void addToMesh(MeshGeometry3D mesh)
{
    WpfTriangle.addTriangleToMesh(p0, p1, p2, mesh);
    WpfTriangle.addTriangleToMesh(p2, p3, p0, mesh);
}

The complete code for the triangle and rectangle classes is available in the zip file for this article. Let's move on to the cube. Only 4 member variables are necessary in order to completely describe our cube:

private Point3D origin;
private double width;
private double height;
private double depth;

public WpfCube(Point3D P0, double w, double h, double d)
{
    width = w;
    height = h;
    depth = d;

    origin = P0;
}

We provide a member function for adding a cube to a mesh. For convenience, it is a static function that will construct a cube and then add it to the mesh. It does this by constructing a rectangle for each side and then adding each of those to the mesh:

public static void addCubeToMesh(Point3D p0, double w, double h, double d,
    MeshGeometry3D mesh)
{
    WpfCube cube = new WpfCube(p0, w, h, d);

    double maxDimension = Math.Max(d, Math.Max(w, h));

    WpfRectangle front = cube.Front();
    WpfRectangle back = cube.Back();
    WpfRectangle right = cube.Right();
    WpfRectangle left = cube.Left();
    WpfRectangle top = cube.Top();
    WpfRectangle bottom = cube.Bottom();

    front.addToMesh(mesh);
    back.addToMesh(mesh);
    right.addToMesh(mesh);
    left.addToMesh(mesh);
    top.addToMesh(mesh);
    bottom.addToMesh(mesh);
}

We also provide a version for an instance of a cube. It does its work by calling the static version:

public GeometryModel3D CreateModel(Color color)
{
    return CreateCubeModel(origin, width, height, depth, color);
}

The cube has member functions that can be called to make each component rectangle based on its width, height, depth and origin:

public WpfRectangle Front()
{
    WpfRectangle r = new WpfRectangle(origin, width, height, 0);

    return r;
}

public WpfRectangle Back()
{
    WpfRectangle r = new WpfRectangle(new Point3D
        (origin.X + width, origin.Y, origin.Z + depth), -width, height, 0);

    return r;
}

public WpfRectangle Left()
{
    WpfRectangle r = new WpfRectangle(new Point3D(origin.X, origin.Y, origin.Z + depth),
        0, height, -depth);

    return r;
}

public WpfRectangle Right()
{
    WpfRectangle r = new WpfRectangle(new Point3D(origin.X + width, origin.Y, origin.Z),
        0, height, depth);

    return r;
}

public WpfRectangle Top()
{
    WpfRectangle r = new WpfRectangle(origin, width, 0, depth);

    return r;
}

public WpfRectangle Bottom()
{
    WpfRectangle r = new WpfRectangle
        (new Point3D(origin.X + width, origin.Y - height, origin.Z),
        -width, 0, depth);

    return r;
}

These 6 member functions are called when adding the cube to our MeshGeometry3D. The complete code for the cube can be found in the zip file for this example.

Those are all the classes we will need. We now just have to add some code to our MainWindow.xaml.cs to make our cube and create a scene.

private double sceneSize = 10;

Point3D lookat = new Point3D(0, 0, 0);

The above variables define an arbitrary size for our scene coordinates and a lookat point for our camera.

Right click on the Viewport3D that you added in the designer view and add a Loaded event. See the comments for an explanation of what it does:

private void Viewport3D_Loaded(object sender, RoutedEventArgs e)
{
    if (sender is Viewport3D)
    {
        Viewport3D viewport = (Viewport3D)sender;

        // create a cube with dimensions as some fraction of the scene size
        WpfCube cube = new WpfCube(new System.Windows.Media.Media3D.Point3D
            (0, 0, 0), sceneSize / 6, sceneSize / 6, sceneSize / 6);

        // construct our geometry model from the cube object
        GeometryModel3D cubeModel = cube.CreateModel(Colors.Aquamarine);

        // create a model group to hold our model
        Model3DGroup groupScene = new Model3DGroup();

        // add our cube to the model group
        groupScene.Children.Add(cubeModel);

        // add a directional light
        groupScene.Children.Add(positionLight
        (new Point3D(-sceneSize, sceneSize / 2, 0.0)));

        // add ambient lighting
        groupScene.Children.Add(new AmbientLight(Colors.Gray));

        // add a camera
        viewport.Camera = camera();

        // create a visual model that we can add to our viewport
        ModelVisual3D visual = new ModelVisual3D();

        // populate the visual with the geometry model we made
        visual.Content = groupScene;

        // add the visual to our viewport
        viewport.Children.Add(visual);

        // animate the model
        turnModel(cube.center(), cubeModel, 0, 360, 3, true);
    }
}

We need some helper functions in our window to accomplish some of the work that we listed above. These functions create out lights and camera. They also make use of the sceneSize variable we defined earlier, so if you want to change the coordinate base, everything will still work together:

public PerspectiveCamera camera()
{
    PerspectiveCamera perspectiveCamera = new PerspectiveCamera();

    perspectiveCamera.Position = new Point3D(-sceneSize, sceneSize / 2, sceneSize);

    perspectiveCamera.LookDirection = new Vector3D
                    (lookat.X - perspectiveCamera.Position.X,
                                               lookat.Y - perspectiveCamera.Position.Y,
                                               lookat.Z - perspectiveCamera.Position.Z);

    perspectiveCamera.FieldOfView = 60;

    return perspectiveCamera;
}

public DirectionalLight positionLight(Point3D position)
{
    DirectionalLight directionalLight = new DirectionalLight();
    directionalLight.Color = Colors.Gray;
    directionalLight.Direction = new Point3D(0, 0, 0) - position;
    return directionalLight;
}

Here is the helper function that animates our cube. You can read the comments to see what it is doing:

public void turnModel(Point3D center, GeometryModel3D model,
    double beginAngle, double endAngle, double seconds, bool forever)
{
    // vectors serve as 2 axes to turn our model
    Vector3D vector = new Vector3D(0, 1, 0);
    Vector3D vector2 = new Vector3D(1, 0, 0);

    // create rotations to use. We can set a 0.0 degrees
    // for our rotations since we are going to animate them
    AxisAngleRotation3D rotation = new AxisAngleRotation3D(vector, 0.0);
    AxisAngleRotation3D rotation2 = new AxisAngleRotation3D(vector2, 0.0);

    // create double animations to animate each of our rotations
    DoubleAnimation doubleAnimation =
        new DoubleAnimation(beginAngle, endAngle, durationTS(seconds));
    DoubleAnimation doubleAnimation2 =
        new DoubleAnimation(beginAngle, endAngle, durationTS(seconds));

    // set the repeat behavior and duration for our animations
    if (forever)
    {
        doubleAnimation.RepeatBehavior = RepeatBehavior.Forever;
        doubleAnimation2.RepeatBehavior = RepeatBehavior.Forever;
    }

    doubleAnimation.BeginTime = durationTS(0.0);
    doubleAnimation2.BeginTime = durationTS(0.0);

    // create 2 rotate transforms to apply to our model.
    // Each needs a rotation and a center point
    RotateTransform3D rotateTransform = new RotateTransform3D(rotation, center);
    RotateTransform3D rotateTransform2 = new RotateTransform3D(rotation2, center);

    // create a transform group to hold our 2 transforms
    Transform3DGroup transformGroup = new Transform3DGroup();
    transformGroup.Children.Add(rotateTransform);
    transformGroup.Children.Add(rotateTransform2);

    // set our model transform to the transform group
    model.Transform = transformGroup;

    // begin the animations -- specify a target object and
    // property for each animation -- in this case,
    // the targets are the two rotations we created and
    // we are animating the angle property for each one
    rotation.BeginAnimation(AxisAngleRotation3D.AngleProperty, doubleAnimation);
    rotation2.BeginAnimation(AxisAngleRotation3D.AngleProperty, doubleAnimation2);
}

We just need a couple more helper functions that help us specify our animation durations in seconds and convert them to time spans that WPF can use:

private int durationM(double seconds)
{
    int milliseconds = (int)(seconds * 1000);
    return milliseconds;
}

public TimeSpan durationTS(double seconds)
{
    TimeSpan ts = new TimeSpan(0, 0, 0, 0, durationM(seconds));
    return ts;
}

So that is our simple rotating cube. The triangle and other classes that we made will be useful in constructing our simple scene with a walking robot character. The animation concepts will also come in handy later for making the robot's arms and legs move. In the next article in the series, we will add a cylinder class and demonstrate the difference between flat shading and smooth shading.

History

  • 1st November, 2010: Initial version

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here