BLOG.CSHARPHELPER.COM

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.

   

Make a BitmapPixelMaker class to make working with pixels in a WriteableBitmap easier using WPF and C#

After building the example Set the pixels in a bitmap using WPF, XAML, and C#, I couldn't get it out of my head. Doing the math to set the correct bytes in a one-dimensional pixel array isn't all that hard, but it is annoying and I don't see why Microsoft had to make it so unfriendly. So I decided to make it a bit friendlier.

This example uses a BitmapPixelMaker class to represent a one-dimensional array that will be used to make a WriteableBitmap object. It only supports the Bgra32 format, but you could write similar classes to handle other formats (or maybe even modify the class to handle them all). Below I'll describe the class in pieces. The following code shows how the class starts.


// A class to represent WriteableBitmap pixels in Bgra32 format.
public class BitmapPixelMaker
{
    // The bitmap's size.
    private int Width, Height;

    // The pixel array.
    private byte[] Pixels;

    // The number of bytes per row.
    private int Stride;

    // Constructor. Width and height required.
    public BitmapPixelMaker(int width, int height)
    {
        // Save the width and height.
        Width = width;
        Height = height;

        // Create the pixel array.
        Pixels = new byte[width * height * 4];

        // Calculate the stride.
        Stride = width * 4;
    }

The class stores the bitmap's width and height in private fields. The Pixels array will hold the bitmap's pixel data. Stride is the number of bytes in a row of pixel data. For the Bgra32 format, it's just 4 bytes per pixel times the bitmap's width.

The class's constructor takes width and height as parameters. It allocates enough bytes for all of the pixels and calculates the stride for later use.

Most of the other methods are quite straightforward. They just do a little math to figure out where a byte needs to be in the Pixels array and then they get or set that byte. The following code shows how the class gets a pixel's red, green, and blue values.

    // Get a pixel's value.
    public void GetPixel(int x, int y, out byte red, out byte green, out byte blue, out byte alpha)
    {
        int index = y * Stride + x * 4;
        blue = Pixels[index++];
        green = Pixels[index++];
        red = Pixels[index++];
        alpha = Pixels[index];
    }

The code starts by calculating the index of the first byte for this pixel. The index includes y * Stride to skip bytes used by earlier rows in the pixel data. It adds x * 4 to skip the 4 bytes for each of the pixels to the left of the target pixel in its row.

Next the code simply copies the target pixel's byte data into its red, green, blue, and alpha return parameters. The only thing to note here is that the Bgra32 format stores a pixel's color components in the order blue, green, red, alpha.

If you only need to get one color component for a pixel, the GetPixel method is a bit heavy-handed, so the class also includes methods to get the red, green, blue, and alpha components separately. For example, the following code shows the GetRed method.

    public byte GetRed(int x, int y)
    {
        return Pixels[y * Stride + x * 4 + 2];
    }

The class defines corresponding methods to set a pixel's byte values. The SetPixel method is similar to GetPixel except it sets the byte values. The class also provides methods to set the red, green, blue, and alpha components separately. For example, the following code sets the green component.

    public void SetGreen(int x, int y, byte green)
    {
        Pixels[y * Stride + x * 4 + 1] = green;
    }

The following SetColor method sets every pixel's bytes to represent the same color

    // Set all pixels to a specific color.
    public void SetColor(byte red, byte green, byte blue, byte alpha)
    {
        int num_bytes = Width * Height * 4;
        int index = 0;
        while (index < num_bytes)
        {
            Pixels[index++] = blue;
            Pixels[index++] = green;
            Pixels[index++] = red;
            Pixels[index++] = alpha;
        }
    }

    // Set all pixels to a specific opaque color.
    public void SetColor(byte red, byte green, byte blue)
    {
        SetColor(red, green, blue, 255);
    }

The first version of the method simply loops through the pixel data and sets each bytes' color data. The second version calls the first to set the pixels to the same opaque color.

The following code shows the end of the class. The MakeBitmap method converts the pixel data into a WriteableBitmap object.

    // Use the pixel data to create a WriteableBitmap.
    public WriteableBitmap MakeBitmap(double dpiX, double dpiY)
    {
        // Create the WriteableBitmap.
        WriteableBitmap wbitmap = new WriteableBitmap(
            Width, Height, dpiX, dpiY,
            PixelFormats.Bgra32, null);

        // Load the pixel data.
        Int32Rect rect = new Int32Rect(0, 0, Width, Height);
        wbitmap.WritePixels(rect, Pixels, Stride, 0);

        // Return the bitmap.
        return wbitmap;
    }
}

This method creates a new WriteableBitmap object of the correct size and dots per inch vertically and horizontally. It uses the Bgra32 format.

Next it creates a Int32Rect to represent the part of the bitmap that should be written and uses the bitmap's WritePixels method to write the pixel data into the bitmap. Finally the method returns the result.

The following shows part of the code that the program uses to test the BitmapPixelMaker.

private void Window_Loaded(object sender, RoutedEventArgs e)
{
    const int width = 240;
    const int height = 240;

    // Make the BitmapPixelMaker.
    BitmapPixelMaker bm_maker = new BitmapPixelMaker(width, height);

    // Clear to black.
    bm_maker.SetColor(0, 0, 0);

    ... Use BitmapPixelMaker methods to set pixel values ...

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

    // Create an Image to display the bitmap.
    Image image = new Image();
    image.Stretch = Stretch.None;
    image.Margin = new Thickness(0);

    grdMain.Children.Add(image);

    // Set the Image source.
    image.Source = wbitmap;
}

The code creates a BitmapPixelMaker and calls its SetColor method to set all of the bitmap's pixels to black. It then uses BitmapPixelMaker methods to set pixel colors. Download the example to see how it works.

After the program has set the pixels' values, it calls the BitmapPixelMaker's MakeBitmap method to create the WriteableBitmap.

The program finishes by creating an Image and displaying the WriteableBitmap in it.

The process is still a bit cumbersome, but at least the BitmapPixelMaker class makes working with the pixel data a lot easier.

   

Create a bitmap and save it into a file using WPF, XAML, and C#

The example Set the pixels in a bitmap using WPF, XAML, and C# shows how to create a WriteableBitmap in WPF. This example shows how to save the bitmap into a file. (Like many things in WPF, it's not as easy as it used to be. In a Windows Forms application, you simply call the Bitmap's Save method.)

The following code shows how the program works. The code from the previous example that creates the WriteableBitmap has been omitted.


private void Window_Loaded(object sender, RoutedEventArgs e)
{
    // Make the WriteableBitmap.
    ...

    // Save the bitmap into a file.
    using (FileStream stream = new FileStream("ColorSamples.png", FileMode.Create))
    {
        PngBitmapEncoder encoder = new PngBitmapEncoder();
        encoder.Frames.Add(BitmapFrame.Create(wbitmap));
        encoder.Save(stream);
    }

    // Tell the user we're done.
    MessageBox.Show("Done");
}

After creating the bitmap, the program creates a FileStream object associated with the file that should hold the bitmap. It creates a PngBitmapEncoder to write the object. It then calls BitmapFrame.Create to create a new bitmap frame for the WriteableBitmap, and adds the result to the encoder's Frames collection. The code finishes by saving the encoder's data into the FileStream.

Not very simple or intuitive, but it shouldn't be hard to copy and paste this code when you need it.

   

Set the pixels in a bitmap using WPF, XAML, and C#

One of the things I like least about WPF is the way Microsoft threw away everything we had learned over years of working with Visual Studio. Yes, it's okay to start moving in a new direction, but there's no reason to ignore lessons learned through years of hard experience. Working with bitmapped images is one of the areas where WPF did just that. The .NET tools aren't great for manipulating the pixels in an image, but they're better than the one (note I don't say "ones") provided by WPF.

Anyway, to create a bitmapped image in WPF, you build a WriteableBitmap object. You then create a one-dimensional array holding the raw pixel information for the image, and you use the bitmap's WritePixels method to copy the pixel data into the image. The following code shows how this program does it.


private void Window_Loaded(object sender, RoutedEventArgs e)
{
    const int width = 240;
    const int height = 240;

    WriteableBitmap wbitmap = new WriteableBitmap(
        width, height, 96, 96, PixelFormats.Bgra32, null);
    byte[, ,] pixels = new byte[height, width, 4];

    // Clear to black.
    for (int row = 0; row < height; row++)
    {
        for (int col = 0; col < width; col++)
        {
            for (int i = 0; i < 3; i++)
                pixels[row, col, i] = 0;
            pixels[row, col, 3] = 255;
        }
    }

    // Blue.
    for (int row = 0; row < 80; row++)
    {
        for (int col = 0; col <= row; col++)
        {
            pixels[row, col, 0] = 255;
        }
    }

    // Green.
    for (int row = 80; row < 160; row++)
    {
        for (int col = 0; col < 80; col++)
        {
            pixels[row, col, 1] = 255;
        }
    }

    // Red.
    for (int row = 160; row < 240; row++)
    {
        for (int col = 0; col < 80; col++)
        {
            pixels[row, col, 2] = 255;
        }
    }

    // Copy the data into a one-dimensional array.
    byte[] pixels1d = new byte[height * width * 4];
    int index = 0;
    for (int row = 0; row < height; row++)
    {
        for (int col = 0; col < width; col++)
        {
            for (int i = 0; i < 4; i++)
                pixels1d[index++]= pixels[row, col, i];
        }
    }

    // Update writeable bitmap with the colorArray to the image.
    Int32Rect rect = new Int32Rect(0, 0, width, height);
    int stride = 4 * width;
    wbitmap.WritePixels(rect, pixels1d, stride, 0);

    // Create an Image to display the bitmap.
    Image image = new Image();
    image.Stretch = Stretch.None;
    image.Margin = new Thickness(0);

    grdMain.Children.Add(image);

    //Set the Image source.
    image.Source = wbitmap;
}

The code starts by creating the WriteableBitmap. It sets the bitmap's resolution to 96 pixels per inch and uses the Bgra32 format so the bitmap uses 32 bits per pixel to represent blue, green, red, and alpha (opacity) information. Note the order: blue, green, red, alpha. That's the order in which the data is stored for each pixel.

Next, to make working with the pixel data easier, the program makes a three-dimensional array of bytes. Each pixel's row and column contains 4 bytes in the array to hold its blue, green, red, and alpha components.

Next the code uses several loops to set the pixel byte data. It first sets each pixel's color components to 0 and its alpha components to 255 (opaque). It then uses three loops to create areas of blue, green, and red.

The bitmap's WritePixels method expects a one-dimensional array as a parameter, so the code copies the three-dimensional data into a one-dimensional array.

Next the program calls the bitmap's WritePixels method to copy the pixel data into the bitmap. The rectangle indicates the part of the bitmap to update. The stride parameter tells the WritePixels method how many bytes of data there are per row in the bitmap. This is simply the bitmap's width in pixels times the number of bytes per pixel. The final parameter is the offset in the array to the spot where the data to be copied begins.

At this point, the bitmap is ready to use. The program creates an Image control to display it, adds the Image to the program's main Grid control, and displays the bitmap in the Image's Source property.

The process is cumbersome and non-intuitive, but not too bad once you know the steps. It shouldn't be too hard to copy and paste this code to manipulate bitmaps in the future.

   

Create a 3-D surface really quickly with WPF, XAML, and C#

The example Draw a smooth 3-D surface with WPF, XAML, and C# draw a smooth surface but take about 16 seconds on my computer. The example Create a 3-D surface more quickly with WPF, XAML, and C# searches for duplicate points from the rear of its points collection so it's much faster, taking only about 2 seconds.

This example stores its points in a Dictionary (as well as in the collection that the WPF drawing code needs) so it can locate points even more quickly. This example uses the following AddPoints method to create new points for the surface.


// 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, 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);
    return points.Count - 1;
}

This code creates a Dictionary to hold the points' indices in the points collection. The AddPoint method first uses the Dictionary's ContainsKey method to see if the Point is already in the collection. If ContainsKey returns true, the code gets the point's index from the Dictionary and returns it. If this is a new Point, the method adds it to the points collection and returns its index.

The previous example builds its surface in under 2 seconds. This example builds its data model almost instantly.

The ultimate solution would be to do some (relatively) simple math to figure out where each triangle's index is stored. Then you could calculate each point's index instead of needing to look it up. For a scene where calculating indices would be hard, this example shows that a Dictionary can give you pretty good performance anyway.

   

Create a 3-D surface more quickly with WPF, XAML, and C#

IMHO I think the example Draw a smooth 3-D surface with WPF, XAML, and C# is pretty cool. Unfortunately it's also a bit slow. It take around 16 seconds to generate the three-dimensional scene. The program is generating 10,000 points and more than 19,000 triangles, so it's doing a lot of work, but the earlier non-smooth example is lightning fast. So the question is, why is the smooth version so much slower?

The answer lies in the way it creates and uses its points. The smooth version reuses points if possible. That means to add a new point, it first looks through all of the old ones to see if it is already present, and that takes a lot of time for such a large point array.


This example uses the following version of the AddPoint method.

// If the point already exists, return its index.
// Otherwise create the point and return its new index.
private int AddPoint(Point3DCollection points, Point3D point)
{
    // See if the point exists.
    for (int i = points.Count - 1; i >= 0; i--)
    // for (int i = 0; i < points.Count; i++)
    {
        if ((point.X == points[i].X) &&
            (point.Y == points[i].Y) &&
            (point.Z == points[i].Z))
            return i;
    }

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

This code is very similar to the previous (slow) version except it searches the array of points from back-to-front instead of from front to back. The triangles share points with other triangles that were added relatively recently. In fact, most of the triangles share two points with the triangle that was added immediately before. One of those points was added in a previous row of triangle so it's back a ways in the array, but the other point was just added so it should be very near the array's end.

By searching the array backwards, this version of the AddPoint method reduces the program's startup time from 16 seconds to about 2 seconds. A pretty nice speed up with very little work!

   

Calendar

April 2014
SuMoTuWeThFrSa
12345
6789101112
13141516171819
20212223242526
27282930

Subscribe


Blog Software
Blog Software