Search code examples
wpfmultithreadingbackgroundworkersharpdxhelix-3d-toolkit

How to parallelize 3d model creation?


Bit of a special case here, because I'm using a library called helix-toolkit but bear with me.

The thing is I would like to parallelize the creation of model objects in my code using a backgroundworker.

I know that there is a big issue with mutlithreading and working on UI elements but maybe somehow there is a workaround.

Here is the rough structure of my code:

First file in which I create the Backgroundwoker, splitting the workload for creating Geometry3D objects and finally calling SetModelGeometry to bind the geometries to the viewport. The second file shows how the binding is done.

MainWindow.xaml.cs

  private void Draw_Building()
        {
            _worker = new BackgroundWorker { WorkerReportsProgress = true, WorkerSupportsCancellation = true };
            _worker.DoWork += Draw_Building_DoWork;
            _worker.ProgressChanged += DrawBuilding_ProgressChanged;
            _worker.RunWorkerCompleted += DrawBuilding_RunWorkerCompleted;
            _worker.RunWorkerAsync(10000);

        }

        private void Draw_Building_DoWork(object sender, System.ComponentModel.DoWorkEventArgs e)
        {
            // returns a list containing Geometry3D objects ( created by meshBuilder.ToMeshGeometry3D() )
            Geometryhandler.Draw_Building(sender, e); 
        }

        private void DrawBuilding_ProgressChanged(object sender, ProgressChangedEventArgs e)
        {
            StatusProgressBar.Value = e.ProgressPercentage;
            var i = (int)e.UserState;
            var actualComponent = MyBuildingComponents.First(c => c.Id == i);
            LblStatusbarInfo.Text = "Currently meshing element #" + actualComponent.Globalid + " (" +
                                    actualComponent.Objectname + ")";
            StatusProgressbarMsg.Text = "Meshing (" + e.ProgressPercentage + " %)";
        }

        private void DrawBuilding_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            StatusProgressbarMsg.Text = "-";
            StatusProgressBar.Value = 0;
            LblStatusbarInfo.Text = "Meshing completed.";

            Geometry = e.Result as List<MeshIdandGeometry>;

            // creates MeshGeometryModel3D objects and binds them to the viewport using the List of Geometries
            MainViewModel.SetModelGeometry(Geometry);

        }

MainViewModel.cs

    public void SetModelGeometry(List<MeshIdandGeometry> geometry)
    {

        MyModelGeometry = new Element3DCollection();

        if (geometry != null)
        {
            foreach (var mygeometry in geometry)
            {

                var s = new MeshGeometryModel3D
                {
                    Geometry = mygeometry.Geometry,
                    Material = mygeometry.Material,
                };
                this.MyModelGeometry.Add(s);

                s.Attach(MyModelViewport.RenderHost);
            }

        }

        this.OnPropertyChanged("MyModelGeometry");
    }

My problem at the moment is the following error message:

The calling thread cannot access this object because a different thread owns it.

which is thrown in the SetModelGeometry function when trying to attach the ModelGeometry to the viewport.

I guess the compiler is complaining about the fact that the geometries were created in different threads, to which he has no access now.

Is there any workaround/solution without destroying the parallel execution of the DrawBuilding function?

EDIT:

EDIT 2: posted the wrong version of the Draw_Building method

The Draw_Building method in the Geometryhandler:

    public void Draw_Building(object sender, DoWorkEventArgs e)
    {

        var geometry = new List<MeshIdandGeometry>();

        var standardMaterial = new PhongMaterial()
        {
            AmbientColor = SharpDX.Color.LightGray,
            //DiffuseColor = new Color4(0.35f, 0.35f, 0.35f, 1.0f),
            //DiffuseMap = new BitmapImage(new System.Uri(@"Con_Diffuse_2.jpg", System.UriKind.RelativeOrAbsolute)),
            //NormalMap = new BitmapImage(new System.Uri(@"Con_Normal_2.jpg", System.UriKind.RelativeOrAbsolute)),
        };

        var max = _mainWindow.MyBuildingComponents.Count;
        var i = 1;

        // Loop over building components
        foreach (var component in _mainWindow.MyBuildingComponents)
        {

            //if (i == 5) break;
            var component1 = component;
            var componentTriangles = _mainWindow.MyTriangles.Where(triangle => triangle.ComponentId == component1.Id);

                var meshBuilder = new MeshBuilder(true, true, true);

            // Loop over triangles in building element
            foreach (var triangle in componentTriangles)
            {
                var triangle1 = triangle;

                var p1 = _mainWindow.MyVertices.Find(
                    vt => vt.Id == triangle1.PointId1);
                var p2 = _mainWindow.MyVertices.Find(
                    vt => vt.Id == triangle1.PointId2);
                var p3 = _mainWindow.MyVertices.Find(
                    vt => vt.Id == triangle1.PointId3);
                if (p1 != null && p2 != null && p3 != null)
                {
                    //meshBuilder.AddTriangle(new Vector3((float)p1.X, (float)p1.Y, (float)p1.Z),
                    //    new Vector3((float)p2.X, (float)p2.Y, (float)p2.Z),
                    //    new Vector3((float)p3.X, (float)p3.Y, (float)p3.Z));

                    // coordination are switched to match the coordinate system in SharpDX viewport
                    meshBuilder.AddTriangle(new Vector3(-(float)p1.Y, (float)p1.Z, -(float)p1.X),
                        new Vector3(-(float)p2.Y, (float)p2.Z, -(float)p2.X),
                        new Vector3(-(float)p3.Y, (float)p3.Z, -(float)p3.X));
                }
            }
            var mesh = meshBuilder.ToMeshGeometry3D();

            var meshandtriangle = new MeshIdandGeometry
            {
                Id = component1.Id,
                Geometry = mesh,
                Material = standardMaterial,
            };
            geometry.Add(meshandtriangle);

            i++;
            var progressPercentage = Convert.ToInt32(((double)i / max) * 100);
            var backgroundWorker = sender as BackgroundWorker;
            backgroundWorker?.ReportProgress(progressPercentage, component1.Id);
        }

        e.Result = geometry;

    }

Solution

  • Big thanks to @egse for finding a solution.

    Part of the code that causes the problem:

        var standardMaterial = new PhongMaterial()
        {
            AmbientColor = SharpDX.Color.LightGray,
            //DiffuseColor = new Color4(0.35f, 0.35f, 0.35f, 1.0f),
            //DiffuseMap = new BitmapImage(new System.Uri(@"Con_Diffuse_2.jpg", System.UriKind.RelativeOrAbsolute)),
            //NormalMap = new BitmapImage(new System.Uri(@"Con_Normal_2.jpg", System.UriKind.RelativeOrAbsolute)),
        };
    

    Basically the problem with the above code is that the material is created as a local variable inside the scope of the backgroundworker. This causes problems with ownership when the UI thread tries to enter the material objects.

    The solution to this problem is to make sure that the material is created by the UI thread (e.g in this case, the constructor of the Geometryhandler)

    TL;DR: Do not create instances of classes which inherit from DependencyObject in another thread than the UI.