Introduction
When reading the book “3D Programming for Windows” written by Charles Petzold, I thought I should develop something using WPF 3D. I hadn't got an idea until I was asked to help resolve a 3D geometry problem: if the student can easily visualize the location of the points and lines of the geometry model, he/she would be able to resolve the problem easily.
So I decided to develop a tool that allows the user to:
- Create a 3D geometry model by defining points and lines
- Examine the model by rotating it around the 3 axes
- Modify the model, and
- Persist the model for later use
Background
The tool was developed using the Petzold.Media3D
library developed by Charles Petzold. With this library, you can easily create meshes, lines, curves, axes, and text in 3D space. The following diagram shows the classes used in this tool.
The WireBase
class inherits from ModelVisual3D
, providing the basic functionality for drawing a line or lines in 3D space. It registers some dependency properties, e.g. Color
, Thickness
, etc.
The Axes
class, derived from WireBase
, draws X, Y, and Z axes in 3D space. You can specify the extent of the three axes with the Extent
property, which applies to all the three axes. The original class draws a big tick per unit (which can be considered as an inch) and 10 small ticks per big tick. If the Extent
is big, e.g. 80 inches, there will be too many ticks in the axes, not only making the axes too crowded with ticks and unit labels, but also affecting the performance. Therefore, I added two new properties to the class, UnitPerBigTick
and SmallTicksPerBigPick
, with which you can specify the number of inches per big tick, the number of small ticks per big tick.
The WireLine
class, also derived from WireBase
, is the workhorse of the tool, which draws a line in 3D space. You can create a line by specifying the two end points.
Another WireBase
-derived class, WireText
, is used to draw the labels of the points in a 3D geometry model.
Figure 1. The classes of Petzold.Media3D used in the tool
The Design
The design of the tool can be divided into 4 parts: the model, the view, the dialogs, and the persistence mechanism.
I am going to introduce the design of the program in two parts: part 1 about the model, the view, and the dialogs, and part 2 about the persistence mechanism.
The Model
The G3DModel
class represents a 3D geometry model that comprises G3DElement
s, which can be G3DPoint
s or G3DLine
s. You can add/remove a G3DPoint
or a G3DLine
to/from a model. You can retrieve all G3DPoint
s or G3DLine
s through the Points
or Lines
properties respectively. The Extent
property returns the extent of the model in the X, Y, or Z axis, whichever is the biggest. The IsDirty
property indicates whether the model has been changed since the last save. The IsEmpty
property returns true
if there isn't any point or line in the model.
The CreateVisual3D()
method of the G3DModel
class returns a ModelVisual3D
that represents the 3D geometry model, which can then be added to a Viewport3D
to get it displayed.
The G3DElement
class is the base class of G3DPoint
and G3DLine
, each representing a point or a line respectively. Each G3DElement
object has a unique identity, represented by the ID
property. A G3DElement
object can have a Label
, which will be drawn together with the point or line.
The G3DPoint
class defines a point in 3D space. The Position
property of type Point3D
specifies the location of the point.
The G3DLine
class defines a line, with two G3DPoint
objects as the end points. A G3DLine
can have a color, with the default value of Black
. All end points (G3DPoint
objects) of the lines must be added to the model by calling one of the AddPoint()
methods of G3DModel
before they can be used as an end point of a line.
In future, we might want to add more types of G3DElement
-derived classes, e.g. G3DVector
that represents a vector in 3D space.
Figure 2. G3DModel
The following code shows how the CreateVisual3D
method of the G3DModel
class is implemented.
public ModelVisual3D CreateVisual3D(double ratio)
{
ModelVisual3D visual = new ModelVisual3D();
AddLines(visual);
AddPoints(visual, ratio);
return visual;
}
private void AddLines(ModelVisual3D visual)
{
foreach (var line in m_linesByName.Values)
{
var wl = new WireLine()
{
Point1 = line.StartPoint.Position,
Point2 = line.EndPoint.Position,
Color = line.Color,
Thickness = LINE_THICKNESS
};
visual.Children.Add(wl);
}
}
private void AddPoints(ModelVisual3D visual, double ratio)
{
foreach (var p in m_pointsByName.Values)
{
if (!IsPointUsedInLines(p))
{
var point = 1.0/96.0;
var wl = new WireLine()
{
Point1 = p.Position,
Point2 = p.Position + new Vector3D(point, point, point),
Thickness = 2
};
visual.Children.Add(wl);
}
if (!string.IsNullOrEmpty(p.Label))
{
var wt = new WireText()
{
Origin = p.Position,
Text = p.Label,
FontSize = POINT_LABEL_SIZE * ratio,
Thickness = 2
};
visual.Children.Add(wt);
}
}
}
private bool IsPointUsedInLines(G3DPoint p)
{
foreach (var l in m_lines)
{
if (l.StartPoint.ID == p.ID || l.EndPoint.ID == p.ID)
{
return true;
}
}
return false;
}
The View
The View consists of two windows: the MainWindow
and the ControlPanel
. The MainWindow
displays the G3DModel
, and the ControlPanel
is used to rotate the model and zoom in/out.
The MainWindow
contains a G3DViewport
, which, in turn, contains a ViewPort3D
. The ViewPort3D
has an AmbientLight
, a DirectionalLight
, and a PerspectiveCamera
. Three RotateTransform3D
objects are assigned to the PerspectiveCamera
in order to rotate the camera around the three axes.
Figure 3. G3DViewport
The ControlPanel
has four Sliders. The three Axis Sliders are used to rotate the camera around the three axes respectively. The Distance Slider is used to zoom in and out by changing the distance of the camera from the origin of the Axes.
Figure 4. The ControlPanel
The positions of Sliders are bound to 4 properties of the Viewport3D
through data binding. The Axis Sliders are bound to the AngleProperty
of the corresponding AxisAngleRotation3D
objects assigned to the camera, ranging from -180 to 180. When we change the position of the one of the axis Slider, the camera will rotate around the corresponding axis, thus allowing us to see the model from different angles.
The Distance Slider is bound to the Distance
property of the G3DViewport
, which adjusts the Position
of the camera when the value is changed. The range of the Distance Slider is from the 8th to 8 times of the Extent
of the G3DViewport
.
To support data binding, we register a DependencyProperty
, named DistanceProperty
of type int
. When the value of the property is changed, the DistancePropertyChanged()
method gets called, which adjusts the Position of the camera, thus rendering the effect of zooming in or zooming out.
public static readonly DependencyProperty DistanceProperty =
DependencyProperty.Register("Distance",
typeof(int),
typeof(G3DViewport),
new PropertyMetadata(0, DistancePropertyChanged));
public int Distance
{
set { SetValue(DistanceProperty, value); }
get { return (int)GetValue(DistanceProperty); }
}
protected static void DistancePropertyChanged
(
DependencyObject obj,
DependencyPropertyChangedEventArgs args
)
{
if (obj != null)
{
(obj as G3DViewport).DistancePropertyChanged(args);
}
}
protected void DistancePropertyChanged(DependencyPropertyChangedEventArgs args)
{
var p = m_camera.Position;
var currentDistance = Math.Sqrt(p.X * p.X + p.Y * p.Y + p.Z * p.Z);
var ratio = Math.Sqrt(Distance / currentDistance);
m_camera.Position = new Point3D(p.X * ratio, p.Y * ratio, p.Z * ratio);
}
The two Bind()
methods of ControlPanel
bind a DependencyProperty
of a DependencyObject
to a Slider.
public void Bind(DependencyObject target, DependencyProperty property, SliderId id)
{
if (target == null || property == null)
{
throw new ArgumentNullException
("The target and property parameters should not be null");
}
switch (id)
{
case SliderId.AxisX:
Bind(target, property, sliderX);
break;
case SliderId.AxisY:
Bind(target, property, sliderY);
break;
case SliderId.AxisZ:
Bind(target, property, sliderZ);
break;
case SliderId.Distance:
Bind(target, property, sliderDistance);
break;
default:
throw new ArgumentException("Invalid SliderId");
}
}
private void Bind(DependencyObject target, DependencyProperty property, Slider slider)
{
Binding binding = new Binding();
binding.Source = slider; ;
binding.Path = new PropertyPath("Value");
binding.Mode = BindingMode.TwoWay;
BindingOperations.SetBinding(target, property, binding);
}
Note that the bindings are two-ways, so that the values of the properties and the positions of the slides will always be synchronized.
The MainWindow.Window_Loaded()
method binds the properties to the Sliders of the ControlPanel
by calling the Bind()
method of the ControlPanel
.
private void Window_Loaded(object sender, RoutedEventArgs e)
{
…
m_controlPanel.Bind(viewport.AxisX,
AxisAngleRotation3D.AngleProperty, ControlPanel.SliderId.AxisX);
m_controlPanel.Bind(viewport.AxisY,
AxisAngleRotation3D.AngleProperty, ControlPanel.SliderId.AxisY);
m_controlPanel.Bind(viewport.AxisZ,
AxisAngleRotation3D.AngleProperty, ControlPanel.SliderId.AxisZ);
m_controlPanel.Bind(viewport,
G3DViewport.DistanceProperty, ControlPanel.SliderId.Distance);
}
The Dialogs
There are a couple of dialogs that allow you to manipulate the model.
You can add one or more points with the AddPointDialog
by specifying the name, the coordinate, and/or the label of the point. When you click on the Add button, the point is added to the model. You can continuously add new points with the same dialog, and click on the Close button once you are done with adding points.
Figure 5. The Add Point Dialog
You can add lines to the model with the AddLineDialog
, which allows you to select the two end points of the line, to specify the name of the line, or to optionally choose a color of the line. The dialog lists all points of the model for you to choose the end points.
Figure 6. Add Line Dialog
You can create a new model or edit an existing model with the EditModelDialog
, with which you can add points and lines, edit the values.
Note that this dialog is not a WPF Window, but a Windows Forms Form, with two DataGridView
controls.
Figure 7. Edit Model Dialog
Unit Tests
The unit tests are done with the NUnit Framework. I have created unit tests for some of the model classes, such as G3DPoint
, G3DLine
, and G3DModel
. Please refer to the files, G3DPointTester.cs, G3DLineTester.cs, and G3DModelTester.cs in the UnitTests project. Although this section is very short, it doesn't mean unit test is not important. Instead, unit test is crucial to make sure the system works correctly and to give us the courage to refactor the code.
Conclusion
We've covered in Part 1 the design of the Model, the View, and the Dialogs. As you can see, it is pretty easy to use the Petzold.Media3D
library to draw 3D geometry models, and we have used only a small fraction of the library. We can add other kinds of visuals to the model, e.g. curves.
In part 2, I will introduce the persistence mechanism of the tool.
I've created a project on CodePlex. Please download the latest code from there.
History
- 11th October, 2009: Initial post