Study WPF 3D performance in C#

[WPF 3D]

The example Make a stellate geodesic sphere with WPF and C# makes a pretty complicated shape so I was wondering about the performance provided by WPF 3D programs. This example makes a group of stellate spheres using WPF 3D.

See the earlier post for information about how to make stellate spheres. Here I’m only going to talk about the more interesting changes and the results.

Unlike the previous examples, this one has few controls. It lets you use scroll bars to zoom in and out and to rotate the image. It doesn’t include a wireframe option or coordinate axes. Just a bunch of spheres.

The program uses the following code to create the spheres.

// Make some spheres.
const int max = 5;
const int xmin = -max;
const int xmax = max;
const int ymin = -max;
const int ymax = max;
const int zmin = -max;
const int zmax = max;
double dr = 255 / (xmax - xmin);
double dg = 255 / (ymax - ymin);
double db = 255 / (zmax - zmin);

for (int x = xmin; x <= xmax; x++)
{
    // Make sure the program doesn't think it's stuck.
    Application.Current.Dispatcher.Invoke(
        System.Windows.Threading.DispatcherPriority.Background,
        new Action(delegate { }));

    for (int y = ymin; y <= ymax; y++)
    {
        for (int z = zmin; z <= zmax; z++)
        {
            // Get the stellate sphere's triangles.
            Triangle[] triangles = MakeTriangles(1);

            // Create the WPF triangles for the stellate sphere.
            MeshGeometry3D solid_mesh = new MeshGeometry3D();
            foreach (Triangle triangle in triangles)
            {
                AddTriangle(solid_mesh, triangle);
            }

            // Make the sphere's material.
            byte r = (byte)((x - xmin) * dr);
            byte g = (byte)((y - ymin) * dg);
            byte b = (byte)((z - zmin) * db);
            Color color = Color.FromArgb(255, r, g, b);
            SolidColorBrush solid_brush =
                new SolidColorBrush(color);
            DiffuseMaterial solid_material =
                new DiffuseMaterial(solid_brush);
            GeometryModel3D solid_model =
                new GeometryModel3D(solid_mesh, solid_material);

            // Scale to make it smaller.
            const double scale = 0.4;
            ScaleTransform3D scale_transform =
                new ScaleTransform3D(
                    scale, scale, scale,
                    0, 0, 0);

            // Translate to center at (x, y, z).
            TranslateTransform3D translate_transform =
                new TranslateTransform3D(x, y, z);

            // Transform the model.
            Transform3DGroup transform_group =
                new Transform3DGroup();
            transform_group.Children.Add(scale_transform);
            transform_group.Children.Add(translate_transform);
            solid_model.Transform = transform_group;

            MainModelGroup.Children.Add(solid_model);
        }
    }
}

The code starts by defining some constants that it will use to position spheres. It then enters a series of nested loops to create the spheres. Each loop represents a coordinate (X, Y, or Z) that runs over a range. The result is a collection of spheres that lie on an integer grid in three dimensions.

For very large grids (like a 15×15×15 = 3,375 spheres), the program spent so long initializing that the CLR thought it was stuck and raised an error. Unfortunately WPF doesn’t have Application.DoEvents, which would satisfy the CLR that the program was getting something done.

As a workaround, I added the call to Dispatcher.Invoke. That allows the program to run and complete an empty action so the CLR is happy.

Next the code enters its three nested loops. For each X, Y, and Z value it creates a stellate sphere. It then calculates a color based on the sphere’s coordinates and uses it to make a material for the sphere.

The original method that creates a stellate sphere makes a fairly large sphere centered at the origin. To make it possible to see all of the spheres, this example scales the spheres so they don’t overlap and translates them to their proper locations.

Finally the code adds the spheres to the main model’s Children collection.

The program has a few other smaller changes such as using a Stopwatch to determine how long it takes the program to build its model.

The following table shows the time it took to build the model and the number of triangles used by the model for different arrangements of spheres.

Arrangement # Spheres # Triangles Time to Load
5×5×5 125 718,740 3 sec
6×6×6 216 1,186,380 5 sec
7×7×7 343 1,822,500 7 sec
8×8×8 512 2,653,020 12 sec
9×9×9 729 3,703,860 13 sec
10×10×10 1,000 5,000,940 17 sec
15×15×15 3,375 16,087,140 73 sec

If you grab one of the scroll bars and whip it back an forth, there is a barely noticeable lag with the 5×5×5 models and the lag grows larger with the larger models. The delay with the 15×15×15 model is quite annoying.

Performance with the 5×5×5 model is probably acceptable for animation and real-time games similar to Warcraft or Guild Wars. You might even be able to get away with a few more triangles depending on how quickly the scene needs to change. It seems likely that you could get away with a million or so triangles.

For larger models, the delay is larger and would be very annoying in a POV-style game. It would be acceptable, however, in a business presentation or some sort of visualization program where you needed to look at the model from different angles. You could probably get away with around 2 million triangles, perhaps more if you’re patient.

With even larger models, such as the 15×15×15 model, the lag is really annoying. You might still be able to do some visualization, but you won’t be able to manipulate the model smoothly.

Still I’m impressed that WPF 3D can display a model containing 16 million triangles at all. (When I was in grad school, the Media Lab had a 3D processor that could draw 3,000 triangles in real-time and we were very impressed. Times have definitely changed.) WPF 3D is not a particularly easy environment for building 3D applications, but it has some definite possibilities.


Download Example   Follow me on Twitter   RSS feed   Donate




About RodStephens

Rod Stephens is a software consultant and author who has written more than 30 books and 250 magazine articles covering C#, Visual Basic, Visual Basic for Applications, Delphi, and Java.
This entry was posted in animation, graphics, mathematics, performance, wpf and tagged , , , , , , , , , , , , . Bookmark the permalink.

9 Responses to Study WPF 3D performance in C#

  1. James Lear says:

    Very cool example, but the numbers appear to be out. When the program has ‘max’ set to 5, it generates 11*11*11 spheres rather than 5*5*5. The triangle count for 11*11*11 shows as 718740.
    When you set ‘max’ to 10, it generates a 21*21*21 set of spheres which shows a count of 5000940 triangles

    • RodStephens says:

      The max value indicates the largest integer values where it places a cube. Note that the min values are the negatives of the max values. In other words, xmin = -xmax. The values also include zero so the whole thing is centered at the origin. For example when max = 5, the coordinates include -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5 for a total of 11 values.

  2. Dragon says:

    Dear @RodStephens
    We have code as below:
    =========================================

    MeshGeometry3D mesh = new MeshGeometry3D();
    Int32Collection triangleIndices = new Int32Collection();
    Point3DCollection positions = new Point3DCollection();
    
    for (var z = 0; z < length; z++)
    {
        for (var x = 0; x < width; x++)
        {
            var point = new Point3D(x, map[z, x] * ImageConstants.YScale, z);
            positions.Add(point);
    
            if (z < length - 1 && x < width - 1)
            {
                ind1 = x + z * width;
                ind2 = ind1 + width;
    
                //first triangle
                triangleIndices.Add(ind1);
                triangleIndices.Add(ind2);
                triangleIndices.Add(ind2 + 1);
    
                //second triangle
                triangleIndices.Add(ind1);
                triangleIndices.Add(ind2 + 1);
                triangleIndices.Add(ind1 + 1);
            }
        }
    }

    ================================================
    With width = 949 & length = 825
    When measured with stopwatch, in c # it only takes about 0.6 seconds, but it takes about 6s it to display 3D images.
    And when using the mouse to rotate the 3D image it is very slow
    Is there any way to increase performance?
    Thank you.

    • RodStephens says:

      Well 949 * 825 = 782,925, which is a LOT of triangles. I suspect you may not be able to easily improve performance for that many triangles. I recall getting good performance for on the order of tens of thousands of triangles, but not as many as you have here.

      I would try to reduce the number of triangles. Is this a surface? You could reduce the number of divisions in the X and Z directions. If you use smoothing, it may make the surface look smoother without adding so many triangles.

      • Dragon says:

        Hi @RodStephens
        Thank for quickly reply.
        I built a 3D image, for example:
        https://www.goepel.com/fileadmin/images/aoi/IC-Pin-Kurzschluss.jpg
        The input data is the array of bytes, the height and width of the image
        How to reduce the number of divisions in the X and Z directions?
        I hope you can describe in more detail

        Thank you.

        • RodStephens says:

          For that image, you could merge a lot of small triangles into bigger ones. For example, the top of the chip could be made up mostly with two large triangles that make up a big rectangle. You would still need some smaller triangles to make the semi-circular indentation on the left.

          Automatically the program could look for adjacent sets of triangles where all of the points are coplanar and merge them using either of the two plans shown in the following picture.

          You can either look for large regions that are coplanar while you’re building the mesh or you can build the mesh and then merge triangles afterward to simplify the mesh. Or if you can you might be able to change your input data so you know where the flat places are when you’re building the mesh or the data.

          You also need to stitch the textures of the small triangles together to form a larger texture for the merged triangles.

          Displaying a complex texture on few triangles is much faster than using many triangles to represent the surface.

          You may also be able to merge some triangles even if they are not exactly coplanar. For your image, the traces and pads are slightly raised above the board but not by much. If you flatten them and show them in the texture, they should look normal to a casual viewer. You can even add shadows to the texture if you like. You’ll only notice the difference if you look at the model edge-on and even then you may not care. (The bigger 3D games do this. They might have a flat area textured with flowers or grass and some lighter and darker areas to make it seem like a rolling meadow. Or textured to look like bumpy rocks.)

          This whole process is called downsampling or mesh reduction. You can google those terms. Unfortunately it’s not completely trivial.

          I hope that helps or at least gives you a starting point.

Leave a Reply

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.