Draw a 3D wireframe using WPF and C#

3D wireframe

This example makes it relatively easy to display a 3D wireframe model. It adds three features to previous examples. First, it creates a wireframe representing the triangles defined in a MeshGeometry3D. Second, it modifies earlier segment drawing methods so it can calculate its own “up” vector. Finally, it allows the user to show or hide different parts of the model.


Making the Wireframe

The example Draw improved 3D line segments using WPF and C# shows how to draw 3D line segments in WPF. This example uses that technique to draw a wireframe MeshGeometry3D corresponding to a MeshGeometry3D. The following ToWireFrame extension method extends the MeshGeometry3D class and returns a new MeshGeometry3D representing the original mesh’s wireframe model.

// Return a MeshGeometry3D representing this mesh's wireframe.
public static MeshGeometry3D ToWireframe(
    this MeshGeometry3D mesh, double thickness)
{
    // Make a dictionary in case triangles share segments
    // so we don't draw the same segment twice.
    Dictionary<int, int> already_drawn =
        new Dictionary<int, int>();

    // Make a mesh to hold the wireframe.
    MeshGeometry3D wireframe = new MeshGeometry3D();

    // Loop through the mesh's triangles.
    for (int triangle = 0;
        triangle < mesh.TriangleIndices.Count;
        triangle += 3)
    {
        // Get the triangle's corner indices.
        int index1 = mesh.TriangleIndices[triangle];
        int index2 = mesh.TriangleIndices[triangle + 1];
        int index3 = mesh.TriangleIndices[triangle + 2];

        // Make the triangle's three segments.
        AddTriangleSegment(mesh, wireframe, already_drawn,
            index1, index2, thickness);
        AddTriangleSegment(mesh, wireframe, already_drawn,
            index2, index3, thickness);
        AddTriangleSegment(mesh, wireframe, already_drawn,
            index3, index1, thickness);
    }

    return wireframe;
}

The basic idea is to create line segments to represent each triangle’s three edges. However, each edge could be shared by other triangles. To avoid drawing the same edge twice, the ToWireframe method uses a Dictionary<int, int> to hold IDs representing the edges. The AddTriangleSegment method described shortly performs the actual check.

The ToWireframe method loops through the original mesh’s TriangleIndices collection looking at triples of indices. Each triple holds the indices of the points that make up a triangle. For each triangle, the method calls the following AddTriangleSegment method three times, passing it the indices of the points that make up the triangle’s edges.

// Add the triangle's three segments.
private static void AddTriangleSegment(MeshGeometry3D mesh,
    MeshGeometry3D wireframe, Dictionary<int, int> already_drawn,
    int index1, int index2, double thickness)
{
    // Get a unique ID for a segment connecting the two points.
    if (index1 > index2)
    {
        int temp = index1;
        index1 = index2;
        index2 = temp;
    }
    int segment_id = index1 * mesh.Positions.Count * index2;

    // If we've already added this segment for
    // another triangle, do nothing.
    if (already_drawn.ContainsKey(segment_id)) return;
    already_drawn.Add(segment_id, segment_id);

    // Create the segment.
    AddSegment(wireframe, mesh.Positions[index1],
        mesh.Positions[index2], thickness);
}

The AddTriangleSegment method first determines whether the edge has already been added to the wireframe model. To do that, it calculates the edge’s ID. If the original mesh contains NumPoints vertices and the edge’s points have indices index1 and index2 where index1 < index2, then the ID is index1 * NumPoints + index2. This scheme guarantees that each edge has a distinct ID in the mesh.

If the edge's ID is already in the dictionary, the segment has been added to the wireframe model already so the method exits.

If the segment has not yet been added to the model, the method calls the AddSegment method to create it. It then adds the edge's ID to the dictionary so it won't be added again.

To create the wireframe, the main program simply calls the surface mesh object's ToWireframe extension method, passing it the thickness the wireframe segments should have. It then creates the wireframe's material and GeometryModel3D, and adds the result to the main model group's Children collection as shown in the following code.

MeshGeometry3D wireframe = mesh.ToWireframe(0.03);
DiffuseMaterial wireframe_material =
    new DiffuseMaterial(Brushes.Red);
WireframeModel = new GeometryModel3D(wireframe, wireframe_material);
model_group.Children.Add(WireframeModel);


Calculating "Up" Vectors

An earlier post explained how to make an AddSegment method that creates a thin box that can represent a line segment in 3D WPF programs. That method required you to include an "up" vector. The AddSegment method made the sides of the thin box parallel to that vector and to the segment's vector.

That was convenient for the previous example where the segments were parallel to the X, Y, and Z axes and it was easy to pick perpendicular "up" vectors. Sometimes, however, you don't really care which direction is "up." For this program, I added the following overloaded version of AddSegment that doesn't require an "up" vector.

public static void AddSegment(MeshGeometry3D mesh,
    Point3D point1, Point3D point2, double thickness, bool extend)
{
    // Find an up vector that is not colinear with the segment.
    // Start with a vector parallel to the Y axis.
    Vector3D up = new Vector3D(0, 1, 0);

    // If the segment and up vector point in more or less the
    // same direction, use an up vector parallel to the X axis.
    Vector3D segment = point2 - point1;
    segment.Normalize();
    if (Math.Abs(Vector3D.DotProduct(up, segment)) > 0.9)
        up = new Vector3D(1, 0, 0);

    // Add the segment.
    AddSegment(mesh, point1, point2, up, thickness, extend);
}

The only requirement for the "up" vector is that it can't be parallel to the segment being drawn. This method starts with an "up" vector that is parallel to the Y axis. It then uses the Vector3D class's DotProduct method to calculate the dot product between the normalized segment and the "up" vector.

The dot product of two vectors equals the product of the lengths of the vectors and the cosine of the angle between them. In this example, the vectors have length 1, so the result is simply the cosine of the angle. If the cosine is greater than 0.9 (or less than -0.9), then angle between the segment and the "up" vector is small (or offset 180 degrees from a small value). That means the segment and "up" vector point in more or less the same direction (or more or less in opposite directions). In that case, the method uses an "up" vector parallel to the X axis so it doesn't point more or less where the segment does. That ensures that the two are not parallel, as desired.

After finding an acceptable "up" vector, the AddSegment method calls the earlier version of itself, passing it the "up" vector.


Showing and Hiding Models

When you click one of the program's check boxes, the program displays the surface, the wireframe, or both. In order to display the models when needed, the program uses the following code to declare variables to hold the surface and wireframe models at the class level.

// The surface's model.
private GeometryModel3D SurfaceModel;

// The wireframe's model.
private GeometryModel3D WireframeModel;

When you click a check box, the following event handler executes.

// Show and hide the appropriate GeometryModel3Ds.
private void chkContents_Click(object sender, RoutedEventArgs e)
{
    // Remove the GeometryModel3Ds.
    for (int i = MainModel3Dgroup.Children.Count - 1; i >= 0; i--)
    {
        if (MainModel3Dgroup.Children[i] is GeometryModel3D)
            MainModel3Dgroup.Children.RemoveAt(i);
    }

    // Add the selected GeometryModel3Ds.
    if ((SurfaceModel != null) && ((bool)chkSurface.IsChecked))
        MainModel3Dgroup.Children.Add(SurfaceModel);
    if ((WireframeModel != null) && ((bool)chkWireframe.IsChecked))
        MainModel3Dgroup.Children.Add(WireframeModel);
}

This event handler first loops through the objects in MainModel3Dgroup.Children and removes any that are GeometryModel3D objects. (The other objects in this example are lights.)

The event handler then re-adds the selected GeometryModel3D objects to the MainModel3Dgroup.Children collection so they are drawn.


Download Example   Follow me on Twitter   RSS feed   Donate




This entry was posted in algorithms, drawing, geometry, graphics, mathematics, wpf, XAML and tagged , , , , , , , , , , , , , , , , , , , , , , , , , . Bookmark the permalink.

14 Responses to Draw a 3D wireframe using WPF and C#

  1. Draw triangle surface normals on a 3D model using WPF and XAML

    The example Draw a 3D wireframe model for a MeshGeometry3D using WPF and XAML shows how to draw a surface and a wireframe. This example uses the following code to create a MeshGeometry3D object holding segments showing surface normals for an existing MeshGeometry3D object. add the ability to draw the surface’s triangle surface normals. // Return a MeshGeometry3D representing this mesh’s triangle normals. public static MeshGeometry3D ToTriangleNormals(this MeshGeometry3D mesh, double length, double thickness) { // Make a mesh to hold the normals. MeshGeometry3D normals …

  2. Chougrani says:

    Hello
    I’d like to make this code work in VB.net for some applications.
    I have some trouble with the
    which cannot be place in a “Class” in VB.net
    would you have some advices?

    Thanks for Sharing

    • RodStephens says:

      What specifically does it not allow you to do?

      • Chougrani says:

        Oh yeah I see that the main part of my last message did not print….(because of HTML tags I guess)
        it’s the ” System.Runtime.CompilerServices.Extension()_” that cannot apply to a class.
        Only to a module. so the ToWireframe() Function cannot be called as

        Dim wireframe As MeshGeometry3D = mesh.ToWireframe(0.03) (it returns an error on mesh.ToWireframe)

        • RodStephens says:

          Oh yeah. I think in Visual Basic you would put the method in a module and then mark it with the <Extension()> attribute. Then omit the this keyword from the parameter list.

          I think that should do it.

          • Chougrani says:

            Yea ! I already figured out a way to deal with it.
            it’s working. (it have bad behaviour for translating/rotating/zomming though, i’m working on it)
            thanks for your quick response !
            if I get better behaviour i’ll try to comment again with my code.

  3. Allen says:

    Many thanks. What a great work you have here. But suppose I want to draw a round bar or cylinder instead a the prisms or custom shapes like a 3d cross. How do I do this? I will appreciate your response.

  4. RodStephens says:

    You’ll need to draw them with triangles. For example, this post uses triangles to draw long skinny boxes to represent line segments.

    I’ll see if I an put together an example when I have a chance.

  5. Peter Nelson says:

    What you’ve described here is not a wireframe; it’s a triangle mesh. The distinction is important: in a mesh a rectangle is rendered as two triangles, so it has 5 visible line segments; in a wireframe a rectangle has 4 line segments. Many industrial and CAD applications require true wireframe rendering, which is a problem in WPF.

    • RodStephens says:

      Yes and no. This drawing was done with triangles not rectangles so it shows a wireframe for the triangles. If you look closely at the picture, you can see that the areas are not rectangles. Some that “should” be rectangles are bent in the middle like a potato chip so they need to be represented by triangles.

      But because you need to build the wireframe yourself, you could modify the program to draw a wireframe built from rectangles if you want. You would have to modify the way it was generated to only draw the segments you want to see.

      Alternatively if you just want the segments that are parallel to the axes, you could check that when you’re drawing them.

      However the program still doesn’t draw a “true” wireframe. It represents the wireframe segments as skinny prisms. If you zoom in far enough, you can see their thickness. A true wireframe system would draw the wireframe as a one-pixel wide line at any scale. Unfortunately WPF doesn’t do wireframes and this is the closest approach I’ve seen.

      • Henon says:

        in the 3DTools library there is code that draws wireframe with screen space uniform with lines (they do it with 3D-2D-3D projection). Check out the samples of 3DTools.

  6. joev says:

    How would I modify this so the image looks more like a cylinder. I don’t want to make a smooth cylinder I want a cylinder type image that’s ruff and adjustable

Leave a Reply

Your email address will not be published. Required fields are marked *