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

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

0.00/5 (No votes)
11 Oct 2009 1  
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 G3DElements, which can be G3DPoints or G3DLines. You can add/remove a G3DPoint or a G3DLine to/from a model. You can retrieve all G3DPoints or G3DLines 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.

/// <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();

    AddLines(visual);
    AddPoints(visual, ratio);

    return visual;
}

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

/// <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
            };

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

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

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

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