3D Animation in VB.NET Using Windows Presentation Foundation (WPF) - Part 2

In the previous article we covered many of the basics of rendering some simple 3D cubes, and then we animated them. In this article we are going to take it a few steps further. The first task we are going to tackle is moving around our 3D world. Regulars to these articles will recognize some of the techniques I'll be using here.

The Third Project

Let's start with a simple background to make the movement a little more visible. We going to use the same cubes from the first two projects and we going to put a large cube over the rest. Also, we are going to use an inverted cube, so that the inside of the faces are rendered.

Let's begin, Start a new Windows Presentation Foundation (WPF) project and copy and paste the below code into Window1.xaml.
        <Viewport3D  Name="Viewport3D1" Margin="12,41,12,12">
        <Viewport3D.Camera>
            <PerspectiveCamera  Position="-40,40,40" LookDirection="40,-40,-40 " 
                         UpDirection="0,0,1" />
        </Viewport3D.Camera>
        <ModelVisual3D >
            <ModelVisual3D.Content>
                <Model3DGroup >
                    <DirectionalLight Color="White"  Direction="-10,-10,-10" />
                    <GeometryModel3D>
                        <GeometryModel3D.Geometry>
                            <MeshGeometry3D Positions="0,0,0 10,0,0 10,10,0 0,10,0 0,0,10  10,0,10 10,10,10 0,10,10"
                              TriangleIndices="0 3 1 1 3 2   0 4 3 4 7 3  4 6 7 4 5 6  0 1 4 1 5 4  1 2 6 6 5 1  2 3 7 7 6 2 "/>
                        </GeometryModel3D.Geometry>
                        <GeometryModel3D.Material>
                            <DiffuseMaterial Brush="red" AmbientColor="Yellow" Color="Yellow" />
                        </GeometryModel3D.Material>
                    </GeometryModel3D>
                   </Model3DGroup>
                </ModelVisual3D.Content>
            </ModelVisual3D>
            <ModelVisual3D >
                <ModelVisual3D.Content>
                    <Model3DGroup >
                        <DirectionalLight Color="White"  Direction="-10,-10,-10" />
                        <GeometryModel3D>
                            <GeometryModel3D.Geometry>
                                <MeshGeometry3D Positions="0,-10,-10 10,-10,-10 10,0,-10 0,0,-10 0,-10,0 10,-10,0 10,0,0 0,0,0"
                              TriangleIndices="0 3 1 1 3 2   0 4 3 4 7 3  4 6 7 4 5 6  0 1 4 1 5 4  1 2 6 6 5 1  2 3 7 7 6 2 "/>
                            </GeometryModel3D.Geometry>
                            <GeometryModel3D.Material>
                                <DiffuseMaterial Brush="yellow" AmbientColor="Yellow" Color="Yellow"  />
                            </GeometryModel3D.Material>
                        </GeometryModel3D>
                    </Model3DGroup>
                </ModelVisual3D.Content>
            </ModelVisual3D>
            <ModelVisual3D >
                <ModelVisual3D.Content>
                    <Model3DGroup >
                        <DirectionalLight Color="White"  Direction="-10,-10,-10" />
                        <GeometryModel3D>
                            <GeometryModel3D.Geometry>
                                <MeshGeometry3D Positions="-10,-10,0 0,-10,0 0,0,0 -10,0,0 -10,-10,10  0,-10,10 0,0,10 -10,0,10"
                             TriangleIndices="0 3 1 1 3 2   0 4 3 4 7 3  4 6 7 4 5 6  0 1 4 1 5 4  1 2 6 6 5 1  2 3 7 7 6 2 "/>
                            </GeometryModel3D.Geometry>
                            <GeometryModel3D.Material>
                                <DiffuseMaterial Brush="green" AmbientColor="white" Color="white" />
                            </GeometryModel3D.Material>
                        </GeometryModel3D>
                    </Model3DGroup>
                </ModelVisual3D.Content>
            </ModelVisual3D>
            <ModelVisual3D >
                <ModelVisual3D.Content>

                    <Model3DGroup >
                        <DirectionalLight Color="White"  Direction="-10,-10,-10" />
                        <GeometryModel3D>
                            <GeometryModel3D.Geometry>
                                <MeshGeometry3D Positions="-10,0,-10 0,0,-10 0,10,-10 -10,10,-10 -10,0,0 0,0,0 0,10,0 -10,10,0"
                              TriangleIndices="0 3 1 1 3 2   0 4 3 4 7 3  4 6 7 4 5 6  0 1 4 1 5 4  1 2 6 6 5 1  2 3 7 7 6 2 "/>
                            </GeometryModel3D.Geometry>
                            <GeometryModel3D.Material>
                                <DiffuseMaterial Brush="blue" AmbientColor="white" Color="white" />
                            </GeometryModel3D.Material>
                        </GeometryModel3D>
                    </Model3DGroup>
                </ModelVisual3D.Content>
            </ModelVisual3D>
        <ModelVisual3D >
            <ModelVisual3D.Content>
                <Model3DGroup >
                    <DirectionalLight Color="White"  Direction="-10,-10,-10" />
                    <GeometryModel3D>
                        <GeometryModel3D.Geometry>
                            <MeshGeometry3D Positions="-40,-40,-40 40,-40,-40 40,40,-40 -40,40,-40 -40,-40,40  40,-40,40 40,40,40 -40,40,40"
                              TriangleIndices="0 1 3 1 2 3   0 3 4 4 3 7  4 7 6 4 6 5  0 4 1 1 4 5  1 6 2 6 1 5  2 7 3 7 2 6 "/>
                        </GeometryModel3D.Geometry>
                        <GeometryModel3D.Material>
                            <DiffuseMaterial Brush="brown" AmbientColor="Yellow" Color="Yellow" />
                        </GeometryModel3D.Material>
                    </GeometryModel3D>
                </Model3DGroup>
            </ModelVisual3D.Content>
            </ModelVisual3D>
        </Viewport3D>

You will notice that most of the XAML above is the same from our previous WPF projects, with the exception of the last 3D Model. Here you will notice the Triangle Indices are in a different order to the rest. This gives us our inverted cube where you see the face on the inside of the cube but not from the outside.

And now place this in the VB code area:

Imports System.Windows.Media.Media3D
Imports System.Windows.Media.Animation

    Private Enum Tristate
        Neg = -1
        None = 0
        Pos = 1
    End Enum

    Private newpcam As New Media3D.PerspectiveCamera()
    Private Pos1 As New Media3D.Point3D(-40, 40, 40)
    Private Dir1 As New Media3D.Point3D(40, -40, -40)
    Private timer As System.Windows.Threading.DispatcherTimer
    Private X As Double = -40
    Private Y As Double = 40
    Private Z As Double = 40
    Private XMove As Tristate
    Private YMove As Tristate
    Private ZMove As Tristate

    Private Sub SetX(ByVal Val As Double)
        Pos1.X = Val
        Dir1.X = -Val
    End Sub

    Private Sub SetY(ByVal Val As Double)
        Pos1.Y = Val
        Dir1.Y = -Val
    End Sub

    Private Sub SetZ(ByVal Val As Double)
        Pos1.Z = Val
        Dir1.Z = -Val
    End Sub

    Private Sub Window1_KeyDown(ByVal sender As Object, ByVal e As System.Windows.Input.KeyEventArgs) Handles Me.PreviewKeyDown
        Select Case e.Key
            Case Key.Up
                YMove = Tristate.Pos
            Case Key.Down
                YMove = Tristate.Neg
            Case Key.Left
                XMove = Tristate.Neg
            Case Key.Right
                XMove = Tristate.Pos
            Case Key.PageUp
                ZMove = Tristate.Neg
            Case Key.PageDown
                ZMove = Tristate.Pos
        End Select
    End Sub

    Private Sub Window1_KeyUp(ByVal sender As Object, ByVal e As System.Windows.Input.KeyEventArgs) Handles Me.PreviewKeyUp
        Select Case e.Key
            Case Key.Up, Key.Down
                YMove = Tristate.None
            Case Key.Left, Key.Right
                XMove = Tristate.None
            Case Key.PageUp,Key.PageDown
                ZMove = Tristate.None
        End Select
    End Sub

    Sub timer_Tick(ByVal sender As Object, ByVal e As EventArgs)
        If XMove <> Tristate.None Then
            X += XMove
            SetX(X)
        End If
        If YMove <> Tristate.None Then
            Y += YMove
            SetY(Y)
        End If
        If ZMove <> Tristate.None Then
            Z += ZMove
            SetZ(Z)
        End If
        newpcam.Position = Pos1
        newpcam.LookDirection = Dir1
    End Sub

    Private Sub Window1_Loaded(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs) Handles MyBase.Loaded
        Viewport3D1.Camera = newpcam
        SetX(X)
        SetY(Y)
        SetZ(Z)
        newpcam.Position = Pos1
        newpcam.LookDirection = Dir1

        Dim Cube_ani As New DoubleAnimation(0, 359, TimeSpan.FromSeconds(2))

        Dim Cube_rot As New AxisAngleRotation3D()
        Cube_rot.Axis = New Vector3D(0, 1, 0)

        Viewport3D1.Children.Item(0).Transform = New RotateTransform3D(Cube_rot, 5, 5, 5)
        Viewport3D1.Children.Item(1).Transform = New RotateTransform3D(Cube_rot, 5, -5, -5)
        Viewport3D1.Children.Item(2).Transform = New RotateTransform3D(Cube_rot, -5, -5, 5)
        Viewport3D1.Children.Item(3).Transform = New RotateTransform3D(Cube_rot, -5, 5, -5)

        Cube_rot.BeginAnimation(AxisAngleRotation3D.AngleProperty, Cube_ani)

        timer = New System.Windows.Threading.DispatcherTimer()
        timer.Interval = TimeSpan.FromMilliseconds(16)
        AddHandler timer.Tick, AddressOf timer_Tick
        Me.timer.Start()
    End Sub

Regulars will notice the Tristate Type that I love to use, and the Timer to smooth out the animation. Beyond that, let's see what's new. In the KeyDown event (We're using the PreviewKeyDown in case the textboxes have focus) we use the normal use left, right, up and down to adjust the X and Y coordinates, but here we add Page Up and Down to adjust the Z coordinate.

And as usual in the KeyDown event, we set the relevant Tristate variable to positive or negative according to the key pressed. Then in the KeyUp event we set the Tristate back to zero.

When you run the application, you will see the rotating cubes similar to those in the second project from part 1, with a brown background. Now start pressing a few of the direction keys. You will notice that you can move left, right, up, down, in and out. What we are doing here is moving the camera around in our 3D mini world. However you will notice that we are always turning the vector to look directly at the rotating cubes. Well that's okay for now, and a working copy of this project, with textboxes showing us the current X,Y and Z positions, is available in the downloads.

On the next page we are going to build our little 3D world and apply a few textures to the faces.

3D Animation in VB.NET Using Windows Presentation Foundation (WPF) - Part 2

Project Four

In this project we are building a complete 3D world. For now we are going to concentrate on the 3D world and moving around in it. By ignoring the individual object animation and other bits, we can properly concentrate on what is needed for smooth accurate movement in the 3D world.

Before we start, Please remember that there are many ways to perform the task at hand, and while it may be 'Outdated' the method I'm going to be describing here is a tried and trusted method, and also gives the programmer a goodly amount of control.

To start we going to create an essentially blank viewport, with only the model lighting set. We going to be building a very complex multi-layered maze via our VB code so we don't need anything more than the ModelGroup.

        <Viewport3D  Name="Viewport3D1" Margin="0,0,0,0">
        <Viewport3D.Camera>
            <PerspectiveCamera Position="0,0,0" LookDirection="0,0,0 " 
                         UpDirection="0,0,1" />
        </Viewport3D.Camera>
            <ModelVisual3D>
                <ModelVisual3D.Content>
                    <Model3DGroup x:Name="Theworld">
                        <AmbientLight Color="Gray"/>
                        <DirectionalLight Color="Gray" Direction="-1,0,0"/>
                        <DirectionalLight Color="Gray" Direction="1,0,1"/>
                    </Model3DGroup>
                </ModelVisual3D.Content>
            </ModelVisual3D>
        </Viewport3D>

Copy and paste the above code, into a new WPF project form editor. As you can see there's nothing to it; make note of how you define the Codes working name of the Model3DGroup object. We've also setup a few Lighting source's to light up our world.

Okay lets start with some VB code. First thing we have to do is create a standard rectangle.

    Private Sub MakeRectangle(ByRef Geometry As MeshGeometry3D, ByVal A As Point3D, ByVal B As Point3D, ByVal C As Point3D, ByVal D As Point3D, ByVal M As Point, ByVal N As Point, ByVal O As Point, ByVal P As Point)
        ' Set the points.
        Geometry.Positions.Add(A)
        Geometry.Positions.Add(B)
        Geometry.Positions.Add(C)
        Geometry.Positions.Add(D)
        ' Set the texture coordinates.
        Geometry.TextureCoordinates.Add(M)
        Geometry.TextureCoordinates.Add(N)
        Geometry.TextureCoordinates.Add(O)
        Geometry.TextureCoordinates.Add(P)
        ' Make the 2 triangles for our Rectange.
        Dim TPoint As Integer = Geometry.Positions.Count - 4
        Geometry.TriangleIndices.Add(TPoint)
        Geometry.TriangleIndices.Add(TPoint + 1)
        Geometry.TriangleIndices.Add(TPoint + 2)
        Geometry.TriangleIndices.Add(TPoint)
        Geometry.TriangleIndices.Add(TPoint + 2)
        Geometry.TriangleIndices.Add(TPoint + 3)
    End Sub

The Subroutine above will create a rectangle of any size you choose, in any model you specify, and apply the texture to it. Now, when it come to the textureCoordinates, this MSDN article pretty much covers it better than I can. However we will be coming back to it a little later.

So what are we doing here. The Sub accepts the model we added the rectangle to, the four corner Coordinates of the rectangle, and the four texture points. It then adds these points to the model and calculates the two triangle indices needed to build it. Simple.

Right next we want to be able to add boxes to our models, similar to cubes, in that we are adding a six sided object. However cubes have six identical sized sides, and the boxes we are adding will not. So lets continue; here's our Box sub.

    Private Sub MakeBox(ByRef Geometry As MeshGeometry3D, ByVal A As Point3D, ByVal B As Point3D, ByVal C As Point3D, ByVal D As Point3D, ByVal E As Point3D, ByVal F As Point3D, ByVal G As Point3D, ByVal H As Point3D, ByVal M As Point, ByVal N As Point, ByVal O As Point, ByVal P As Point)

        MakeRectangle(Geometry, A, B, C, D, M, N, O, P)
        MakeRectangle(Geometry, H, G, F, E, M, N, O, P)

        MakeRectangle(Geometry, A, E, F, B, M, N, O, P)
        MakeRectangle(Geometry, C, G, H, D, M, N, O, P)

        MakeRectangle(Geometry, B, F, G, C, M, N, O, P)
        MakeRectangle(Geometry, D, H, E, A, M, N, O, P)
    End Sub

In this sub we give it the eight coordinates of the corners of our box, and the points for our texture. We then call the Rectangle sub for our six sides with the relevant Coordinates for each one..

Next we have a quick and easy sub that will create each of our models in our 3D world.

    Private Function MakeGeometry(ByVal Model As Model3DGroup, ByVal Image As Material) As MeshGeometry3D
        MakeGeometry = New MeshGeometry3D()
        Dim new_model As New GeometryModel3D(MakeGeometry, Image)
        Model.Children.Add(new_model)
    End Function

Not much here. Define the New Model, add it to the Model group, and assign the given Image to it.

On the next page we going to start building the world that we want to move around in.

3D Animation in VB.NET Using Windows Presentation Foundation (WPF) - Part 2

Building the Maze

Now that we have those subs out the way, let's look at some of the declarations that we will need.

In the code below, we are loading the different images that we are going to use into memory, then we'll create the required object that the Model will use to skin the objects. We then call our MakeGeometry sub to create the model with all the relevent info.

    Private Sub PrepMaze()
        Dim Floor_Bitmap As New ImageBrush(New BitmapImage(New Uri("pack://application:,,,/Floor.jpg")))
        Dim ceiling_Bitmap As New ImageBrush(New BitmapImage(New Uri("pack://application:,,,/ceiling.jpg")))
        Dim brick_Bitmap As New ImageBrush(New BitmapImage(New Uri("pack://application:,,,/walls.jpg")))
        Dim wood_Bitmap As New ImageBrush(New BitmapImage(New Uri("pack://application:,,,/Wood-wall.jpg")))
        Dim Floor_Image As Material = New DiffuseMaterial(Floor_Bitmap)
        Dim ceiling_Image As Material = New DiffuseMaterial(ceiling_Bitmap)
        Dim brick_Image As Material = New DiffuseMaterial(brick_Bitmap)
        Dim wood_Image As Material = New DiffuseMaterial(wood_Bitmap)
        Dim Floor_Geometry As MeshGeometry3D = MakeGeometry(Theworld, Floor_Image)
        Dim ceiling_Geometry As MeshGeometry3D = MakeGeometry(Theworld, ceiling_Image)
        Dim OWall_Geometry As MeshGeometry3D = MakeGeometry(Theworld, brick_Image)
        Dim IWall_Geometry As MeshGeometry3D = MakeGeometry(Theworld, wood_Image)

Comming back to the Textures. Each of these images have been trimmed and edited to create an almost seemless picture when repeatedly placed next to each other. Each mesh model can essentially only have one texture, but you can streach, rotate or repeate it as you please on the model. Something else to note' if a model does not have a texture, even just a plain color, assigned to it, it effectively is not visible.

Lets start building our maze. This is relatively simple, with all the Subs we've now created, it is as simple as looping over the sub and calling it with incrementive values.

         For x As Integer = -5 To 4 Step 1
            For z As Integer = -5 To 4 Step 1
                MakeRectangle(ceiling_Geometry, New Point3D(x, Roof, z), New Point3D(x + 1, Roof, z), New Point3D(x + 1, Roof, z + 1), New Point3D(x, Roof, z + 1), _
                        New Point(0, 0), New Point(0, 1), New Point(1, 1), New Point(1, 0))
            Next
        Next

Here you can see how we build the ceiling of our maze. The floor and outer walls are done in much the same manner.

For the Interior walls we want to use our six sided box sub from the previous page to render walls that we can see from all sides. (Remember that each face is only visible from the front)

It does not really matter how we create the maze walls, because we will render them much the same way as the rest of the walls.

Moving Around the Maze

Moving around a 3D world is a little different to the 2D world. In this case, the movement involved is back and forth on a horizontal plane ,much like the 2D Vector game Asteroids. However instead of along the X,Y plane we move along the X,Z plane. We also will not realy going to be moving on the Y plane but will be Looking up and down.

So now we have an X,Y,Z location and two angles for looking direction, One angle that we will also use for X,Z movement direction.

And now we get to what I normally call the Sprite object. This is a Class object that holds the important information needed to place, in this case, our camera.

    Private Class Sprite
        Private _Position As Point3D
        Private _HAngle As Double
        Private _VAngle As Double

Obviously we have a number of properties to set and get these. What we now also have to do is add some math to the project so that we can move our Sprite around.

The first Math sub we are going add is to recalculate the current X,Z position if we move backward or forward on the HAngle direction.

        Private Function Move(ByVal Left As Double, ByVal Distance As Double, ByRef Pos As Point3D)
            Pos.X += Sin(Left * PI / 180) * Distance
            Pos.Z += Cos(Left * PI / 180) * Distance
        End Function

Wait, is that it? Yes. We simply add the Cos and Sin of the angle to the X and Z location. This function accepts an Angle(Left), a movement amount(Distance) and the point(pos) to move. It then updates the point to the new location.

Next we are going to calculate the looking direction. This, if you remember, is a 3D point with reference from the standing point.

        Private Function Calculate(ByVal Left As Double, ByVal Up As Double, ByVal Distance As Double) As Point3D
            Dim X1 As Double
            Dim Z1 As Double
            Calculate = New Point3D

            X1 = Sin(Left * PI / 180) * Distance
            Z1 = Cos(Left * PI / 180) * Distance

            Calculate.Y = (Sin(Up * PI / 180) * Distance)
            Calculate.Z = (Cos(Up * PI / 180) * Z1)
            Calculate.X = (Cos(Up * PI / 180) * X1)

        End Function

This one is a little more complex but still simple. We pass it our Horizontal angle(Left), Verticle angle(Up) and the Distance to this point. Using Cos and Sin again and the two angles we can calculate the 3D point that will be exactly the given distance away in the 3D world. This is then the location we use for the looking direction of our camera.

Here is a screen capture of the rendered maze from Project 4, available in the downloads.

[Maze.jpg]
Figure 1: The rendered maze from Project 4

In Closing

As you can see here, 3D rendering has been made easy with WPF. You no longer have to worry about the intricate calculations required to perform the rendering; all you need to do is create your 3D model, apply a texture to it and give it a location.

These projects are designed to give you the startup tools needed to start creating your own 3D worlds. You may notice that at the moment the code does not include collision detection, so you could walk through the walls and right out of the maze. In the Next article we will look at some methods of interacting with objects within the 3D world.

Happy 3D game writing.



About the Author

Richard Newcombe

Richard Newcombe has been involved in computers since the time of the Commodore 64. Today, he has excelled in programming, and designs. Richard is in his mid 30's and, if or when you looking for him look no further than his computer. Always willing to help and give advice where he can in regard to computer related subjects. At present he is working as a .NET 2008 Software Developer for Syncrony Web Services, South Africa.

Downloads

Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Java developers know that testing code changes can be a huge pain, and waiting for an application to redeploy after a code fix can take an eternity. Wouldn't it be great if you could see your code changes immediately, fine-tune, debug, explore and deploy code without waiting for ages? In this white paper, find out how that's possible with a Java plugin that drastically changes the way you develop, test and run Java applications. Discover the advantages of this plugin, and the changes you can expect to see …

  • Live Event Date: September 17, 2014 @ 1:00 p.m. ET / 10:00 a.m. PT Another day, another end-of-support deadline. You've heard enough about the hazards of not migrating to Windows Server 2008 or 2012. What you may not know is that there's plenty in it for you and your business, like increased automation and performance, time-saving technical features, and a lower total cost of ownership. Check out this upcoming eSeminar and join Rich Holmes, Pomeroy's practice director of virtualization, as he discusses the …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds