The opening perfomance of the 10.000 Boxes
#1
In my project, mostly use with BoxVisual3D but in that project included 10.000 boxes and it cause slower the opening performance. How can i raise performance in way ?

P.S. In current settings, project open in 70 sec.

Regards.
#2
Creating many BoxVisual3D objects can be slow because for each of the object its own MeshGeometry3D is created.

It is much faster to create one box MeshGeometry3D at position (0, 0, 0) and size (1, 1, 1) and than transform that mesh with ScaleTransorm3D and TranslateTransform3D - or even better combine those two transformations into one MatrixTransform3D.

So you create one MeshGeometry3D and than create many GeometryModel3D objects with it - each GeometryModel3D can have its own material and has its Transform that scales and translates it.

It also helps to Freeze (call Freeze method) on each object that will not be changed.

The following code can be used to test the object creation in such manner (on my machine it takes less than half a second to create 10000 boxes):

XAML:
Code:
<UserControl x:Class="Ab3d.PowerToys.MyTests.Tests.Performance.BoxesTest"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:ab3d="clr-namespace:Ab3d.Controls;assembly=Ab3d.PowerToys"  
             xmlns:cameras="clr-namespace:Ab3d.Cameras;assembly=Ab3d.PowerToys"
             xmlns:visuals="clr-namespace:Ab3d.Visuals;assembly=Ab3d.PowerToys"            
             mc:Ignorable="d"
             d:DesignHeight="300" d:DesignWidth="300">
    <Grid>
        <Viewport3D Name="MainViewport">
            
        </Viewport3D>

        <cameras:TargetPositionCamera Name="Camera1"
                             Heading="30" Attitude="-20" Bank="0"
                             Distance="5000" ShowCameraLight="Always"/>

        <ab3d:MouseCameraController Name="MouseCameraController1" UsedMouseButton="Left" EventsSourceElement="{Binding ElementName=MainGrid}"/>

        <StackPanel HorizontalAlignment="Left" VerticalAlignment="Top" Orientation="Horizontal">
            <Label>Number of boxes:</Label>
            <TextBox Name="NumberTextBox" Width="50" Margin="0 0 10 0" Text="5000"/>
            <Label>Size:</Label>
            <TextBox Name="SizeTextBox" Width="50" Margin="0 0 10 0" Text="50"/>            
            <Button Name="CreateButton" Content="Create" Click="CreateButton_OnClick"/>
        </StackPanel>
    </Grid>
</UserControl>

cs:
Code:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Media.Media3D;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace Ab3d.PowerToys.MyTests.Tests.Performance
{
    /// <summary>
    /// Interaction logic for BoxesTest.xaml
    /// </summary>
    public partial class BoxesTest : UserControl
    {
        // Based on emial form Semih (semih@solon.com.tr) on 7. 3. 2013

        private MeshGeometry3D _boxMesh;

        private List<Vector3D> _randomPositions;

        private static Random _rnd;

        public BoxesTest()
        {
            InitializeComponent();

            _randomPositions = new List<Vector3D>(10000);

            if (_rnd == null)
                _rnd = new Random();

            for (int i = 0; i < 10000; i++)
            {
                _randomPositions.Add(new Vector3D(_rnd.NextDouble() * 1000 - 500,
                                                 _rnd.NextDouble() * 1000 - 500,
                                                 _rnd.NextDouble() * 1000 - 500));
            }

            _boxMesh = new Ab3d.Meshes.BoxMesh3D(new Point3D(0, 0, 0), new Size3D(1, 1, 1), 1, 1, 1).Geometry;
            _boxMesh.Freeze();
        }

        private void CreateButton_OnClick(object sender, RoutedEventArgs e)
        {
            int count = Int32.Parse(NumberTextBox.Text);
            double size = double.Parse(SizeTextBox.Text);

            var stopwatch = new Stopwatch();
            stopwatch.Start();

            var group = new Model3DGroup();

            var material = new DiffuseMaterial(Brushes.Green);
            material.Freeze();

            for (int i = 0; i < count; i++)
            {
                var geometryModel3D = new GeometryModel3D(_boxMesh, material);
                geometryModel3D.Transform = new MatrixTransform3D(new Matrix3D(size, 0, 0, 0,
                                                                               0, size, 0, 0,
                                                                               0, 0, size, 0,
                                                                               _randomPositions[i].X, _randomPositions[i].Y, _randomPositions[i].Z, 1));
                geometryModel3D.Freeze();

                group.Children.Add(geometryModel3D);
            }

            group.Freeze();

            var visual3D = new ModelVisual3D();
            visual3D.Content = group;

            MainViewport.Children.Add(visual3D);

            stopwatch.Stop();

            MessageBox.Show(string.Format("Time: {0:#,##0.00} ms", stopwatch.Elapsed.TotalMilliseconds));
        }
    }
}


Because the GeometryModel3D is frozen, this code can be optimized even more with creating the for loop multithreaded - in this case you would need to create Model3DGroup for each thread and than add GeometryModel3D to them.
Andrej Benedik
#3
Thank you. Proposed method, performance has increased a lot.
#4
Andrej:

I'm having a similar performance problem. However, I'm adding about 5000 MultiMaterialBoxes. I tried to adjust the sample code to a MultiMaterialBox but I can't get it to work. Would you pleae post a sample that uses a MultiMaterialBox? As you might expect, when creating each new MultiMaterialBox I need to be able to set the material for each face of the box.

Thanks in advance.
#5
MultiMaterialBox is more complicated 3D objects because it is actually a Model3DGroup with 6 difference GeometryModel3D - one for each material.

Below I am posting the code to create MultiMaterialBox (taken from Ab3d.Models.Model3DFactory).

You should change the code so that you will create 6 PlaneMesh3D objects to create MeshGeometryes - as with the previous sample on this post you need to create them with size = 1 and at (0, 0, 0). Than freeze the meshes.

After that you should create GeometryModel3D-s with different materials, group them into Model3DGroup and apply transformation to Model3DGroup to scale and position the group.

I would also advice you that you use only one instance of Material for the same material (this means that you should use as little Material instances as possible - so all Orange materials should be in one Material instace). You can also freeze the material (you should freeze as many things as possible).

This will surely increase performance a lot, but I am not sure if the performance for 5000 MultiMaterialBox will be really good.

Code:
/// <summary>
        /// Creates a 3D Box model where each side can have its own material.
        /// </summary>
        /// <remarks>
        /// <para>
        /// The <b>CreateMultiMaterialBox</b> creates a 3D box where each side of the box can have its own material.
        /// </para>
        /// <para>
        /// To create a 3D box where all sides have the same material use <see cref="CreateBox(Point3D, Size3D, int, int, int, Material)"/> method.
        /// </para>
        /// </remarks>        
        /// <param name="centerPosition">box center position</param>
        /// <param name="size">size of the box</param>
        /// <param name="xCellsCount">number of cells in x direction</param>
        /// <param name="yCellsCount">number of cells in y direction</param>
        /// <param name="zCellsCount">number of cells in z direction</param>
        /// <param name="topMaterial">top material</param>
        /// <param name="bottomMaterial">bottom material</param>
        /// <param name="leftMaterial">left material</param>
        /// <param name="rightMaterial">right material</param>
        /// <param name="frontMaterial">front material</param>
        /// <param name="backMaterial">back material</param>
        /// <param name="isBackMaterialSet">is true the BackMaterial for each side is set to the same value as Material.</param>
        /// <returns>3D Box model as Model3DGroup</returns>
        public static Model3DGroup CreateMultiMaterialBox(Point3D centerPosition, Size3D size,
                                                          int xCellsCount, int yCellsCount, int zCellsCount,
                                                          Material topMaterial, Material bottomMaterial,
                                                          Material leftMaterial, Material rightMaterial,
                                                          Material frontMaterial, Material backMaterial,
                                                          bool isBackMaterialSet)
        {
            Size3D halfSize;
            Model3DGroup boxGroup;
            PlaneMesh3D onePlaneMesh;
            GeometryModel3D oneGeometryModel3D;

            halfSize = new Size3D(size.X / 2, size.Y / 2, size.Z / 2);

            boxGroup = new Model3DGroup();

            // Top side
            onePlaneMesh = new PlaneMesh3D(new Point3D(centerPosition.X, centerPosition.Y + halfSize.Y, centerPosition.Z),
                                           new Vector3D(0, 1, 0), // normal: up
                                           new Vector3D(0, 0, -1), // heightDirection: into the screen
                                           new Size(size.X, size.Z),
                                           xCellsCount, zCellsCount);

            oneGeometryModel3D = new GeometryModel3D();
            oneGeometryModel3D.Geometry = onePlaneMesh.Geometry;
            oneGeometryModel3D.Material = topMaterial;

            if (isBackMaterialSet)
                oneGeometryModel3D.BackMaterial = topMaterial;

            boxGroup.Children.Add(oneGeometryModel3D);


            // Bottom side
            onePlaneMesh = new PlaneMesh3D(new Point3D(centerPosition.X, centerPosition.Y - halfSize.Y, centerPosition.Z),
                                           new Vector3D(0, -1, 0), // normal: down
                                           new Vector3D(0, 0, -1), // heightDirection: into the screen
                                           new Size(size.X, size.Z),
                                           xCellsCount, zCellsCount);

            oneGeometryModel3D = new GeometryModel3D();
            oneGeometryModel3D.Geometry = onePlaneMesh.Geometry;
            oneGeometryModel3D.Material = bottomMaterial;

            if (isBackMaterialSet)
                oneGeometryModel3D.BackMaterial = bottomMaterial;

            boxGroup.Children.Add(oneGeometryModel3D);


            // Left side
            onePlaneMesh = new PlaneMesh3D(new Point3D(centerPosition.X - halfSize.X, centerPosition.Y, centerPosition.Z),
                                           new Vector3D(-1, 0, 0), // normal: to the left
                                           new Vector3D(0, 1, 0), // heightDirection: up
                                           new Size(size.Z, size.Y),
                                           zCellsCount, yCellsCount);

            oneGeometryModel3D = new GeometryModel3D();
            oneGeometryModel3D.Geometry = onePlaneMesh.Geometry;
            oneGeometryModel3D.Material = leftMaterial;
            if (isBackMaterialSet)
                oneGeometryModel3D.BackMaterial = leftMaterial;

            boxGroup.Children.Add(oneGeometryModel3D);


            // Right side
            onePlaneMesh = new PlaneMesh3D(new Point3D(centerPosition.X + halfSize.X, centerPosition.Y, centerPosition.Z),
                                           new Vector3D(1, 0, 0), // normal: to the right
                                           new Vector3D(0, 1, 0), // heightDirection: up
                                           new Size(size.Z, size.Y),
                                           zCellsCount, yCellsCount);

            oneGeometryModel3D = new GeometryModel3D();
            oneGeometryModel3D.Geometry = onePlaneMesh.Geometry;
            oneGeometryModel3D.Material = rightMaterial;
            if (isBackMaterialSet)
                oneGeometryModel3D.BackMaterial = rightMaterial;

            boxGroup.Children.Add(oneGeometryModel3D);


            // Front side
            onePlaneMesh = new PlaneMesh3D(new Point3D(centerPosition.X, centerPosition.Y, centerPosition.Z + halfSize.Z),
                                           new Vector3D(0, 0, 1), // normal: away from screen
                                           new Vector3D(0, 1, 0), // heightDirection: up
                                           new Size(size.X, size.Y),
                                           xCellsCount, yCellsCount);

            oneGeometryModel3D = new GeometryModel3D();
            oneGeometryModel3D.Geometry = onePlaneMesh.Geometry;
            oneGeometryModel3D.Material = frontMaterial;
            if (isBackMaterialSet)
                oneGeometryModel3D.BackMaterial = frontMaterial;

            boxGroup.Children.Add(oneGeometryModel3D);


            // Back side
            onePlaneMesh = new PlaneMesh3D(new Point3D(centerPosition.X, centerPosition.Y, centerPosition.Z - halfSize.Z),
                                           new Vector3D(0, 0, -1), // normal: away from screen
                                           new Vector3D(0, 1, 0), // heightDirection: up
                                           new Size(size.X, size.Y),
                                           xCellsCount, yCellsCount);

            oneGeometryModel3D = new GeometryModel3D();
            oneGeometryModel3D.Geometry = onePlaneMesh.Geometry;
            oneGeometryModel3D.Material = backMaterial;
            if (isBackMaterialSet)
                oneGeometryModel3D.BackMaterial = backMaterial;

            boxGroup.Children.Add(oneGeometryModel3D);

            return boxGroup;
        }
Andrej Benedik
#6
deleted
#7
I finished migrating my code as suggested and had mixed results. My original code using MultiMaterialBoxes took about 7 seconds to load, using the PlaneMesh3D trick the load time came down to 4 seconds. I was obviously pretty happy with that. Unfortunately, an unexpected side effect is that rotating my 3D object is now extremely choppy.

When I was using the MultiMaterialBoxes I could rotate the entire 3D model smoothly without any problems. Now, when using the Planes, my model rotation is not smooth at all and "jumps" from one position to the next.

Why might this be? And by the way, I am using a pretty good PC. I have 8 GB of RAM, quad core processor, and an nVidia 650 card.
#8
That are interesting result.

The load time is expected - much better but still takes quite long.

But the long rotation (=render) time is a surprise. If using MultiMaterialBoxes was faster here, than this can mean that it is better to use many Visual3D objects than many GeometryModel3D objects. So I would advice you to create a ModelVisual3D for each Model3DGroup - you can also try to put the transformation matrix to ModelVisual3D instead of Model3DGroup.

This way you could also reuse the "boxes" with same colors - they would use the same Model3DGroup that cold be frozen and than each "box" would have different ModelVisual3D with its own transformation.

You can use multiple cores to create objects that are frozen - if you would froze the whole Model3DGroup, than all the Model3DGroup can be created in parallel for - after they are created you can create ModelVisual3D objects on the UI thread.

You could also create a more complicated solution where Model3DGroup-s would be created on background threads and filled into a queue (optimized for many concurrent writers and one reader) - the queue could be read on UI thread and ModelVisual3D objects could be created there. I guess that this would be faster than previous multithreaded version - but cannot say for sure.

You can also try to set ClipToBounds on Viewport3D to false and (it not needed) set IsHitTestVisible to false.


WPF 3D is almost always CPU bound - so having very good graphics card does not help a lot.
Andrej Benedik
#9
Hi,

Can we have a picture in the boxmesh?
#10
If you would like to show image with 3D box than you should use ImageBrush for DiffuseMaterial (note that you should add a transformation that will vertically flip the image):

<DiffuseMaterial >
<DiffuseMaterial.Brush>
<ImageBrush ImageSource="C:\mypng.png" Transform="1,0,0,-1,0,1"/>
</DiffuseMaterial.Brush>
</DiffuseMaterial>
Andrej Benedik
  


Forum Jump:


Users browsing this thread:
1 Guest(s)