Bad performance when loading more than 3500 meshes into the scene

Hello to all,
in am writing a quite big 3D app with three.js. So I hope I can copy out enough parts to support you information without posting these thousends of line of code…
First of all the scene, lights, objects aso… work just fine with all my test-objects.
Then I started to load data from a real project (from another system). So I read all the information and create three.js objects according to the data given.

displayObjectTree(tree , parentObjNr = 0) {
        if(parentObjNr < 100) this.clearScene();
        //todo find parentObjNr
        this.m_iCountMeshes = 0;
        let currentNode = this.m_rootNode;
        this.createThreeSceneObject(currentNode , tree);
        this.updateGroups(this.m_rootNode);
    }

    createThreeSceneObject(currentSceneNode , srvObjInformation) {
        if(!srvObjInformation.children) {
            console.error("NO SRV INFORMATION TO BUILD OBJECT!!");
        }
        for(let i = 0 ; i < srvObjInformation.children.length ; i++) {
            let newSceneParent = null;
            let child = srvObjInformation.children[i];
            switch(child.id) {
                case ClassID.eClass_ID_CCadObjTree:
                    case ClassID.eClass_ID_CCadObject:
                    newSceneParent = this.createThreeSceneObject_ClassTree(currentSceneNode , child);
                    break;
                case ClassID.eClass_ID_CCadPunkt:
                case ClassID.eClass_ID_CCadVPunkt:
                case ClassID.eClass_ID_CCadAPunkt:
                case ClassID.eClass_ID_CCadLEPunkt:
                case ClassID.eClass_ID_CCadESPunkt:
                case ClassID.eClass_ID_CCadLGPunkt:
                case ClassID.eClass_ID_CCadVEPunkt:
                case ClassID.eClass_ID_CCadRPunkt:
                case ClassID.eClass_ID_CCadSPunkt:
                case ClassID.eClass_ID_CCadW2Punkt:
                    newSceneParent = this.createThreeSceneObject_CadPunkt(currentSceneNode , child);
                    break;
                case ClassID.eClass_ID_CCadFlaeche:
                    newSceneParent = this.createThreeSceneObject_CadFlaeche(currentSceneNode , child);
                    break;
                case ClassID.eClass_ID_CCadLinie:
                    newSceneParent = this.createThreeSceneObject_CadFLinie(currentSceneNode , child);
                    break;
                default:
                    err("unkown child.id: " + child.id);
            }            
            if(newSceneParent) {
                this.m_iCountMeshes++;
                document.title = "# Meshes = " + this.m_iCountMeshes;
                newSceneParent.name = child.objnr;
                newSceneParent.userData.SIA = {};
                newSceneParent.userData.SIA.guid = child.guid;
                newSceneParent.userData.SIA.objnr = child.objnr;
                newSceneParent.userData.SIA.parent = child.parent;
                newSceneParent.userData.SIA.tag = child.tag;
                newSceneParent.userData.SIA.type = child.type;
                newSceneParent.userData.SIA.id = child.id;
                newSceneParent.userData.SIA.flags = child.flags;
                newSceneParent.userData.SIA.layer = child.layer;
                newSceneParent.userData.SIA.select = child.select;
                if("ref1" in child) newSceneParent.userData.SIA.ref1 = child.ref1;
                if("ref2" in child) newSceneParent.userData.SIA.ref2 = child.ref2;
                if("ref3" in child) newSceneParent.userData.SIA.ref3 = child.ref3;
                if("ref4" in child) newSceneParent.userData.SIA.ref4 = child.ref4;
                if("ref5" in child) newSceneParent.userData.SIA.ref5 = child.ref5;
                if("vx" in child) newSceneParent.userData.SIA.vx = child.vx;
                if("vy" in child) newSceneParent.userData.SIA.vy = child.vy;
                if("vz" in child) newSceneParent.userData.SIA.vz = child.vz;
                if("invers" in child) newSceneParent.userData.SIA.invers = child.invers;
                if("inversX" in child) newSceneParent.userData.SIA.inversX = child.inversX;
                if("inversY" in child) {
                    newSceneParent.userData.SIA.inversY = child.inversY;
                }
                if("inversZ" in child) newSceneParent.userData.SIA.inversZ = child.inversZ;

                if((newSceneParent.userData.SIA.flags & ObjectFlags.OF_GRUPPE_AUS) == 0) {
                    newSceneParent.layers.enable(LAYER.OBJECT_IS_VISIBLE);
                }else {
                    newSceneParent.layers.disable(LAYER.OBJECT_IS_VISIBLE);
                }
                if(newSceneParent.userData.SIA.select == false) {
                    newSceneParent.layers.disable(LAYER.OBJECT_IS_SELECTABLE);
                }else {
                    newSceneParent.layers.enable(LAYER.OBJECT_IS_SELECTABLE);
                }
                
                this.createThreeSceneObject(newSceneParent , child);
            }
        }
    }

creating the single objects looks like this

createThreeSceneObject_CadPunkt(parentNode, objProperties) {
        parentNode = parentNode || this.m_rootNode;
        let color = 0xff0000;
        let opacity = 1.0;
        let radius = 0.5; // 2*0.5 = 1 (m)
        let fScale = 1.0 / 1000.0; //Skalieren von Meter auf MM

        if("color" in objProperties) {
            let tplC = this.kkpColorToColor(objProperties.color);
            color = tplC[0];
            opacity = tplC[1] / 255.0;
        }

        if("ptSize" in objProperties) {
            fScale = (objProperties.ptSize / 1000.0);
        }

        let geometry = new THREE.SphereGeometry(2 * radius , 5 , 5);
        let material = new THREE.MeshPhongMaterial({color: color, fog: false, specular: 0x858585 , shininess:48, transparent: true, opacity: opacity});
        let node = new THREE.Mesh(geometry , material);
        node.castShadow = true;
        node.receiveShadow = true;
        node.position.x = objProperties.vPos.x;
        node.position.y = objProperties.vPos.y;
        node.position.z = objProperties.vPos.z;
        
        node.scale.setScalar(fScale);
        
        parentNode.add(node);
        return node;
        return null;
    }

    createThreeSceneObject_CadFlaeche(parentNode, objProperties) {
        parentNode = parentNode || this.m_rootNode;
        let opacity = 1.0;
        let color = 0xff0000;
        if(!("coords" in objProperties)) return null;
        const coords = [];
        for(let i = 0; i < objProperties.coords.length ; i++) {
            let coord = objProperties.coords[i];
            coords.push(coord.x);
            coords.push(coord.y);
            coords.push(coord.z);
        }
        if("color" in objProperties) {
            let tplC = this.kkpColorToColor(objProperties.color);
            color = tplC[0];
            opacity = tplC[1] / 255.0;
        }

        const vertices = new Float32Array(coords);
        let geometry = new THREE.BufferGeometry();
        geometry.setAttribute("position" , new THREE.BufferAttribute(vertices , 3));
        geometry.computeVertexNormals();
        let material = new THREE.MeshPhongMaterial({color: color, fog: false, specular: 0xc4c4c4 , shininess:69, transparent: true, opacity: opacity , side:THREE.DoubleSide});
        let node = new THREE.Mesh(geometry , material);
        node.castShadow = true;
        node.receiveShadow = true;
        parentNode.add(node);
        return node;
        return null;
    }

    createThreeSceneObject_CadFLinie(parentNode , objProperties) {
        parentNode = parentNode || this.m_rootNode;
        let opacity = 1.0;
        let color = 0xff0000;
        if("color" in objProperties) {
            let tplC = this.kkpColorToColor(objProperties.color);
            color = tplC[0];
            opacity = tplC[1] / 255.0;
        }
        if(!("coords" in objProperties)) return null;
        if(objProperties.coords.length < 2) return null;
        const points = [];
        let offset = null;
        for(let i = 0; i < objProperties.coords.length ; i++) {
            let coord = objProperties.coords[i];
            offset = objProperties.coords[0];
            coord = {x: coord.x - offset.x , y: coord.y - offset.y , z: coord.z - offset.z }
            points.push(new THREE.Vector3(coord.x , coord.y, coord.z));
        }
        const geometry = new THREE.BufferGeometry().setFromPoints(points);
        let material = new THREE.LineBasicMaterial({color: color});
        let node = new THREE.Line(geometry , material);
        if(offset) node.position.set(offset.x , offset.y , offset.z);
        node.castShadow = true;
        node.receiveShadow = true;
        //parentNode.add(node);
        //return node;
        return null;
    }

    createLine(vA , vB) {
        if(!vA) return;
        if(!vB) return;
        const points = [vA , vB];
        const geometry = new THREE.BufferGeometry().setFromPoints(points);
        let material = new THREE.LineBasicMaterial({color: 0xff0201});
        let node = new THREE.Line(geometry , material);
        return node;
    }

    createText(text , size = 0.01 /* m */ , color = 0x0) {
        if(!this.m_font) return null;
        let textGeom = new TextGeometry(text , {
            font: this.m_font,
            size: size,
            height: 0.001,
            curveSegments:10,
            bevelThickness:0,
            bevelSize:0,
            bevelEnabled:false
        });
        textGeom.computeBoundingBox();
        let material = new THREE.MeshBasicMaterial({color: color, side:THREE.DoubleSide});
        let node = new THREE.Mesh(textGeom , material);
        node.castShadow = true;
        node.receiveShadow = true;
        return node;
    }

And as said, in general this works fine. But when it comes to a real project to load, I have to load about 3500 Spheres, 1500 Lines, and about 2500 thousend shapes.

By animation scene is quite simple

animateScene() {
        let self = this;
        requestAnimationFrame( () => {self.animateScene(); } );

        self.m_controls.update();
        //self.m_renderer.render( self.m_scene, self.m_camera );
        this.m_composer.render();
    }

I use outline effects, that’ s why I render through a composer. Also I do use a raycaster that works on mouse move event. I haven’t tried to switch off raycasting, to see performance results, Or only raycast on mouse-click. I don’t know if that makes THE big difference.

What I tried to far…
Only create spheres, set them to invisible. Don’t create anything else. Performance is good. Memory gets used as expected.
Creating spheres and add them visible to the scene. Don’t create anything else. I get around 4-10 fps.

Creating only lines, resolves in the same performance issues.
Creating only shapes resolves in the same performance issues.

Further more I found out when keeping the amount of meshes below something around 2000 - 2500 object, I get 120 fps. Up to 3500 objects I still get up to 60 fps but when I try to move the scene (orbit controls) the frame rate drops to something around 20 fps.
If I have around 5500 objects in my scene the fps is a desaster of a maximum of 10 fps and when I try to move the scene an release the mouse, the scene takes more than 20 seconds to calm down.

I can try instancing all the spheres since there are more less the sames, except position, color and eventually radius.
Lines can (maybe?) get instanced as well.
But the shapes are set from individual points. I can try to merge them, but only if I can find a way to still select them by the raycaster.
What really annoys me, the application the data come from uses OpenGL1.1 using a mesa driver (software renderer). And that application laughs about the “poor performance” three.js seems to deliver, though I am sure, three.js indeed is a very powefull engine.

So yeah, I hope you might give me some ideas, what I could try to change to keep the fps going.

Just in case this helps as well, here are some screenshots.

Ignoring the floor-grid this scene contains 5544 objects and the tab uses 219MB RAM.

This object is almost unhable, and it doesn’t even contain 25% of a standard object.

For things like spheres - just use LODs and sprites. As soon as the sphere is not-close-up-to-the-camera, there’s almost no difference in rendering sphere vs rendering a circle with an averaged sphere color.

2 Likes

Uhh a new tool. I didn’t kow of LODs. I am reading the the documentation right now.
And using sprites, Yeah I thought so as well, since the “points” are the most important thing to be clickable by the user. So I am also looking for something to make these points become size-independ from the distance to the camera. And maybe sprites could help me there.

But even if I completly avoid creating points or lines, the I get poor fps.

createThreeSceneObject_CadPunkt(parentNode, objProperties) {
        return null;
}
createThreeSceneObject_CadFLinie(parentNode , objProperties) {
        return null;
}


These are 990 objects only shapes. Performance seems just fine.


These are 7909 objects including points, lines, and shapes.

And its almost impossible to even show the results when I wanted to load this version

Using InstancedMesh would be reasonable enough, too. But 3500 meshes is too many draw calls, with one per mesh.

Aim for something like <100 draw calls and <100,000 vertices if you can. Using InstancedMesh will reduce the draw calls to only one draw for all instances, but does not reduce the vertex count compared to drawing spheres individually. Using sprites or points will reduce the vertex count compared to spheres. You could use some combination of those, if needed.

2 Likes

Hey, thanks for the advice.
I set up more debugging information to know more about my imported object.
So I get 11FPS, with 100MB memory consumption.
I have 8350 render calls!
4554 spheres
2365 lines
784 custom shapes
and 206 groups

Currently I am thinking if I could merge things. I guess instancing the points should be the best solution. The objects are arranged in a tree, and I only have to “handle” 2 objects from the perspective of the user. The current node in the tree containing everything in it, and the rest.
So maybe merging the whole scene into 2 objects might also work, even if the memory consumtion might be higher. But I am devolping this project for desktop pc’s, as they have bigger screen. So I may assume they can make use of more memory…

Counting my objects is also the first step on creating instanced meshes.
Since I figured out, that I am using around 2500 lines, does anyone know if they could get instanced as well, even though they have different coordinates? Just in case this might be relevant, I don’t use polylines, only 2-Point-lines.
But before I do anything else, I will get myself a big cup of coffee.

Thanks to everyone who already helped me.

I just instanced my points for now and reduced the draw calls from ~8000 to ~3900 calls.
Still bad, I know, but now I have around 35 fps, and the scene works already smoothly. I would never have thought, this would make SUCH a big difference, as the amount of vertices doesn’t change at all.

So now I will focus on reducing the draw-calls as this really seems to be THE problem.

Thanks for alle help. (Now I need to find out where to mark this discussion as solved…)

Note that individual points and lines do not need to be instanced, just merged into a larger geometry. Similar to how a Mesh is a large collection of triangles, one geometry can also hold many points or lines without instancing.

Usually instancing comes into play when you want to draw the same collection of many triangles, points, or lines many times.

Just as an update information.
I changed points and shapes so, that they all points become 1 mesh with a buffergeometry set from coords. The lines are on my todo.

The framerate has changes significantly. Even under “stress” I have my 144fps.
And (and I really don’t know why) the memory usage dropped from 291MB to 15MB.

So just in any case someone might be interessted in how I created these buffergeometries.

I have a list of JSON-formatted objects, each containing information about the position, the color and (in case of points) the pointSize.

createScenePointsMesh(srvPoints) {
        let positions = [];
        let colors = [];
        let sizes = [];
        for(let i = 0 ; i  < srvPoints.length ; i++) {
            let srvInfo = srvPoints[i];
            let ca = this.converColor(srvInfo.color);
            let c = new THREE.Color(ca[0]);
            let alpha = c / 255.0;
            positions.push(srvInfo.vPos.x * 1000.0 , srvInfo.vPos.y * 1000.0, srvInfo.vPos.z * 1000.0);
            colors.push(c.r , c.g , c.b , alpha);
            sizes.push(srvInfo.ptSize);
        }

        let geometry = new THREE.BufferGeometry();
        geometry.setAttribute("position" , new THREE.Float32BufferAttribute(positions , 3));
        geometry.setAttribute("color" , new THREE.Float32BufferAttribute(colors , 4));
        geometry.setAttribute("size" , new THREE.Float32BufferAttribute())
        geometry.computeBoundingBox();
        let material = new THREE.PointsMaterial({vertexColors : true });
        let mesh = new THREE.Points(geometry , material);
        mesh.name = "points";
        mesh.layers.set(LAYER.OBJECT_IS_VISIBLE);
        this.m_scene.add(mesh);
    }

    createSceneShapesMesh(srvShapes) {
        let positions = [];
        let colors = [];

        for(let i = 0 ; i  < srvShapes.length ; i++) {
            let srvInfo = srvShapes[i];
            let ca = this.converColor(srvInfo.color);
            let c = new THREE.Color(ca[0]);
            let alpha = ca[1] / 255.0;
            for(let j = 0 ; j < srvInfo.coords.length ; j++) {
                let coord = srvInfo.coords[j];
                positions.push(coord.x * 1000.0 , coord.y * 1000.0 , coord.z * 1000.0);
                colors.push(c.r , c.g , c.b , alpha);
            }
        }

        let geometry = new THREE.BufferGeometry();
        geometry.setAttribute("position" , new THREE.Float32BufferAttribute(positions , 3));
        geometry.setAttribute("color" , new THREE.Float32BufferAttribute(colors , 4));
        geometry.computeVertexNormals();
        geometry.computeBoundingBox();
        let material = new THREE.MeshPhongMaterial({specular: 0xc4c4c4 , shininess:69, transparent: true, side:THREE.DoubleSide , vertexColors : true});
        let mesh = new THREE.Mesh(geometry , material);
        mesh.name = "shapes";
        mesh.layers.set(LAYER.OBJECT_IS_VISIBLE);
        this.m_scene.add(mesh);
    }

I also “merged” all the lines of the floor grid into one single mesh by the same technique. (The lines in the construction are still in “TODO”)

createGrid(width , height , gridSize = 1000 , color = 0x0000ff , colorX = 0xffffff) {
        const positions = [];
        const normals = [];
        const colors = [];
        const colorA = new THREE.Color(color);
        const colorB = new THREE.Color(colorX);
        let n = 10;
        for(let i = 0 ; i <= width ; i+= gridSize) {
            positions.push( i , height , 0);
            positions.push(i , 0 , 0);
            normals.push(0 , 1 , 0);
            normals.push(0 , 1 , 0);
            let r = n % 10;
            n++;
            if(r == 0) {
                colors.push(colorB.r , colorB.g , colorB.b);
                colors.push(colorB.r , colorB.g , colorB.b);
            }else {
                colors.push(colorA.r , colorA.g , colorA.b);
                colors.push(colorA.r , colorA.g , colorA.b);
            }
        }
        n = 10;
        for(let i = 0 ; i <= height ; i+= gridSize) {
            positions.push(0 , i , 0);
            positions.push(width , i , 0);
            normals.push(0 , 1 , 0);
            normals.push(0 , 1 , 0);
            let r = n % 10;
            n++;
            if(r == 0) {
                colors.push(colorB.r , colorB.g , colorB.b);
                colors.push(colorB.r , colorB.g , colorB.b);
            }else {
                colors.push(colorA.r , colorA.g , colorA.b);
                colors.push(colorA.r , colorA.g , colorA.b);
            }
        }
        let geometry = new THREE.BufferGeometry();
        let material = new THREE.LineBasicMaterial({vertexColors : true });
        geometry.setAttribute('position' , new THREE.Float32BufferAttribute(positions , 3));
        geometry.setAttribute("normal" , new THREE.Float32BufferAttribute(normals , 3));
        geometry.setAttribute('color' , new THREE.Float32BufferAttribute(colors , 3));
        let line = new THREE.LineSegments(geometry , material);
        return line;
    }

It’s not the most elegant code, but I can read and understand its contents.

So in any case someone else is struggeling the issue of “bad performance” with three js, reducing draw calls by creating “merged” objects is an absolute game changer.

Once again, thanks for all the advices you gave to me.

1 Like