Merge buffer geometries with multiple materials and groups

Hi, I have a scene in OBJ format where there is one mesh object duplicated several times, (same geometry, same materials, different positions).

For optimization, I would like to merge all the instances of this mesh as a single BufferGeometry.
If the mesh has a single material the result is correct, but with multiple materials I got incorrect result.

This is the geometry data of the mesh as imported from the obj file:

1. 0: Mesh

  1. castShadow: false
  2. children: []
  3. drawMode: 0
  4. frustumCulled: true
  5. geometry: BufferGeometry

    1. attributes: {position: Float32BufferAttribute, normal: Float32BufferAttribute, uv: Float32BufferAttribute}
    2. boundingBox: null
    3. boundingSphere: null
    4. drawRange: {start: 0, count: Infinity}
    5. groups: Array(2)

      1. 0: {start: 0, count: 78, materialIndex: 0}
      2. 1: {start: 78, count: 6, materialIndex: 1}
      3. length: 2
      4. __proto__: Array(0)

    6. index: null
    7. morphAttributes: {}
    8. name: ""
    9. type: "BufferGeometry"
    10. userData: {}
    11. uuid: "C9FFD8FA-15E3-424B-9C86-1AA3D0A638F5"
    12. drawcalls: (...)
    13. id: 15
    14. offsets: (...)
    15. __proto__: EventDispatcher

  6. layers: Layers {mask: 1}
  7. material: (2) [MeshPhongMaterial, MeshPhongMaterial]
  8. matrix: Matrix4 {elements: Array(16)}
  9. matrixAutoUpdate: true
  10. matrixWorld: Matrix4 {elements: Array(16)}
  11. matrixWorldNeedsUpdate: false
  12. name: "FLOOR_1"
   [...]

This is the code I have used for merging:

function sceneTraverse(obj, fn) {
    fn(obj);
    if (obj.children && obj.children.length) {
        obj.children.forEach(o => {
            sceneTraverse(o, fn);
        });
    }
}

let textureLoader = new T.TextureLoader()
let loader = new T.OBJLoader()

let material_1 = new T.MeshStandardMaterial({
    name: "material_1",
    color: 0xffffff,
    map: textureLoader.load("assets/texture1.png"),
    metalness: 1.0,
    roughness: 0.4    
})

let material_2 = new T.MeshStandardMaterial({
    name: "material_2",
    color: 0xffffff,
    map: textureLoader.load("assets/texture2.png"),
    metalness: 0,
    roughness: 0.6    
})


loader.load("assets/scene.obj", obj => {

    console.log(obj)
    let geometries = []

    sceneTraverse(obj, o => {

        if (o.type && o.type === "Mesh" && o.name && o.name === "FLOOR_1") {
            geometries.push(o.geometry.clone())
        }

    })
    
    let mergedGeometry = T.BufferGeometryUtils.mergeBufferGeometries(geometries, true)
    let mergedMesh = new T.Mesh(mergedGeometry, [material_1, material_2])     
    scene.add(mergedMesh)

})

This is the correct result (without merging):

s2

This is the result I got with merging:

s1

Any idea how to fix that?

1 Like

I’m not sure this is related but try the following: When you merge individual meshes into a single one, you have to ensure that for each mesh the respective geometry is transformed correctly. In other words, you have to apply the transformation matrix of the mesh to the geometry data like illustrated in the following example code:

2 Likes

Or a simplified version of that, for each geometry:

var geometry = mesh.geometry;
geometry.applyMatrix( mesh.matrixWorld );

For optimization, I would like to merge all the instances of this mesh as a single BufferGeometry.
If the mesh has a single material the result is correct, but with multiple materials I got incorrect result.

Note that if each geometry has a different material, merging will not improve your performance — each group still has to be a separate draw call, the same as if they were separate meshes. Some code, like raycasting, may get slower. If you have more meshes than materials, the best thing would be to merge all of the geometry that shares a material, without using groups.

There are also techniques (e.g. texture atlases or vertex colors) to prepare your models so that they can all share a single material while still looking the same way they do now.

1 Like

Thank you guys for the help. I’ve tried to apply the transform matrix as you suggested, this is the result:

05

I think there is something wrong on how the group indexes generated during the merge process.
The original mesh has a geometry with the following groups:

groups: Array(2)
    0: {start: 0, count: 12, materialIndex: 0}
    1: {start: 12, count: 696, materialIndex: 1}

These are the materials (which I override):

material: Array(2)
    0: MeshPhongMaterial {uuid: "02355278-B0AB-473B-9A28-A10E3DCF4AAA", name: "HD_Alpha_02_Norm", type: "MeshPhongMaterial", fog: true, lights: true, …}
    1: MeshPhongMaterial {uuid: "8B135361-EC23-4F3D-83DB-05DE2377B472", name: "HD_Stuff_01_Norm", type: "MeshPhongMaterial", fog: true, lights: true, …}

When I merge the geometries of the two meshes (which are the same object) I got a BufferGeometry with the following groups:

groups: Array(2)
    0: {start: 0, count: 708, materialIndex: 0}
    1: {start: 708, count: 708, materialIndex: 1}

It seems incorrect to me, what do you think?

I’ve attached a test project to reproduce the result

merge_test.zip (1.2 MB)

1 Like

Hm, I don’t think BufferGeometryUtils can correctly merge geometries that already have groups — the existing groups are being lost, as you see… is OBJLoader creating the groups, or your own code? You may need to file a bug or try to patch the mergeBufferGeometries() method to support existing groups…

2 Likes

Hi @donmccurdy, the groups come from the exporter. I’ve tried also to load the same model in glTF format but got the same result.

I will be happy to patch the mergeBufferGeometries but I need some reference on how groups and indexes work in buffer geometries. Where can I find some good resources about this topic?

Thanks for the help!