15,741,752 members
Articles / Programming Languages / C#
Article
Posted 11 Oct 2009

73.1K views
72 bookmarked

# A Tool for Visualizing 3D Geometry Models (Part 1)

Rate me:
11 Oct 2009MIT7 min read
An article describing a tool developed using WPF for visualizing 3D geometry models

## 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.

C#
```/// <summary>
/// Creates and returns a ModelVisual3D that represents the model.
/// </summary>
/// <param name="ratio">The ratio to be used to display the labels.
/// </param>
/// <returns>The ModelVisual3D that represents the model</returns>
public ModelVisual3D CreateVisual3D(double ratio)
{
ModelVisual3D visual = new ModelVisual3D();

return visual;
}

/// <summary>
/// Add all lines of the model to the ModelVisual3D object.
/// </summary>
/// <param name="visual">The ModelVisual3D to which the lines are
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
};

}
}

/// <summary>
/// Add points and labels to the ModelVisual3D object.
/// </summary>
/// <param name="visual">The ModelVisual3D to which the points
/// and labels are added</param>
/// <param name="ratio">The ratio to scale the labels</param>
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
};

}

if (!string.IsNullOrEmpty(p.Label))
{
var wt = new WireText()
{
Origin = p.Position,
Text = p.Label,
FontSize = POINT_LABEL_SIZE * ratio,
Thickness = 2
};

}
}
}

/// <summary>
/// Check if the given point is used in any line.
/// </summary>
/// <param name="p">The point to be checked</param>
/// <returns>Returns true if the point is used in a line.</returns>
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.

C#
```public static readonly DependencyProperty DistanceProperty =
DependencyProperty.Register("Distance",
typeof(int),
typeof(G3DViewport),

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.

C#
```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`.

C#
```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

Written By
Technical Lead Rockwell Automation
Singapore
He is a Software Engineer at Rockwell Automation Asia Pacific Business Center, working on RSLogix 5000. Prior to joining Rockwell Automation, he had worked for Sybase for 8 years and was the original architect of the PowerBuilder Native Interface and the PowerBuilder .NET Compiler that can compile PowerBuilder applications to .NET Windows Forms or Web Forms applications. The programming languages he has used or is using intensively include C#, C++, C and 8086 assembly.

Wu XueSong's Blog

## Comments and Discussions

 First Prev Next
 Is possible Many box's in big Box prof300011-Feb-10 4:35 prof3000 11-Feb-10 4:35
 Rewrote the EditModelDialog using MVVM Wu Xuesong16-Jan-10 20:26 Wu Xuesong 16-Jan-10 20:26
 CoreMVVM Wu Xuesong23-Oct-09 18:09 Wu Xuesong 23-Oct-09 18:09
 excuse me: a simple question about UML... Southmountain21-Oct-09 12:15 Southmountain 21-Oct-09 12:15
 Re: excuse me: a simple question about UML... Wu Xuesong21-Oct-09 15:00 Wu Xuesong 21-Oct-09 15:00
 Great job man Sacha Barber19-Oct-09 21:48 Sacha Barber 19-Oct-09 21:48
 Excellent article Pete O'Hanlon12-Oct-09 4:08 Pete O'Hanlon 12-Oct-09 4:08
 Have yourself a 5. "WPF has many lovers. It's a veritable porn star!" - Josh SmithAs Braveheart once said, "You can take our freedom but you'll never take our Hobnobs!" - Martin Hughes.
 Last Visit: 31-Dec-99 18:00     Last Update: 22-Sep-23 16:23 Refresh 1

General    News    Suggestion    Question    Bug    Answer    Joke    Praise    Rant    Admin

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.