// 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.
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 DictionaryAs 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.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; }
// 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. |
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.
// 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. public static void FindMinimalBoundingCircle(ListThe 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.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); }
// Return true if the indicated circle encloses all of the points. private static bool CircleEnclosesPoints(PointF center, float radius2, ListThis 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.)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; }
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.
// 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.
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.
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.
// A dictionary to hold points for fast lookup. private DictionaryThis 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.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; }
// 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!