Merge materials and split mesh in sub-meshes based on texture

We are working with huge 3D mesh objects and are evaluating the Aspose library.

We Aspose reads the obj or fbx files we get 1 Mesh ChildNode with a million materials. When using Assimp we get 4 meshes and 4 materials. How can we merge all these materials during import and how can we get the 4 sub-meshes (1 per texture) instead of 1 huge mesh?

@njannink

To merge materials and split a large mesh into sub-meshes based on texture using Aspose.3D, follow these steps:

  1. Load the 3D model: Use the appropriate loading method for your file type (e.g., OBJ or FBX).
  2. Iterate through the mesh: Access the mesh nodes and their materials.
  3. Group by texture: Create a dictionary or similar structure to group sub-meshes by their textures.
  4. Create new sub-meshes: For each unique texture, create a new mesh and assign the corresponding vertices and materials.

Here’s a basic example in C#:

using Aspose.ThreeD;
using Aspose.ThreeD.Entities;
using System.Collections.Generic;

public void SplitMeshByTexture(string filePath)
{
    // Load the 3D model
    Scene scene = new Scene();
    scene.Open(filePath);

    // Dictionary to hold sub-meshes by texture
    Dictionary<string, Mesh> subMeshes = new Dictionary<string, Mesh>();

    foreach (var node in scene.RootNode.ChildNodes)
    {
        if (node is Mesh mesh)
        {
            foreach (var material in mesh.Materials)
            {
                string texturePath = material.DiffuseTexture?.FilePath;

                if (!subMeshes.ContainsKey(texturePath))
                {
                    subMeshes[texturePath] = new Mesh();
                }

                // Add vertices and indices to the sub-mesh based on the texture
                // This part requires more detailed implementation based on your mesh structure
            }
        }
    }

    // Now you can save or further process the sub-meshes
}

This code provides a starting point. You will need to implement the logic for copying vertices and indices to the new sub-meshes based on the textures.

For further details, please consult the official Aspose.3D documentation directly within your development environment.

@njannink
Could you share the IFC file that’s giving you trouble? It would help me reproduce the exact issue you’re seeing and make sure the examples I’m working on address your specific case.
Thanks.

Sorry I cannot share the obj or fbx files because they are proprietary.

I use this algorithm now. The shapes of the sub-meshes seems to be fine, but the textures / uvs are completely distored. Maybe you have an idea why?

        Dictionary<string, Mesh> meshes = new();
 
        foreach (var child in root.ChildNodes.Where(x => x.Entity is Mesh))
        {
            var mesh = ((Mesh) child.Entity).Triangulate();
            var materialVertex = mesh.VertexElements.OfType<VertexElementMaterial>().First();
            var uvVertex = mesh.VertexElements.OfType<VertexElementUV>().First();

            foreach (var material in child.Materials)
            {
                if (material.GetTexture(Material.MapDiffuse) is Texture tex && !meshes.ContainsKey(tex.FileName))
                {
                    var index = meshes.Count;

                    meshes[tex.FileName] = new()
                    {
                        Index = index,
                        Uvs = [],
                        Trimesh = new() { Tris = [], Vertices = [] }
                    };
                }
            }

            var polygonMap = new int[mesh.ControlPoints.Count, meshes.Count];

            for (int i = 0; i < mesh.PolygonCount; i++)
            {
                var materialIndex = materialVertex.Indices[i];
                var material = child.Materials[materialIndex];
                var slot = material.GetTexture(Material.MapDiffuse) as Texture;
                if (slot == null) continue;

                if (!meshes.TryGetValue(slot.FileName, out var part)) continue;
                var trimesh = part.Trimesh;

                var polygon = mesh.Polygons[i];
                var p1 = MapPolygonVertices(polygon[0]);
                var p2 = MapPolygonVertices(polygon[1]);
                var p3 = MapPolygonVertices(polygon[2]);

                trimesh.Tris.AddRange([p1 - 1, p2 - 1, p3 - 1]);

                continue;

                int MapPolygonVertices(int p)
                {
                    // 1-based index in map, 0 means not mapped yet
                    if (polygonMap[p, part.Index] == 0)
                    {
                        polygonMap[p, part.Index] = trimesh.Vertices.Count + 1;

                        var cp = mesh.ControlPoints[p];
                        trimesh.Vertices.Add(new float3
                        {
                            X = (float) cp.X,
                            Y = (float) cp.Y,
                            Z = (float) cp.Z
                        });

                        var uv = uvVertex.Data[uvVertex.Indices[p]];
                        part.Uvs.Add(new float2
                        {
                            X = (float) uv.X % 1f,
                            Y = (float) uv.Y % 1f
                        });
                    }

                    return polygonMap[p, part.Index];
                }
            }
        }

@njannink
There’s no methods to merge materials directly, do you have duplicated(by reference or by definition) materials in the “million” materials?

Maybe you can try PolygonModifier.SplitMesh which allows you to split Mesh to sub meshes by materials defined in VertexElementMaterial:

Thanks.

The obj file in question has almost a million materials and only 4 texture files after loading the obj file. Assimp has the option to merge materials does Aspose have this option?

@njannink
We have opened the following new ticket(s) in our internal issue tracking system and will deliver their fixes according to the terms mentioned in Free Support Policies.

Issue ID(s): THREEDNET-1728

You can obtain Paid Support Services if you need support on a priority basis, along with the direct access to our Paid Support management team.

If I use the SplitMesh option on the obj file in question I run out of memory (64GB) while without split I can load the file. I will check with my team if I can share the obj.

If you mail me on my registered email I can share a download link to the obj file in question

Hi @njannink, I don’t have your email, can you please share me the file to lex.chou@aspose.com?
The file will be only used for analyzing the issue, and will be removed once the issue fixed.
Thanks!

Hi Lex I send the file through wetransfer because its big. This is what I do now to merge all the materials.

    private static void RepairMaterials(Node root)
    {
        var original = (Mesh) root.Entity;
        var materialVertex = original.VertexElements.OfType<VertexElementMaterial>().FirstOrDefault();

        if (root.Materials.Count == 0 || materialVertex == null)
        {
            return;
        }

        var newMaterials = root.Materials
            .GroupBy(x => x.Name)
            .Select(x => x.First())
            .ToList();

        var materialIndex = newMaterials
            .Select((m, i) => (m, i))
            .ToDictionary(x => x.m.Name, y => y.i);

        var newMapping = materialVertex
            .Indices
            .Select(i => materialIndex[root.Materials[i].Name])
            .ToArray();

        root.Materials.Clear();
        foreach (var mat in newMaterials)
        {
            root.Materials.Add(mat);
        }
        materialVertex.Clear();
        materialVertex.SetIndices(newMapping);
    }

@njannink
Got the file, thanks! We’ll work through the issues next week.

Hi @njannink

We have resolved the material-related issues in version 25.11.0, including problems with duplicated materials and filename quotation handling.

And we also achieved substantial enhancements in memory footprint and performance.
Benchmark Results:
Version 25.9:

Benchmark of 25.9.0.0
Peak working set 6006030336 - 43499520 = 5.55GB, time: 00:00:28.2536556

Version 25.11:

Benchmark of 25.11.0.0
Peak working set 3850350592 - 43450368 = 3.55GB, time: 00:00:21.3658741

Performance Improvements:

Memory usage: 36% reduction (from 5.55GB to 3.55GB)
Import time: 24% reduction (from 28.25s to 21.37s)

Test Code:

Console.WriteLine("Benchmark of " + typeof(Scene).Assembly.GetName().Version);
var proc = Process.GetCurrentProcess();
var peak = proc.WorkingSet64;
var sw = Stopwatch.StartNew();
var s = Scene.FromFile(@"Raplee Trail_higher res texture\Raplee Trail_higher res texture.obj");
sw.Stop();
proc = Process.GetCurrentProcess();
var peak2 = proc.WorkingSet64;
Console.WriteLine($"Peak working set {peak2} - {peak} = {((peak2 - peak)/1024.0/1024.0/1024.0).ToString(".00")}GB, time: {sw.Elapsed}");

The reason we couldn’t achieve even faster performance is due to the OBJ file format’s inherent structure. OBJ files allow mixed data between different objects, which requires us to reconstruct meshes internally to retain only the necessary data for each individual mesh. While this reconstruction process is unnecessary and resource-intensive for most files, we must perform it to maintain full compatibility with the OBJ specification.

We hope these improvements enhance your development experience with Aspose.3D.

Best Regards.

Hello @lex.chou,

Thanks for the fixed and enhancements. Exactly because of what you mention for full compatibility is the reason why we cannot use Aspose.3D, because simple because of the big drone obj files that we use we quickly run into memory an performance issues when importing these files.

Maybe its an idea you can turn this off through the ObjOptions?

@njannink
Thank you for your suggestions. We will investigate the feasibility and begin implementing the improvements accordingly.

The current released version has optimized memory usage for normal/UV/polygon data. However, due to our unified model architecture that must persist between 3D CAD and standard 3D files, we are required to use double4 data types for control points. This creates a memory inefficiency when working with OBJ files. While the compatibility option may accelerate import performance, it will not significantly address the control point memory overhead (float3 vs. double4).

Over the coming days, we plan to investigate the following optimizations:

Dual data type support for control points: Enable float3/double4 to coexist and switch dynamically based on requirements. This optimization could reduce control point memory usage by up to 65% for large files.

Optional mesh reconstruction bypass: Explore the possibility of skipping mesh reconstruction for OBJ files when not needed.

Additional performance optimizations: Identify and implement other potential improvements.

We will update you as soon as we have progress to report.