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;
}
}