BLOG.CSHARPHELPER.COM

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 adds surface normals to that program. It uses the following code to create a MeshGeometry3D object holding segments showing surface normals for an existing MeshGeometry3D object.


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

    // Loop through the mesh's triangles.
    for (int triangle = 0; triangle < mesh.TriangleIndices.Count; triangle += 3)
    {
        // Get the triangle's vertices.
        Point3D point1 = mesh.Positions[mesh.TriangleIndices[triangle]];
        Point3D point2 = mesh.Positions[mesh.TriangleIndices[triangle + 1]];
        Point3D point3 = mesh.Positions[mesh.TriangleIndices[triangle + 2]];

        // Make the triangle's normal
        AddTriangleNormal(mesh, normals,
            point1, point2, point3, length, thickness);
    }

    return normals;
}

The method simply creates a MeshGeometry3D object and then loops through the original mesh's triangles calling the following AddTriangleNormal method for each.

// Add a segment representing the triangle's normal to the normals mesh.
private static void AddTriangleNormal(MeshGeometry3D mesh,
    MeshGeometry3D normals, Point3D point1, Point3D point2, Point3D point3,
    double length, double thickness)
{
    // Get the triangle's normal.
    Vector3D n = FindTriangleNormal(point1, point2, point3);

    // Set the length.
    n = ScaleVector(n, length);

    // Find the center of the triangle.
    Point3D endpoint1 = new Point3D(
        (point1.X + point2.X + point3.X) / 3.0,
        (point1.Y + point2.Y + point3.Y) / 3.0,
        (point1.Z + point2.Z + point3.Z) / 3.0);

    // Find the segment's other end point.
    Point3D endpoint2 = endpoint1 + n;

    // Create the segment.
    AddSegment(normals, endpoint1, endpoint2, thickness);
}

This method calls the FindTriangleNormal method described shortly to find a vector normal to the triangle's surface. It averages the triangle's corners to find its "center." (There are other definitions of a triangle's center, but this is easy and probably good enough.) It then adds the normal vector to the "center" to get the other end point for the normal segment. Finally it calls AddSegment to add a segment showing the normal. (See earlier posts for information about the AddSegment method.)

The following code shows the FindTriangleNormal method.

// Calculate a triangle's normal vector.
public static Vector3D FindTriangleNormal(Point3D point1, Point3D point2, Point3D point3)
{
    // Get two edge vectors.
    Vector3D v1 = point2 - point1;
    Vector3D v2 = point3 - point2;

    // Get the cross product.
    Vector3D n = Vector3D.CrossProduct(v1, v2);

    // Normalize.
    n.Normalize();

    return n;
}

This method creates vectors representing the triangle's first and second edges. It then uses Vector3D.CrossProduct to find a vector perpendicular to the two edge vectors. If the triangle is outwardly oriented, then the vector points in the outward direction.

Download the example for additional details.

   

Draw a 3D wireframe model for a MeshGeometry3D using WPF and XAML

This program 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 3D two interlocked tetrahedrons in a cage of "line segments" using WPF and XAML 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'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 CheckBoxes, 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 CheckBox, 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 the MainModel3Dgroup.Children and removes any that are GeometryModel3D objects. (The other objects in this example are lights.)

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

   

Draw improved 3D "line segments" using WPF and XAML

The example Draw 3D two interlocked tetrahedrons in a cage of "line segments" using WPF and XAML shows how to draw a skinny rectangular prism to represent line segments. If you make the prisms thick enough, you can see that they don't line up well at their corners. (See the picture on the left side.)

This example uses the following AddSegment method to extend prisms slightly so segments that share an end point overlap by the size of their thickness. That makes them line up nicely. (At least when their prism sides are parallel. See the picture on the right side.) The new code is shown in blue.


private void AddSegment(MeshGeometry3D mesh,
    Point3D point1, Point3D point2, Vector3D up,
    bool extend)
{
    const double thickness = 0.25;

    // Get the segment's vector.
    Vector3D v = point2 - point1;

    if (extend)
    {
        // Increase the segment's length on both ends by thickness / 2.
        Vector3D n = ScaleVector(v, thickness / 2.0);
        point1 -= n;
        point2 += n;
    }

    // Get the scaled up vector.
    Vector3D n1 = ScaleVector(up, thickness / 2.0);

    // Get another scaled perpendicular vector.
    Vector3D n2 = Vector3D.CrossProduct(v, n1);
    n2 = ScaleVector(n2, thickness / 2.0);

    // Make a skinny box.
    // p1pm means point1 PLUS n1 MINUS n2.
    Point3D p1pp = point1 + n1 + n2;
    Point3D p1mp = point1 - n1 + n2;
    Point3D p1pm = point1 + n1 - n2;
    Point3D p1mm = point1 - n1 - n2;
    Point3D p2pp = point2 + n1 + n2;
    Point3D p2mp = point2 - n1 + n2;
    Point3D p2pm = point2 + n1 - n2;
    Point3D p2mm = point2 - n1 - n2;

    // Sides.
    AddTriangle(mesh, p1pp, p1mp, p2mp);
    AddTriangle(mesh, p1pp, p2mp, p2pp);

    AddTriangle(mesh, p1pp, p2pp, p2pm);
    AddTriangle(mesh, p1pp, p2pm, p1pm);

    AddTriangle(mesh, p1pm, p2pm, p2mm);
    AddTriangle(mesh, p1pm, p2mm, p1mm);

    AddTriangle(mesh, p1mm, p2mm, p2mp);
    AddTriangle(mesh, p1mm, p2mp, p1mp);

    // Ends.
    AddTriangle(mesh, p1pp, p1pm, p1mm);
    AddTriangle(mesh, p1pp, p1mm, p1mp);

    AddTriangle(mesh, p2pp, p2mp, p2mm);
    AddTriangle(mesh, p2pp, p2mm, p2pm);
}

If the extend parameter is true, the program creates a vector in the direction of the line segment that has length equal to half of the prism's width. It then moves the segment's end points that distance away from each other to extend the segment the correct amount.

The rest of the method is similar to the code used in the previous example.

   

Draw 3D two interlocked tetrahedrons in a cage of "line segments" using WPF and XAML

A noticeable omission in WPF's 3D tools is any way to draw line segments. That means you can't draw wireframe models, show surface normals, or draw other line-like features. You can use the CodePlex toolkit 3D Tools for the Windows Presentation Foundation, but I usually prefer to implement my own solutions if possible.

The CodePlex toolkit does this by drawing skinny rectangles. That works, but if the viewing angle is along the edge of a rectangle, it disappears. Another approach would be to draw two skinny perpendicular rectangles that are interlocked. Then you can see something from any angle unless you look at them end-on.

Instead ot taking those approaches, I decided to represent line segments with skinny rectangular prisms (boxes). The following code shows how the AddSegment method adds a prism to represent a line segment connecting two points.

// Make a thin rectangular prism between the two points.
private void AddSegment(MeshGeometry3D mesh, Point3D point1, Point3D point2, Vector3D up)
{
    const double thickness = 0.01;

    // Get the segment's vector.
    Vector3D v = point2 - point1;

    // Get the scaled up vector.
    Vector3D n1 = ScaleVector(up, thickness / 2.0);

    // Get another scaled perpendicular vector.
    Vector3D n2 = Vector3D.CrossProduct(v, n1);
    n2 = ScaleVector(n2, thickness / 2.0);

    // Make a skinny box.
    // p1pm means point1 PLUS n1 MINUS n2.
    Point3D p1pp = point1 + n1 + n2;
    Point3D p1mp = point1 - n1 + n2;
    Point3D p1pm = point1 + n1 - n2;
    Point3D p1mm = point1 - n1 - n2;
    Point3D p2pp = point2 + n1 + n2;
    Point3D p2mp = point2 - n1 + n2;
    Point3D p2pm = point2 + n1 - n2;
    Point3D p2mm = point2 - n1 - n2;

    // Sides.
    AddTriangle(mesh, p1pp, p1mp, p2mp);
    AddTriangle(mesh, p1pp, p2mp, p2pp);

    AddTriangle(mesh, p1pp, p2pp, p2pm);
    AddTriangle(mesh, p1pp, p2pm, p1pm);

    AddTriangle(mesh, p1pm, p2pm, p2mm);
    AddTriangle(mesh, p1pm, p2mm, p1mm);

    AddTriangle(mesh, p1mm, p2mm, p2mp);
    AddTriangle(mesh, p1mm, p2mp, p1mp);

    // Ends.
    AddTriangle(mesh, p1pp, p1pm, p1mm);
    AddTriangle(mesh, p1pp, p1mm, p1mp);

    AddTriangle(mesh, p2pp, p2mp, p2mm);
    AddTriangle(mesh, p2pp, p2mm, p2pm);
}

The code first gets a Vector3D representing the vector between the start and end points. It then uses the ScaleVector method (which is straightforward) to create a vector n1 in the "up" direction that has length half of the prism's thickness. It uses the Vactor3D class's CrossProduct method to get a new vector n2 perpendicular to the other two. (If you don't know what a vector cross product is, see WikiPedia.) It then scales vector n2 so it also has length equal to half the prism's desired thickness.

Next the method adds combinations of the vectors n1 and n2 to the segment's end points to get the corners of the prism. It then uses those points to add the necessary triangles to build the prism to the MeshGeometry3D.

This approach uses 12 triangles to represent the segment so it is more work than the CodePlex toolkit's approach, which uses only 2 triangles, but it gives a slightly better result. Still, if you need to draw 100,000 segments, you might want to use CodePlex's approach to save drawing time.

   

Draw two interlocked tetrahedrons defined by a cube using WPF and XAML

My post Platonic Solids Part 2: The tetrahedron shows how to calculate the locations of a tetrahedron's vertices. The example Draw two interlocked tetrahedrons using WPF, XAML, and C# uses those vertices to draw two interlocked tetrahedrons.


The vertex calculations are interesting (if you like geometry), but there's an easier way to find the vertices of two interlocked tetrahedrons. You can use the corners of a cube to define the vertices of two interlocked tetrahedrons as shown in the picture on the right.

Much of this example is similar to Draw two interlocked tetrahedrons using WPF, XAML, and C# except it uses the following code to define its tetrahedrons' vertices and faces.

<!-- Tetrahedron 1 -->
...
<MeshGeometry3D 
Positions="
     1, 1, 1    -1,-1, 1     1,-1,-1
     1, 1, 1    -1, 1,-1    -1,-1, 1
     1, 1, 1     1,-1,-1    -1, 1,-1
    -1,-1, 1    -1, 1,-1     1,-1,-1
"
TriangleIndices="
     0  1  2     3  4  5
     6  7  8     9 10 11
"/>
...

<!-- Tetrahedron 2 -->
...
<MeshGeometry3D 
Positions="
    -1,-1,-1    -1, 1, 1     1, 1,-1
    -1,-1,-1     1,-1, 1    -1, 1, 1
    -1,-1,-1     1, 1,-1     1,-1, 1
     1,-1, 1     1, 1,-1    -1, 1, 1
"
TriangleIndices="
     0  1  2     3  4  5
     6  7  8     9 10 11
"/>
...

If you look carefully at the vertex coordinates, you can see where they lie on the unit cube. The program uses a ScaleTransform3D to scale the coordinates by a factor of 2.5 in the X, Y, and Z directions to make the tetrahedrons fill the viewing area nicely. Download the example for additional details.

   

Draw a 3D surface overlaid with a shaded altitude map using WPF and C#

The example Draw a 3D surface overlaid with a grid using WPF and C# explains how to overlay an image on a three-dimensional surface. This example does something similar. Instead of simply using an existing image containing a grid, however, it generates an image that is shaded to show the surface's height.

The following CreateAltitudeMap method generates the texture bitmap.

// Create the altitude map texture bitmap.
private void CreateAltitudeMap()
{
    // Calculate the function's value over the area.
    const int xwidth = 512;
    const int zwidth = 512;
    const double dx = (xmax - xmin) / xwidth;
    const double dz = (zmax - zmin) / zwidth;
    double[,] values = new double[xwidth, zwidth];
    for (int ix = 0; ix < xwidth; ix++)
    {
        double x = xmin + ix * dx;
        for (int iz = 0; iz < zwidth; iz++)
        {
            double z = zmin + iz * dz;
            values[ix, iz] = F(x, z);
        }
    }

    // Get the upper and lower bounds on the values.
    var get_values =
        from double value in values
        select value;
    double ymin = get_values.Min();
    double ymax = get_values.Max();

    // Make the BitmapPixelMaker.
    BitmapPixelMaker bm_maker = new BitmapPixelMaker(xwidth, zwidth);

    // Set the pixel colors.
    for (int ix = 0; ix < xwidth; ix++)
    {
        for (int iz = 0; iz < zwidth; iz++)
        {
            byte red, green, blue;
            MapRainbowColor(values[ix, iz], ymin, ymax,
                out red, out green, out blue);
            bm_maker.SetPixel(ix, iz, red, green, blue, 255);
        }
    }

    // Convert the BitmapPixelMaker into a WriteableBitmap.
    WriteableBitmap wbitmap = bm_maker.MakeBitmap(96, 96);

    // Save the bitmap into a file.
    wbitmap.Save("Texture.png");
}

The method starts by calculating the surface function's value over the area being drawn. It calculates a value for each pixel in the image it will create, in this case a 512 × 512 pixel image. The code then uses the LINQ Min and Max methods to get the largest and smallest values in the array.

The code then makes a BitmapPixelMaker object. (See this post.) It loops over the pixels in the image and uses the corresponding function value to determine a color for the pixel. The code uses the MapRainbowColor method (described shortly) to map each function value to an appropriate color.

The method finishes by calling the BitmapPixelMaker object's MakeBitmap method to create a WriteableBitmap, and then using the bitmap's Save extension method to save the result into a file. (See this post.)

The MapRainbowColor method uses the following code to map a value between given bounds to a color.

// Map a value to a rainbow color.
private void MapRainbowColor(double value, double min_value, double max_value,
    out byte red, out byte green, out byte blue)
{
    // Convert into a value between 0 and 1023.
    int int_value = (int)(1023 * (value - min_value) / (max_value - min_value));

    // Map different color bands.
    if (int_value < 256)
    {
        // Red to yellow. (255, 0, 0) to (255, 255, 0).
        red = 255;
        green = (byte)int_value;
        blue = 0;
    }
    else if (int_value < 512)
    {
        // Yellow to green. (255, 255, 0) to (0, 255, 0).
        int_value -= 256;
        red = (byte)(255 - int_value);
        green = 255;
        blue = 0;
    }
    else if (int_value < 768)
    {
        // Green to aqua. (0, 255, 0) to (0, 255, 255).
        int_value -= 512;
        red = 0;
        green = 255;
        blue = (byte)int_value;
    }
    else
    {
        // Aqua to blue. (0, 255, 255) to (0, 0, 255).
        int_value -= 768;
        red = 0;
        green = (byte)(255 - int_value);
        blue = 255;
    }
}

The code first scales the value so it ranges from 0 to 1023. Depending on whether the value is the range 0 - 255, 256 - 511, 512 - 767, and 768 - 1023, the code converts the color into different parts of a rainbow.

The rest of the program is similar to the previous one that maps a grid onto the surface. The following code shows how this example uses the texture image saved by the CreateAltitudeMap method to create the surface's material.

// Make the surface's material using an image brush.
ImageBrush texture_brush = new ImageBrush();
texture_brush.ImageSource =
    new BitmapImage(new Uri("Texture.png", UriKind.Relative));
DiffuseMaterial surface_material = new DiffuseMaterial(texture_brush);

Download the example to see additional details.

   

Draw a 3D surface overlaid with a grid using WPF and C#

The example Apply textures to triangles using WPF and C# shows how to apply textures to triangles. This example simply uses that technique to apply a bitmap holding a large grid to a surface.

At the class level, the program uses the following code to define scale factors that map a point's X and Z coordinates to the range 0.0 - 1.0.


private const double texture_xscale = (xmax - xmin);
private const double texture_zscale = (zmax - zmin);

The following code shows how the program adds a new point to the 3D model.

// A dictionary to hold points for fast lookup.
private Dictionary PointDictionary =
    new Dictionary();

// If the point already exists, return its index.
// Otherwise create the point and return its new index.
private int AddPoint(Point3DCollection points,
    PointCollection texture_coords, Point3D point)
{
    // If the point is in the point dictionary,
    // return its saved index.
    if (PointDictionary.ContainsKey(point))
        return PointDictionary[point];

    // We didn't find the point. Create it.
    points.Add(point);
    PointDictionary.Add(point, points.Count - 1);

    // Set the point's texture coordinates.
    texture_coords.Add(
        new Point(
            (point.X - xmin) * texture_xscale,
            (point.Z - zmin) * texture_zscale));

    // Return the new point's index.
    return points.Count - 1;
}

As in earlier examples, the code defines a dictionary to hold Points so it can look them up quickly. The AddPoint method looks up a point and adds it if it doesn't already exists. It then uses the point's X and Z coordinates to map the point to the 0.0 - 1.0 range of the U-V coordinates used by the object's texture. In other words, points with the smallest X/Z coordinates are mapped to U/V coordinates near (0, 0) and points with the largest X/Z coordinates are mapped to U/V coordinates near (1, 1).

After it creates the triangles, the program uses the following code to create its material.

// Make the surface's material using an image brush.
ImageBrush grid_brush = new ImageBrush();
grid_brush.ImageSource =
    new BitmapImage(new Uri("Grid.png", UriKind.Relative));
DiffuseMaterial grid_material = new DiffuseMaterial(grid_brush);

The file Grid.png simply contains a 513 × 513 pixel grid. Alternatively you could create the grid in code. A third approach would be to use partial derivatives to figure out where the lines should be drawn and then use skinny rectangles or boxes to draw them on top of the surface. (A later post will explain how to draw skinny boxes.) That would be a LOT more work, however.

I know these examples omit a huge amount of detail. They build on each other so you've seen the key points in earlier posts. The details are also fairly long so, to save space, I'm not going to include them in every post. Download the example to see how the whole thing works.

   

Apply textures to triangles using WPF and C#

My 3D WPF examples so far have worked with triangles that are shaded with solid colors. To make really compelling 3D scenes, you need to be able to color triangles with images. In this context, those images are called textures.

Here are the basic steps for texturing a triangle.

  1. Create a MeshGeometry3D object.
  2. Define the triangle's points and normals as usual.
  3. Set the triangle's texture coordinates by adding values to the mesh's TextureCoordinates collection.
  4. Create an ImageBrush that uses the texture that you want to display.
  5. Use the brush to create a material and apply it to the mesh as usual.

This list of steps glosses over a few details that are described in the following sections.


Texture Coordinates

Texture coordinates are measured starting at the upper left corner of the texture image and increasing to the right and downward. The coordinate of the upper left corner is (0, 0), and the coordinate of the lower right corner is (1, 1) as shown in the picture on the right. The coordinates are usually referred to as U and V coordinates instead of X and Y coordinates.

Due to a weird "feature" of the way WPF colors triangles, it seems to scale the texture coordinates that are actually used so they use the entire width and height of the texture. If your triangles use texture coordinates that cover the entire range 0 to 1, then the texture isn't scaled (or it's scaled by a factor of 1) so everything is as you would expect.

But suppose you have a single triangle in a mesh and its texture coordinates only span the range 0 <= U <= 0.5 and 0 <= V <= 0.5. In that case, WPF "helpfully" scales the coordinates so they use the texture's entire surface. Instead of using coordinates in the upper left part of the texture, the triangle uses the texture's whole surface. (I don't really know why it's doing this. I've messed with the ImageBrush's viewport parameters and haven't been able to find a reasonable solution. If you find one, please let me know.)

One workaround is to make sure the triangles in the mesh cover the ranges 0 <= U <= 0.5 and 0 <= V <= 0.5. Another solution is to resize the texture image so it only includes the parts of the texture that you actually want to use.


If you look at the picture at the top of this post, the triangle on the left uses the full extent of the texture shown on the right. The second triangle uses only the lower left quarter of the texture. WPF scales it so it uses the full texture.

The remaining two triangles each use a quarter of the texture. The bottom triangle uses the lower left quarter and the upper triangle uses the upper right quarter of the texture. You can see that the texture has not been scaled for those two triangles so they look as they should. (The second triangle from the left would look like the lower right triangle if the texture wasn't scaled for it.)


Using ImageBrushes

Creating an ImageBrush should be trivial, but in WPF it's not. For the approach used by this example, start by adding the image file you want to use to the project. To do that, open the Project menu, select Add Existing Object, select the file, and click Add.

Next select the image file in Solution Explorer. Set its "Build Action" property to Content and set its "Copy to Output Directory" to "Copy if newer." Now you can use code similar to the following to create the brush.

ImageBrush colors_brush = new ImageBrush();
colors_brush.ImageSource =
    new BitmapImage(new Uri("Colors.png", UriKind.Relative));

After you create the brush, you can use it to make a material and apply it to the mesh as usual.


The Program

The program uses four methods to create its triangles. The following code shows the second method, which creates the second triangle from the left.

// Make a triangle that uses the lower left quarter of the texture.
private void MakeMesh2(Model3DGroup model_group)
{
    // Make a mesh to hold the surface.
    MeshGeometry3D mesh = new MeshGeometry3D();

    // Set the triangle's points.
    mesh.Positions.Add(new Point3D(-1, 1, 0));
    mesh.Positions.Add(new Point3D(-1, 0, 0));
    mesh.Positions.Add(new Point3D(0, 0, 0));

    // Set the points' texture coordinates.
    mesh.TextureCoordinates.Add(new Point(0, 0.5));
    mesh.TextureCoordinates.Add(new Point(0, 1));
    mesh.TextureCoordinates.Add(new Point(0.5, 1));

    // Create the triangle.
    mesh.TriangleIndices.Add(0);
    mesh.TriangleIndices.Add(1);
    mesh.TriangleIndices.Add(2);

    // Make the surface's material using an image brush.
    ImageBrush colors_brush = new ImageBrush();
    colors_brush.ImageSource =
        new BitmapImage(new Uri("Colors.png", UriKind.Relative));
    DiffuseMaterial colors_material = new DiffuseMaterial(colors_brush);

    // Make the mesh's model.
    GeometryModel3D surface_model = new GeometryModel3D(mesh, colors_material);

    // Make the surface visible from both sides.
    surface_model.BackMaterial = colors_material;

    // Add the model to the model groups.
    model_group.Children.Add(surface_model);
}

If you look at the way the code sets the TextureCoordinates values, you'll see that this should make the triangle use only the lower left quarter of the texture.

The other two methods are fairly similar. Download the example to see how they work.

Now that you know how to apply textures to triangles, I'll post a few examples that use textures in more interesting ways.

   

Find a minimal bounding circle of a set of points in C#

The example Find the convex hull of a set of points in C# finds the convex hull of a set of points. A convex hull is the smallest polygon that encloses the points. This example extends that result to find a minimal circle enclosing the points.


The key is to note that a minimal bounding circle passes through two or three of the convex hull's points. The following picture shows the two possible scenarios. In the pictures, blue shows the convex hull, red shows a culling trapezoid, and orange shows a culling rectangle, all as described in the previous post.

The following code shows how this example finds minimal bounding circles.

// Find a minimal bounding circle.
public static void FindMinimalBoundingCircle(List points, out PointF center, out float radius)
{
    // Find the convex hull.
    List hull = MakeConvexHull(points);

    // The best solution so far.
    PointF best_center = points[0];
    float best_radius2 = float.MaxValue;

    // Look at pairs of hull points.
    for (int i = 0; i < hull.Count - 1; i++)
    {
        for (int j = i + 1; j < hull.Count; j++)
        {
            // Find the circle through these two points.
            PointF test_center = new PointF(
                (hull[i].X + hull[j].X) / 2f,
                (hull[i].Y + hull[j].Y) / 2f);
            float dx = test_center.X - hull[i].X;
            float dy = test_center.Y - hull[i].Y;
            float test_radius2 = dx * dx + dy * dy;

            // See if this circle would be an improvement.
            if (test_radius2 < best_radius2)
            {
                // See if this circle encloses all of the points.
                if (CircleEnclosesPoints(test_center, test_radius2, points, i, j, -1))
                {
                    // Save this solution.
                    best_center = test_center;
                    best_radius2 = test_radius2;
                }
            }
        } // for i
    } // for j

    // Look at triples of hull points.
    for (int i = 0; i < hull.Count - 2; i++)
    {
        for (int j = i + 1; j < hull.Count - 1; j++)
        {
            for (int k = j + 1; k < hull.Count; k++)
            {
                // Find the circle through these three points.
                PointF test_center;
                float test_radius2;
                FindCircle(hull[i], hull[j], hull[k], out test_center, out test_radius2);

                // See if this circle would be an improvement.
                if (test_radius2 < best_radius2)
                {
                    // See if this circle encloses all of the points.
                    if (CircleEnclosesPoints(test_center, test_radius2, points, i, j, k))
                    {
                        // Save this solution.
                        best_center = test_center;
                        best_radius2 = test_radius2;
                    }
                }
            } // for k
        } // for i
    } // for j

    center = best_center;
    if (best_radius2 == float.MaxValue)
        radius = 0;
    else
        radius = (float)Math.Sqrt(best_radius2);
}

The code first uses the technique described in the previous post to find the convex hull. It then loops through every pair of points on the hull to see if they lie on a bounding circle. For each pair of points, the program tests the circle with center exactly halfway between the two points. If the circle's radius squared is smaller than the best value found so far, the program calls the CircleEnclosesPoints method (described shortly) to see if the circle encloses all of the points. If the circle does enclose the points, the program updates its best circle center and radius.

After checking all pair of points, the program loops through all triples of points. For each triple, the program uses the technique described in the post Draw a circle through three points in C# to get a circle passing through the three points. It compares the circle's radius squared to the best so far and calls CircleEnclosesPoints as before to see if it should update the best circle.

When it finishes checking all of the triples of points, the code compares best_radius2 to float.MaxValue to see if it found a circle. If the values are the same, that means the points array holds a single point. In that case, the program sets the radius to 0 so it returns a circle centered at the single point with radius 0.

If best_radius2 doesn't equal float.MaxValue, the program sets the return radius result and ends.

The following code shows the CircleEnclosesPoints method.

// Return true if the indicated circle encloses all of the points.
private static bool CircleEnclosesPoints(PointF center,
    float radius2, List points, int skip1, int skip2, int skip3)
{
    for (int i = 0; i < points.Count; i++)
    {
        if ((i != skip1) && (i != skip2) && (i != skip3))
        {
            PointF point = points[i];
            float dx = center.X - point.X;
            float dy = center.Y - point.Y;
            float test_radius2 = dx * dx + dy * dy;
            if (test_radius2 > radius2) return false;
        }
    }
    return true;
}

This method takes as parameters a circle's center and radius squared, the list of points to examine, and three points that lie on the circle. It loops through the list of points, skipping the three on the circle, and determines whether they are all inside the circle. The code skips the three on the circle so rounding errors don't incorrectly make it seem like those points are not within the circle.

Because this method examines all triples of the points in the convex hull, it has runtime O(H3) where H is the number of points in the convex hull. There are faster algorithms, but for most "typical" applications, the number of points in the convex isn't huge so this is fast enough. (It's also much simpler than faster algorithms.)

   

Write an extension method to make saving a WriteableBitmap into a file easy using WPF and C#

In my post Create a bitmap and save it into a file using WPF, XAML, and C# I lamented (okay, whined) about how cumbersome it is to save a WriteableBitmap into a file. Fortunately there's a way you can make it easier. Simply add an extension method to the WriteableBitmap class. The following code shows such a method.


public static class WriteableBitmapExtentions
{
    // Save the WriteableBitmap into a PNG file.
    public static void Save(this WriteableBitmap wbitmap, string filename)
    {
        // Save the bitmap into a file.
        using (FileStream stream = new FileStream(filename, FileMode.Create))
        {
            PngBitmapEncoder encoder = new PngBitmapEncoder();
            encoder.Frames.Add(BitmapFrame.Create(wbitmap));
            encoder.Save(stream);
        }
    }
}

This "this WriteableBitmap wbitmap" part of the method declaration means this method extends the WriteableBitmap class. The parameter wbitmap represents the WriteableBitmap object for which you called the method. The second parameter, filename, is the only one you actually pass into the method.

The method creates a FileStream to hold the saved PNG file. It creates a PngBitmapEncoder to write the file's bitmap data. It then calls BitmapFrame.Create to create a new bitmap frame for the WriteableBitmap, and it adds the result to the encoder's Frames collection. The code finishes by saving the encoder's data into the FileStream.

The blue statement in the following code shows how the main program uses this method to save a WriteableBitmap into a PNG file.

// Convert the pixel data into a WriteableBitmap.
WriteableBitmap wbitmap = bm_maker.MakeBitmap(96, 96);

...

// Save the bitmap into a file.
wbitmap.Save("ColorSamples.png");

It would have been nice if Microsoft had included this functionality in the WriteableBitmap class, but at least it's easy to add this feature with an extension method.

   

Calendar

April 2014
SuMoTuWeThFrSa
12345
6789101112
13141516171819
20212223242526
27282930

Subscribe


Blog Software
Blog Software