Issue with merging objects by its geometries using mergeBufferGeometries()

In a scene I have a lot of equal static objects, only having a different position. So to have a better performance I want to merge the buffergeometries of these objects togetter into one large object (mergedObj).

So I’ve created a functon called mergeObjects() that merges a copy of a baseObjects geometry and its children geometries into the new mergedObjects geometry and its children geometries.

That is working as expected when having some objects, but if the count of objects expand, or the amount of vertices inside the different geometries rises, problems occur. Some of the geometries will get merged and others aren’t resulting in misformed objects. Sometimes geometries only get merged half.

I’m not sure if this is a problem in my code or that I’m hitting on some limitation of either the merge geometries functions of BufferGeometrieUtils, or some other limitiation of WebGL or memory I’m not aware of. Or maybe I’m missing something else here. But I could use some help at this point after a lot of debugging.

I’ve created a codePen to illustrate the problem:

The base-object in the example exists of an Object3D object with two children: a sphere and a cube. These are on the same x-location, stacked on top of each other.
In canvas you see only the cube gets copied and merged all the way, but the spere is not. If you lower the resolution of the sphere more instances of the spere get copied, so it has something to do with a limitiation somewhere about maximum buffer size of maximum amount of vertices or something like that.

After lot of debugging I’m kind of stuck on this. Anybody who can give me some clue or direction where to look to solve this strange issue? Thanks!

Can you please try to refactor your code according to the following example: https://threejs.org/examples/#webgl_geometry_minecraft

The idea is to use BufferGeometryUtils.mergeBufferGeometries only once. All intermediate geometries are maintained in an array and provided as the parameter of the function. Your code feels a little bit complex right now since you are using stuff like recursive calls. Something like that makes it harder to debug the problem.

Thanks for your response @Mugen87. I just turned the code into creating arrays of geometries first and than calling merge on them once, like you advised.

Here’s the updated codepen:

(Please don’t get intimidated by all the code from the bufferutils I put in there, for me that was just the fastest way of including the threejs utility file.)

The code about merging starts at line 437 and is basically only this:

    const count = 90;
    const mergedObj = baseObj.clone();
    let geometries = [];
    for (let c = 0; c < baseObj.children.length; c++) {
        geometries = [];
        for (let i = 0; i < count; i++) {
            geometries[i] = baseObj.children[c].geometry.clone();
            geometries[i].translate(i * 0.5, 0, 0);
        }
        const mergedGeo = THREE.BufferGeometryUtils.mergeBufferGeometries(geometries);
        mergedObj.children[c].geometry = mergedGeo;
    }

    this.scene.add(mergedObj);

BTW In this demo you can see clearly not only it skips merging the last spheres, it also truncates the last sphere it can handle.

Really? Nobody can help me on this one, not even with this simplyfied version? @Mugen87 ?

I’ve created a new fiddle with parts of your code: https://jsfiddle.net/f2Lommf5/6877/

The result of the merging looks now good.

1 Like

Thanks @Mugen87 for taking the time to setup a project. I understand just merging geometries work with the utils, but what I want and have in the codepen I created, is merge all geometries of equal Objects, including its childrens geometries. So the result I’m after should be one object with its geometry (or not if it’s just an Object3D, but not a mesh, to group children) and the same amount of children the base object has (also either an Object3D/noMesh with subchildren, or a Mesh with or without children).

The example I posted is very simplified. In the project however I have a big gltf scene full of objects. For all equal static (but complex) object, I only include 1 instance in the gltf file, that’s the baseObj. After importing the glft has finished, more ‘instances’ like the baseObj needs to be created on different positions in the scene. But instead of cloning the object, I clone the geometries of the baseObj instead and after that its childrens geometries, making one large object of merged geometries and also merged children geometries (with sub children geometries and so on). That should work, because I only merge objects having geometries (is mesh) and do the same with all children of the baseObj. That’s why I use recursive functions in my project.

But for some strange reason merging children seem to be a problem. I can’t find a reason why, 'cause to me it should work exactly the same as merging the geometry of the object itself. But I might miss something here.

I’d say puting the result of the merge inside the child.geometries should work the same way as putting a result of a merge in the geometry of the object itself. I can’t see a difference, and it looks like it’s doing what is expected for the first few objects too. But I must be missing something here, 'cause it’s getting broke the more meshes/vertices/larger buffers are involved.

So although nice to know merging is working fine with lots of objects, that’s not really the question I have. Basically I want to merge the geometries of objects AND all of its childrens geometries too. I still can’t figure out why this is only working for a limited amount of objects. Any help or pointing in a direction is highly appreciated!

Hi Mugen87! Im using this class to merge geometries to one geometry in this link

But after merge all the geometries,it still looks like more then one geometry when im apply texture
can you give me some guidances?

I’m afraid what you see is the expected behavior. When vertex data are merged by BufferGeometryUtils.mergeBufferGeometries(), they are not changed. Hence, you have no continuous texture coordinates after the merge and thus see seams across the entire mesh.

They only way to avoid this issue is to compute new texture coordinates after the merging process.

Thanks!Is there a API so i can call to compute new texture coordinates?I see the BufferGeometry dont have a method

Hi man!I found a solution in stackoverflow!using Box Uv Mapping
in this link

here is Alex Khoroshylov code

//build some mesh
var bufferGeometry = new THREE.BufferGeometry().fromGeometry(new THREE.DodecahedronGeometry(2.5, 0));
let material = new THREE.MeshPhongMaterial({
    color: 0x10f0f0,
    map: new THREE.TextureLoader().load('http://mbnsay.com/rayys/images/1K_UV_checker.jpg')
});

//find out the dimensions, to let texture size 100% fit without stretching
bufferGeometry.computeBoundingBox();
let bboxSize = bufferGeometry.boundingBox.getSize();
let uvMapSize = Math.min(bboxSize.x, bboxSize.y, bboxSize.z);

let boxGeometry = new THREE.BoxBufferGeometry(uvMapSize, uvMapSize, uvMapSize);
let cube = new THREE.Mesh(boxGeometry, material);

//calculate UV coordinates, if uv attribute is not present, it will be added
applyBoxUV(bufferGeometry, new THREE.Matrix4().getInverse(cube.matrix), uvMapSize);

//let three.js know
bufferGeometry.attributes.uv.needsUpdate = true;

function _applyBoxUV(geom, transformMatrix, bbox, bbox_max_size) {

    let coords = [];
    coords.length = 2 * geom.attributes.position.array.length / 3;

    // geom.removeAttribute('uv');
    if (geom.attributes.uv === undefined) {
        geom.addAttribute('uv', new THREE.Float32BufferAttribute(coords, 2));
    }

    //maps 3 verts of 1 face on the better side of the cube
    //side of the cube can be XY, XZ or YZ
    let makeUVs = function(v0, v1, v2) {

        //pre-rotate the model so that cube sides match world axis
        v0.applyMatrix4(transformMatrix);
        v1.applyMatrix4(transformMatrix);
        v2.applyMatrix4(transformMatrix);

        //get normal of the face, to know into which cube side it maps better
        let n = new THREE.Vector3();
        n.crossVectors(v1.clone().sub(v0), v1.clone().sub(v2)).normalize();

        n.x = Math.abs(n.x);
        n.y = Math.abs(n.y);
        n.z = Math.abs(n.z);

        let uv0 = new THREE.Vector2();
        let uv1 = new THREE.Vector2();
        let uv2 = new THREE.Vector2();
        // xz mapping
        if (n.y > n.x && n.y > n.z) {
            uv0.x = (v0.x - bbox.min.x) / bbox_max_size;
            uv0.y = (bbox.max.z - v0.z) / bbox_max_size;

            uv1.x = (v1.x - bbox.min.x) / bbox_max_size;
            uv1.y = (bbox.max.z - v1.z) / bbox_max_size;

            uv2.x = (v2.x - bbox.min.x) / bbox_max_size;
            uv2.y = (bbox.max.z - v2.z) / bbox_max_size;
        } else
        if (n.x > n.y && n.x > n.z) {
            uv0.x = (v0.z - bbox.min.z) / bbox_max_size;
            uv0.y = (v0.y - bbox.min.y) / bbox_max_size;

            uv1.x = (v1.z - bbox.min.z) / bbox_max_size;
            uv1.y = (v1.y - bbox.min.y) / bbox_max_size;

            uv2.x = (v2.z - bbox.min.z) / bbox_max_size;
            uv2.y = (v2.y - bbox.min.y) / bbox_max_size;
        } else
        if (n.z > n.y && n.z > n.x) {
            uv0.x = (v0.x - bbox.min.x) / bbox_max_size;
            uv0.y = (v0.y - bbox.min.y) / bbox_max_size;

            uv1.x = (v1.x - bbox.min.x) / bbox_max_size;
            uv1.y = (v1.y - bbox.min.y) / bbox_max_size;

            uv2.x = (v2.x - bbox.min.x) / bbox_max_size;
            uv2.y = (v2.y - bbox.min.y) / bbox_max_size;
        }

        return {
            uv0: uv0,
            uv1: uv1,
            uv2: uv2
        };
    };

    if (geom.index) { // is it indexed buffer geometry?
        for (let vi = 0; vi < geom.index.array.length; vi += 3) {
            let idx0 = geom.index.array[vi];
            let idx1 = geom.index.array[vi + 1];
            let idx2 = geom.index.array[vi + 2];

            let vx0 = geom.attributes.position.array[3 * idx0];
            let vy0 = geom.attributes.position.array[3 * idx0 + 1];
            let vz0 = geom.attributes.position.array[3 * idx0 + 2];

            let vx1 = geom.attributes.position.array[3 * idx1];
            let vy1 = geom.attributes.position.array[3 * idx1 + 1];
            let vz1 = geom.attributes.position.array[3 * idx1 + 2];

            let vx2 = geom.attributes.position.array[3 * idx2];
            let vy2 = geom.attributes.position.array[3 * idx2 + 1];
            let vz2 = geom.attributes.position.array[3 * idx2 + 2];

            let v0 = new THREE.Vector3(vx0, vy0, vz0);
            let v1 = new THREE.Vector3(vx1, vy1, vz1);
            let v2 = new THREE.Vector3(vx2, vy2, vz2);

            let uvs = makeUVs(v0, v1, v2, coords);

            coords[2 * idx0] = uvs.uv0.x;
            coords[2 * idx0 + 1] = uvs.uv0.y;

            coords[2 * idx1] = uvs.uv1.x;
            coords[2 * idx1 + 1] = uvs.uv1.y;

            coords[2 * idx2] = uvs.uv2.x;
            coords[2 * idx2 + 1] = uvs.uv2.y;
        }
    } else {
        for (let vi = 0; vi < geom.attributes.position.array.length; vi += 9) {
            let vx0 = geom.attributes.position.array[vi];
            let vy0 = geom.attributes.position.array[vi + 1];
            let vz0 = geom.attributes.position.array[vi + 2];

            let vx1 = geom.attributes.position.array[vi + 3];
            let vy1 = geom.attributes.position.array[vi + 4];
            let vz1 = geom.attributes.position.array[vi + 5];

            let vx2 = geom.attributes.position.array[vi + 6];
            let vy2 = geom.attributes.position.array[vi + 7];
            let vz2 = geom.attributes.position.array[vi + 8];

            let v0 = new THREE.Vector3(vx0, vy0, vz0);
            let v1 = new THREE.Vector3(vx1, vy1, vz1);
            let v2 = new THREE.Vector3(vx2, vy2, vz2);

            let uvs = makeUVs(v0, v1, v2, coords);

            let idx0 = vi / 3;
            let idx1 = idx0 + 1;
            let idx2 = idx0 + 2;

            coords[2 * idx0] = uvs.uv0.x;
            coords[2 * idx0 + 1] = uvs.uv0.y;

            coords[2 * idx1] = uvs.uv1.x;
            coords[2 * idx1 + 1] = uvs.uv1.y;

            coords[2 * idx2] = uvs.uv2.x;
            coords[2 * idx2 + 1] = uvs.uv2.y;
        }
    }

    geom.attributes.uv.array = new Float32Array(coords);
}

function applyBoxUV(bufferGeometry, transformMatrix, boxSize) {

    if (transformMatrix === undefined) {
        transformMatrix = new THREE.Matrix4();
    }

    if (boxSize === undefined) {
        let geom = bufferGeometry;
        geom.computeBoundingBox();
        let bbox = geom.boundingBox;

        let bbox_size_x = bbox.max.x - bbox.min.x;
        let bbox_size_z = bbox.max.z - bbox.min.z;
        let bbox_size_y = bbox.max.y - bbox.min.y;

        boxSize = Math.max(bbox_size_x, bbox_size_y, bbox_size_z);
    }

    let uvBbox = new THREE.Box3(new THREE.Vector3(-boxSize / 2, -boxSize / 2, -boxSize / 2), new THREE.Vector3(boxSize / 2, boxSize / 2, boxSize / 2));

    _applyBoxUV(bufferGeometry, transformMatrix, uvBbox, boxSize);

}