Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Low performance in MultiMaterialNode
#1
Bug 
Hi

I am trying to use new MultiMaterialNode class in my app, but I found scene extremely slow when using it instead MeshModelNodes

Is there anything I'm doing wrong?
#2
Please provide more details and/or a minimal example.
#3
Hi.
This is the code I use to generate the node:

public static GeometryModelNode? CreateGeometryNode(IBimElement? element, long elementPtr, ConcurrentBag<StandardMaterial> sceneMats)
{
if (element is null || element.Hierarchy is not BimHierarchy.GeometryElement || !element.GetModel().GetVertexesAndIndexesBuffers(elementPtr, out var flatVertexData, out var indexes))
{
return null;
}

var valuesPerVertex = element.GetModel().GetValuesPerVertex();
var vertexCount = flatVertexData!.Length / element.GetModel().GetValuesPerVertex();
var vertexes = new Vector3[vertexCount];
var normals = new Vector3[vertexCount];
for (int i = 0; i < vertexCount; i++)
{
vertexes[i] = new Vector3(flatVertexData[i * valuesPerVertex], flatVertexData[i * valuesPerVertex + 1], flatVertexData[i * valuesPerVertex + 2]);
normals[i] = new Vector3(flatVertexData[i * valuesPerVertex + 3], flatVertexData[i * valuesPerVertex + 4], flatVertexData[i * valuesPerVertex + 5]);
}

var subIndexes = new Dictionary<StandardMaterial, List<int>>();
for (int i = 0; i < indexes!.Length; i += 3)
{
int index0 = indexes[i];
int index1 = indexes[i + 1];
int index2 = indexes[i + 2];
var colors = ModelGroupNode.GetColorKey(flatVertexData, valuesPerVertex, index0);
var mat = subIndexes.Keys.FirstOrDefault(mat => mat.DiffuseColor == colors[0] && mat.EmissiveColor == colors[1] && mat.SpecularColor == colors[2] && mat.HasTransparency == colors[0].Alpha < 1f);
mat ??= sceneMats.FirstOrDefault(mat => mat.DiffuseColor == colors[0] && mat.EmissiveColor == colors[1] && mat.SpecularColor == colors[2] && mat.HasTransparency == colors[0].Alpha < 1f);
if (mat is null)
{
mat = new StandardMaterial()
{
DiffuseColor = colors[0],
EmissiveColor = colors[1],
Opacity = colors[0].Alpha,
SpecularColor = colors[2],
SpecularPower = 0.5f
};
sceneMats.Add(mat);
}

if (!subIndexes.TryGetValue(mat, out var value))
{
value = ([]);
subIndexes[mat] = value;
}

value.AddRange([index0, index1, index2]);
}

var updatedIndexes = subIndexes.SelectMany(kvp => kvp.Value).ToArray();
var texture = MeshUtils.GenerateCubicTextureCoordinates(vertexes, updatedIndexes);
var mesh = new GeometryMesh(vertexes, normals, texture, updatedIndexes);
var mats = subIndexes.Keys.ToArray();
var subMeshes = new SubMesh[subIndexes.Count];
var stIndLoc = 0;

for (int i = 0; i < subIndexes.Count; i++)
{
var kvp = subIndexes.ElementAt(i);
var sm = new SubMesh(i, stIndLoc, kvp.Value.Count)
{
BackMaterialIndex = kvp.Key is StandardMaterial stdMat && stdMat.HasTransparency ? i : -1
};
stIndLoc += kvp.Value.Count;
subMeshes[i] = sm;
}

return new GeometryModelNode(element, mesh, mats, subMeshes);
}
#4
Brick 
Brief explanation...

I first retrieve all vertex and normals, and get the color for each point.

Then I re-organize them by material, so I can create the submeshes. I try to re-use existing scene materials if possible

I finally iterate throgh the dictionary to create the submeshes one by one

Lastly, I return the geomtry node

I attach the image of what I get. As seen, only few geometries ar rendered, but it moves hell slow
I also attach the image of what I should get using MeshModelNodes per each different geometry, that works really fast


Attached Files Thumbnail(s)
       
#5
I have taken your post seriously and have written a big test to check the performance of the  MultiMaterialModelNode (see code below).

After that, I can say that as I expected, the MultiMaterialModelNode is faster than many MeshModelNodes. But maybe you have some special use case that works differently.

Here is the comparison between MultiMaterialModelNode with 1000 SubMeshes and 1000 MeshModelNodes:

Initialization:
Initializing MultiMaterialModelNode is much faster than many MeshModelNodes - the test code runs more than 100x faster. The problem with initializing many MeshModelNodes is that for each MeshModelNodes a new VertexBuffer and new IndexBuffer need to be created on the GPU - this requires sending the data to the GPU and waiting for this operation to be completed (for each buffer). This can be very slow (in the next version of SharpEngine I plan to introduce background resource creation so this will be much faster). When using MultiMaterialModelNode, then only a single VertexBuffer and a single IndexBuffer are created. This is much faster (time from 1 to 2 ms, compared to around 200 ms for 1000 MeshModelNodes).

Rendering:
Rendering one SubMesh requires one draw call. Also, rendering one MeshModelNodes requires one draw call. But when rendering SubMeshes we do not need to change the bound VertexBuffer and bound IndexBuffer because they all use the same buffers. This means that rendering SubMeshes is slightly faster than rendering  MeshModelNodes.

Changing materials:
When a material is changed, then new rendering commands need to be recorded again (after this is done, then the same commands can be replayed for the next frame - in case only camera is changed, then only camera matrix is changed and the same commands are replayed).

This works the same for MultiMaterialModelNode and for many MeshModelNodes. In the text code below you can test how this works for changing the material to any other already used material (calling ChangeMaterialFromExistingMaterials) or for changing the material to a completely new material (ChangeMaterialToNewMaterial).



Here is the test code. You can copy it to the end of the QuickStart/SharpEngineSceneViewInXaml.xaml.cs file and then call the GenerateTestScene(useMultiMaterialModelNode: true / false); in the RenderToBitmapButton_OnClick event handler. To see rendering performance, open the Diagnostics window while the scene in rendered.

Code:
        private const string ResultFileName = @"c:\temp\MultiMaterialModelNode.txt";

        private Stopwatch _stopwatch = new();
        private SubMesh[]? _subMeshes;
        private Material[]? _randomMaterials;
        private MultiMaterialModelNode _multiMaterialModelNode;

        private void GenerateTestScene(bool useMultiMaterialModelNode)
        {
            MainSceneView.Scene.RootNode.Clear();

            _stopwatch.Start();


            var allMeshes = new List<StandardMesh>();

            int singleMeshTriangleIndicesCount = 0;

            for (int x = 0; x < 10; x ++)
            {
                for (int y = 0; y < 10; y++)
                {
                    for (int z = 0; z < 10; z++)
                    {
                        var position = new Vector3(-180 + x * 40, -180 + y * 40, -180 + z * 40);
                        var oneBoxMesh = MeshFactory.CreateBoxMesh(position, new Vector3(30, 30, 30), 1, 1, 1);
                        allMeshes.Add(oneBoxMesh);

                        singleMeshTriangleIndicesCount = (int)oneBoxMesh.IndexCount;
                    }
                }
            }


            _randomMaterials = new Material[10];
            for (int i = 0; i < _randomMaterials.Length; i++)
            {
                var randomColor = Color4.FromHsl(Random.Shared.NextSingle() * 360);
                _randomMaterials[i] = new StandardMaterial(randomColor);
            }


            if (useMultiMaterialModelNode)
            {
                var singleMesh = MeshUtils.CombineMeshes(allMeshes);

                _subMeshes = new SubMesh[1000];

                int currentIndex = 0;
                for (int i = 0; i < _subMeshes.Length; i++)
                {
                    _subMeshes[i] = new SubMesh(Random.Shared.Next(_randomMaterials.Length), currentIndex, singleMeshTriangleIndicesCount); // it is recommended to also specify BoundingBox if it is known (we could store that in a seperate list after calling MeshFactory.CreateBoxMesh - this prevents to calculate bounding box again by SharpEngine)
                    currentIndex += singleMeshTriangleIndicesCount;
                }


                _multiMaterialModelNode = new MultiMaterialModelNode(singleMesh, _randomMaterials, _subMeshes.ToArray());

                MainSceneView.Scene.RootNode.Add(_multiMaterialModelNode);
            }
            else
            {
                for (int i = 0; i < allMeshes.Count; i++)
                {
                    var material = _randomMaterials[Random.Shared.Next(_randomMaterials.Length)];
                    var meshModelNode = new MeshModelNode(allMeshes[i], material);
                    MainSceneView.Scene.RootNode.Add(meshModelNode);
                }
            }
           
            System.IO.File.AppendAllText(ResultFileName, $"{(useMultiMaterialModelNode ? "MultiMaterialModelNode" : "MeshModelNodes")} init time: {_stopwatch.Elapsed.TotalMilliseconds:F2} ms\n");

            MainSceneView.SceneRendered += SharpEngineSceneViewOnSceneRendered;

            _stopwatch.Restart();
        }



        private void SharpEngineSceneViewOnSceneRendered(object? sender, EventArgs e)
        {
            // Change material on every frame:

            ChangeMaterialFromExistingMaterials();
            //ChangeMaterialToNewMaterial();
        }

        private void ChangeMaterialFromExistingMaterials()
        {
            if (_randomMaterials == null)
                return;

            if (_subMeshes != null)
            {
                var subMesh = _subMeshes[Random.Shared.Next(_subMeshes.Length)];

                // Make sure that we do not change the material to already used material => this would stop rendering new frames
                while (true)
                {
                    int newRandomMaterialIndex = Random.Shared.Next(_randomMaterials.Length);
                    if (subMesh.MaterialIndex != newRandomMaterialIndex)
                    {
                        subMesh.MaterialIndex = newRandomMaterialIndex;
                        break;
                    }
                }
            }
            else
            {
                if (MainSceneView.Scene.RootNode[Random.Shared.Next(MainSceneView.Scene.RootNode.Count)] is MeshModelNode meshModelNode)
                {
                    // Make sure that we do not change the material to already used material => this would stop rendering new frames
                    while (true)
                    {
                        var newMaterial = _randomMaterials[Random.Shared.Next(_randomMaterials.Length)];
                        if (meshModelNode.Material != newMaterial)
                        {
                            meshModelNode.Material = newMaterial;
                            break;
                        }
                    }
                }
            }
        }
       
        private void ChangeMaterialToNewMaterial()
        {
            var randomColor = Color4.FromHsl(Random.Shared.NextSingle() * 360);
            var randomMaterial = new StandardMaterial(randomColor);

            if (_subMeshes != null)
            {
                var subMesh = _subMeshes[Random.Shared.Next(_subMeshes.Length)];
                subMesh.ChangeMaterial(randomMaterial);
            }
            else
            {
                if (MainSceneView.Scene.RootNode[Random.Shared.Next(MainSceneView.Scene.RootNode.Count)] is MeshModelNode meshModelNode)
                    meshModelNode.Material = randomMaterial;
            }
        }
Andrej Benedik
#6
Thank you for sharing the code. It looks ok - you create a MultiMaterialModelNode. Check out also my test code in my post.

I would recommend you to use Visual Studio performance profiler (or any other profiler) and check where the time is spent. Maybe you are recreating the geometry on each frame. 

To check the rendering performance copy the DiagnosticsWindow.xaml and .xaml.cs and CommonDiagnostics.cs (from Common project) to your application and open the diagnostics window when your application is running - this will give you rendering statistics. 

Alternatively, you can set the SceneView.IsCollectingStatistics to true and then check the value of the SceneView.Statistics to see which part of the rendering pipeline takes most of the time.
Andrej Benedik
  


Forum Jump:


Users browsing this thread:
1 Guest(s)