Introduction
The Windows Presentation Foundation (WPF) comes with an easy-to-use 3D framework which is essentially a wrapper around DirectX. Although the performance is not as high as it would be when using Direct3D directly, it's good enough for simple scenarios, and it's definitely fun to play around with it.
The WFTools3D library makes using WPF 3D even more simple and more fun.
I started this project years ago to save myself from writing the same lines of code over and over again when building up a 3D scene for different kinds of simulations. For example, when creating a simulation of the solar system, it shouldn't take more than a few lines to set up the whole scene including the sun and the planets. The positions and rotation states of objects in the scene should be accessible with simple properties of the objects and last but not least I wanted to be able to fly through my scenes in the same way as I can fly around the world with a flight simulator. I love flight simulators!
Background
Microsoft's 3D Graphics Overview to WPF and the advanced How-To topics have all the information that one needs to embed 3D graphics in a Windows application. All you need is a Viewport3D
, one or more ModelVisual3D
, a camera and some lights. So far, so good. But it takes a rather long time until the first triangle appears on the screen! The WFTools3D
library helps to shorten this time and allows for building up even complex scenes with e.g. moving cars and flying airplanes in some minutes. The scenes are interactive, i.e., you can move and rotate the built-in cameras with the keyboard and the mouse. Additionally, the cameras can move themselves, which means they can fly, and you can change their speed and yaw, pitch and roll angles just like in a flight simulator.
Using the Code
For a quick demo, we will build a simulation of the sun-earth-moon system. This is somewhat simpler than the demo project which you should also download, but perhaps later. For now, just create a WPF application called 'Demo' with Visual Studio 2010 or later. The .NET Framework version needs to be at least four. If the project is created, add a reference to WFTools3D.dll. In MainWindow.xaml replace the Grid
element by a WFTools3D.Scene3D
element like this:
<Window x:Class="Demo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:wft3d="WFTools3D"
Title="MainWindow" WindowState="Maximized"
FocusManager.FocusedElement="{Binding ElementName=scene}">
<wft3d:Scene3D x:Name="scene"/>
</Window>
Note that I maximized the window and set the logical focus to the scene. Now replace the code in MainWindow.xaml.cs with this:
using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Media3D;
using WFTools3D;
namespace Demo
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
scene.Models.Add(new AxisModel(10));
Sphere sun = new Sphere(32);
sun.DiffuseMaterial.Brush = Brushes.Goldenrod;
scene.Models.Add(sun);
scene.Camera.Position = new Point3D(25, -15, 8);
scene.Camera.LookAtOrigin();
}
}
}
Enough for now! Although our mini solar system is not finished yet, we can build and start the application. You will see a yellow sphere in the middle of a black screen with a red, green and blue line coming out of it. The lines are showing the x, y and z coordinate axes of our world. X is red, Y is green and Z is blue (or (r,g,b) for (x,y,z)). Before talking about the code, try this:
Press the left mouse button and move the mouse. It looks like the underlying world is being rotated. A left-right movement of the mouse apparently rotates the world around its z axis, whereas an up-down movement seems to rotate the world about a horizontal axis in the middle of the screen. In fact, what's really happening is that the camera is moved to a new position and directed towards the origin.
The arrow keys and PgUp, PgDn will rotate the camera, but not move it. Moving is done with Ctrl plus the previous keys.
To start flying, press 'W'. Every 'W' will increase the speed, 'S' will decrease it and 'X' will stop the movement. In flying mode (speed != 0
), left mouse dragging will change the flying direction. If you're lost in space, press the spacebar. This will turn the camera back to the origin.
But now let's talk about the code:
scene.Models.Add(new AxisModel(10));
Class Scene3D
has a Models
property which is used to populate the scene with 3D objects. The first object added is a model of the coordinate axes, which helps in the beginning when you build up a scene. The axes will have a length of 10 and you might ask "10 what? Yards, meters, pixels?" The answer is "It doesn't matter." It's just 10 units and I will build up the scene with objects whose size and position will fit into a cube of size 10. Any other number like 1 or 1000 or 42 will be fine as well. You just have to size and position your models (and cameras) according to this number.
Sphere sun = new Sphere(32);
sun.DiffuseMaterial.Brush = Brushes.Goldenrod;
scene.Models.Add(sun);
This adds a yellow sphere to the scene. Class Sphere
is a Primitive3D
which is an Object3D
which is a ModelVisual3D
which is the WPF base class for objects in a 3D scene. Class Object3D
makes it easy to scale, rotate and position the object. For sure, this can also be done with the underlying ModelVisual3D
class, but Object3D
has properties like Position
, ScaleX
or Rotation1
which modify the Transform
property of the base class more conveniently.
Class Primitive3D
adds a mesh and materials to the object. The mesh describes the surface of the object and is made up of many (sometimes many, many) triangles. The materials (there is a front and a back material) specify how the surface looks like. The Material
property (which represents the front material) in fact is a group of materials. You can access the individual items with properties DiffuseMaterial
, SpecularMaterial
and EmissiveMaterial
. If the object is closed, you don't have to take care about the BackMaterial
, otherwise you might want to code BackMaterial = Material
if the object looks the same from all sides.
The sphere is a closed object, so we just have to take care about the front material. We can give it any color by setting the Brush
of its DiffuseMaterial
. Instead of a SolidColorBrush
, we can also use an ImageBrush
, which for sure is more appealing and is done in the demo project. Each Primitive3D
has a constructor which takes an integer value called 'divisions
' which determines the number of triangles used to create the mesh. If you would go with '1
' instead of '32
', the sphere would look like a double-pyramid as the total number of triangles is only 8
.
scene.Camera.Position = new Point3D(25, -15, 8);
scene.Camera.LookAtOrigin();
This is the last step in our 6 lines setup of the scene: we have to set the camera to a certain position and make it look at a certain point (which right now is the origin of the coordinate system). Class Scene3D
has three perspective cameras and scene.Camera
refers to the active camera. To activate another camera, use scene.ActivateCamera(int index)
.
The scene is illuminated by a default lighting model which is accessible by property Scene3D.Lighting
. There are two directional lights called DirectionalLight1
and DirectionalLight2
and an ambient light called AmbientLight
. The default model works OK for most of my scenarios.
But Where is the Earth?
Indeed our sun needs at least one companion! So let's add an earth to the scene. The code is pretty much the same as it is for adding the sun. Put the following code right after the sun has been added to the scene:
Sphere earth = new Sphere(24) { Radius = 0.5, Position = new Point3D(9, 0, 0) };
earth.DiffuseMaterial.Brush = Brushes.Blue;
scene.Models.Add(earth);
The only real difference is its radius and position. The default position (0,0,0) and the default radius of 1 worked fine for the sun, but for the earth we have chosen different values. If you build and run the application, you'll now see the earth near the end of the red x axis.
And What About the Moon?
For the moon, we take a slightly different approach:
Sphere moon = new Sphere { Radius = 0.3, Position = new Point3D(2, 0, 0) };
moon.DiffuseMaterial.Brush = Brushes.NavajoWhite;
earth.Children.Add(moon);
The important thing here is that the moon is not added to the models of the scene, but to the children of the earth (which is also a models collection). The reason for this is the fact that we want earth and moon to stay together. They make up their own system. Whenever the earth is positioned to a new location, we want the moon to follow the earth. If we decide to remove the earth, its moon should also be gone. And this is exactly what the Children
property is meant for. Build and run the application and you'll see that now there is a moon at the end of the x axis.
Why does it appear at x = 10
? The earth is located at x = 9
, and the x
position of the moon is 2
. So where is the 10
coming from?
The reason is that since the moon is a child of the earth, its coordinate system is centered at the earth. So if we would put the moon at (0,0,0), it would be placed right in the middle of the earth (and we wouldn't see it anymore because it's smaller). To get the global position of the moon, we have to add its relative position (2,0,0) to its parent position (9,0,0). But that makes (11,0,0) - and the moon is definitely located at (10,0,0)! So how is that?
It's because we gave the earth a radius of 0.5! By scaling an Object3D
, and setting the radius of a sphere is the same as setting its ScaleX
, ScaleY
and ScaleZ
to the same value, we're not only scaling the geometry of the object but the whole world of this object. Even its coordinate system! So everything is shrunk to the half in the earth system. And that's why a distance of 2 in the earth system only means 1 in the earth's parent system (which is the global system right now). And that's why the moon is located at x = 10
.
And Yet It Moves!
According to Galileo Galilei, the earth moves around the sun and the moon moves around the earth. We have no reason to not believe this wise man, so let's add a few lines of code to show some kind of movement. It will not be the real movement, but anyway it looks funny. At the end of the window constructor, add these two lines:
scene.TimerTicked += TimerTicked;
scene.StartTimer();
Also, add a method for the TimerTicked
event:
void TimerTicked(object sender, EventArgs e)
{
angle += 2;
Object3D earth = scene.Models[2] as Object3D;
earth.Rotation1 = Math3D.RotationZ(angle);
earth.Rotation3 = Math3D.RotationZ(angle * 0.1);
}
double angle;
The above code starts a DispatcherTimer
inside of the scene which fires every 30 milliseconds. When this happens, our TimerTicked()
method gets called which just sets two Rotation
properties of the earth. Rotation1
and the not used Rotation2
are applied with respect to the center of the earth, whereas Rotation3
uses the origin of the parent coordinate system as rotation center. So the first rotation makes the earth move around itself while the second one makes the earth move around the sun (with a lower speed).
Since the moon is a child of the earth, it follows the earth whatever she's doing. So if the earth is spinning around itself, the moon will also spin around the center of the earth. And for sure the moon follows mother earth on her way around the sun!
Flying Through Space!
Remember the 'W', 'S' and 'X' keys? Pick up some speed with 'W' and try to follow the earth! If you're too fast, press 'S' to get slower or press 'X' to stop completely. You will find that it's really hard to fly a given course! By the way, press 'H' for two times. The first keystroke will visualize the cameras and the second one will show a special kind of attitude director indicator (ADI) which helps to not lose orientation while flying. The stick with the red ball is always directed to the origin.
The cameras are visualized as airplanes. Look around and you will find two of them. You will not find another airplane, because that belongs to the camera that you're currently looking with! The tip of an airplane is showing the looking direction of the camera while its vertical stabilizer shows the camera's up direction. You can change the active camera with keys '1', '2' and '3'.
To finish this demo, we will set up the cameras in a funny way: the first one is fixed as it was before, the second one orbits the sun and the third one follows the earth. The complete code for the MainWindow
now looks like this:
using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Media3D;
using WFTools3D;
namespace Demo
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
scene.Models.Add(new AxisModel(10));
Sphere sun = new Sphere(32);
sun.DiffuseMaterial.Brush = Brushes.Goldenrod;
scene.Models.Add(sun);
Sphere earth = new Sphere(24) { Radius = 0.5, Position = new Point3D(9, 0, 0) };
earth.DiffuseMaterial.Brush = Brushes.Blue;
scene.Models.Add(earth);
Sphere moon = new Sphere { Radius = 0.3, Position = new Point3D(2, 0, 0) };
moon.DiffuseMaterial.Brush = Brushes.NavajoWhite;
earth.Children.Add(moon);
scene.ActivateCamera(2);
scene.Camera.Position = new Point3D(9, 0, 0.1);
scene.Camera.LookDirection = Math3D.UnitY;
scene.Camera.UpDirection = Math3D.UnitZ;
scene.Camera.Rotate(Math3D.UnitZ, -30);
scene.Camera.ChangeRoll(-12);
scene.Camera.Speed = 8;
scene.ActivateCamera(1);
scene.Camera.Position = new Point3D(0, 4, 0);
scene.Camera.LookDirection = Math3D.UnitX;
scene.Camera.UpDirection = Math3D.UnitZ;
scene.Camera.ChangeRoll(25);
scene.Camera.Speed = 8;
scene.ActivateCamera(0);
scene.Camera.Position = new Point3D(25, -15, 8);
scene.Camera.LookAtOrigin();
scene.ToggleHelperModels();
scene.TimerTicked += TimerTicked;
scene.StartTimer();
}
void TimerTicked(object sender, EventArgs e)
{
angle += 2;
Object3D earth = scene.Models[2] as Object3D;
earth.Rotation1 = Math3D.RotationZ(angle);
earth.Rotation3 = Math3D.RotationZ(angle * 0.1);
}
double angle;
}
}
You will notice that the airplanes are visible without pressing the 'H' key. That's done by a call to scene.ToggleHelperModels()
. Press 'H' again to additionally show the ADI and again to remove both airplanes and the ADI. Here's a list of all keyboard/mouse commands:
- 1, 2, 3: Activate camera 1, 2 or 3
- W, S: Increase/decrease speed
- X: Set speed to 0
- T: Turn backwards
- Space: Turn to origin
- H: Toggle airplanes and ADI
- Mouse Wheel: Increase/decrease field of view
If camera speed is 0:
- LMB: Rotate scene about origin
- Ctrl+LMB: Rotate scene about touchpoint
- Arrows: Change look direction
- PgUp, PgDn: Change roll angle
- Ctrl+Arrows: Move camera left/right and forward/backward
- Ctrl+PgUp, PgDn: Move camera up, down
- Shift: Increase all above motion steps
If camera speed is not 0, i.e., in flying mode:
- LMB, Arrows: Change pitch and roll angles
- Ctrl+LMB: Change look direction
- A, D: Fly standard turn left/right
- F: Fly parallel to the ground
That's It!
I hope you enjoyed the tour. If you like to see more examples, check out my repositories on github. Project EquationOfTime
has a very realistic sun-earth simulation which explains a surprising fact about sunrise and sunset times and project DoublePendulum
inspects a double pendulum and its chaotic behaviour.
History
- 19-Mar-2016: Initial upload