Performance Optimization Tips for Lots of Spheres (but a bit complicated)?

Hi there, I’m having trouble optimizing the “overhead cost” of a scene with lots of spheres in it, could you guys give some advises?

So I have this scene with lots of spheres, like below (one unit cell) :

and it has lots of periodic replicates, so it really should like like this (multiple unit cells):

You can probably tell it’s something related to chemistry :slight_smile:

To optimize the performance, the scene itself is a giant buffergeoemtry, and the periodic replicates are handled by instancing. the rendering performance is phenomenal, but the problem I’m having is that preparing this geometry (~6000 spheres per unit cell) takes ~300ms on my laptop. since similar scenes sometimes has to be created and played sequentially as a movie, I would really appreciate if we could optimize this overhead step hopefully down to <50ms level

I have created a live example here: (it’s working now), the overhead performance is printed to the console

more specifically, the spheres are prepared as:

var sphereTemplate = new THREE.SphereBufferGeometry(1, 12,12);
for (var i = 0; i < moleculeData.length; i++) {

    var atomData = moleculeData[i];
        atomList.push(sphereTemplate.clone().scale(atomSize, atomSize, atomSize).translate(atomData.x, atomData.y,atomData.z));

    var tempColor = new THREE.Color( color );
    atomColorList.push([tempColor.r, tempColor.g, tempColor.b]);
var atomsGeometry = combineGeometry(atomList, atomColorList);
var material = getMoleculeMaterialInstanced(options); // getting customized material for instancing
var offsetResult = getOffsetArray(systemDimension, latticeVectors, options); //getting offset array 
atomsGeometry.setAttribute('offset', new THREE.InstancedBufferAttribute(offsetResult.sumDisplacement, 3 ));

var atoms = new THREE.Mesh( atomsGeometry, material);


function combineGeometry(geoarray, colorarray) {
    let posArrLength = 0;
    let normArrLength = 0;
    let uvArrLength = 0;
    let indexArrLength = 0;
    geoarray.forEach(geometry => {
        posArrLength += geometry.attributes.position.count * 3;
        normArrLength += geometry.attributes.normal.count * 3;
        uvArrLength += geometry.attributes.uv.count * 2;
        indexArrLength += geometry.index.count;

    const sumPosArr = new Float32Array(posArrLength);
    const sumColorArr = new Float32Array(posArrLength);
    const sumNormArr = new Float32Array(normArrLength);
    const sumUvArr = new Float32Array(uvArrLength);
    const sumIndexArr = new Uint32Array(indexArrLength);

    const postotalarr = [];
    let sumPosCursor = 0;
    let sumNormCursor = 0;
    let sumUvCursor = 0;
    let sumIndexCursor = 0;
    let sumIndexCursor2 = 0;
    for (let a = 0; a < geoarray.length; a++ ) {
        const posAttArr = geoarray[a].getAttribute('position').array;
        for (let b = 0; b < posAttArr.length; b++) {
            sumPosArr[b + sumPosCursor] = posAttArr[b];
            sumColorArr[b + sumPosCursor] = colorarray[a][b % 3];

        sumPosCursor += posAttArr.length;
        const numAttArr = geoarray[a].getAttribute('normal').array;
        for (let b = 0; b < numAttArr.length; b++) {
            sumNormArr[b + sumNormCursor] = numAttArr[b];

        sumNormCursor += numAttArr.length;
        const uvAttArr = geoarray[a].getAttribute('uv').array;
        for (let b = 0; b < uvAttArr.length; b++) {
            sumUvArr[b + sumUvCursor] = uvAttArr[b];

        sumUvCursor += uvAttArr.length;
        const indexArr = geoarray[a].index.array;
        for (let b = 0; b < indexArr.length; b++) {
            sumIndexArr[b + sumIndexCursor] = indexArr[b] + sumIndexCursor2;

        sumIndexCursor += indexArr.length;
        sumIndexCursor2 += posAttArr.length / 3;

    const combinedGeometry = new THREE.InstancedBufferGeometry();
    combinedGeometry.setAttribute('position', new THREE.BufferAttribute(sumPosArr, 3 ));
    combinedGeometry.setAttribute('normal', new THREE.BufferAttribute(sumNormArr, 3 ));
    combinedGeometry.setAttribute('uv', new THREE.BufferAttribute(sumUvArr, 2 ));
    combinedGeometry.setAttribute('color', new THREE.BufferAttribute(sumColorArr, 3 ));
    combinedGeometry.setIndex(new THREE.BufferAttribute(sumIndexArr, 1));
    return combinedGeometry;

based on my observation, preparing the list of sphereBufferGeometry takes ~150 - 200ms, and combining the geometry takes ~ 100 - 150ms. Do you guys have any suggestions to improve this performance?

PS: I’ve also considered instancing the ~6000 spheres in one unit cell, but since there are ~10-1000 periodic replicates, the instanced attribute array would be super long, unless there are ways of two-level instancing…?

I have an idea: Could you make the individual atoms (spheres) the instances, instead of the periodic replicates? I think the build of the instances would be much quicker: No merging geometries, just set atoms positions, scale and color.

BTW, cool project.

1 Like

@lxiangyun93 Just out of curiousity, why did you decide to use spheres instead of textured points?

I took this example as a base, and modified it a bit: Instancing Point Cloud

@yombo Thanks!
yes, I have thought about that too, I thought the instanced attributes would be too long, unless we could do multiple levels of instancing

for example, currently each buffer attributes (vertex position, vertex color ) is ~6000 * 169 long, and the instanced attribute (offset) are ~500 long
and if we instance each atom instead of unit cell, the buffer attribute needs (vertex position, color) to be ~169, and the instanced attributes (scale, offset, color) would need to be ~6000 * 500 long

as I was typing, I realizes it’s probably not so bad, maybe I should give it a try :slight_smile:

I’m still curious though, is it possible to “instance” it once to get the atoms in one unit cell, and “second level instancing” to get the periodic replicates?

@prisoner849 yep, you are absolutely correct, for better performance, I did create a point cloud version of it, with lots of your help actually :grin: (Display sprites correctly with customized Points material?, Instancing Point Cloud)

It’s a great alternative if there are more atoms, However, people still prefers actual spheres and it would be great if we could make this work as well. Again, the rendering performance with actual sphere is great, it’s only the overhead that’s too costly currently

I did a quick test, showing sphereTemplate.clone() along takes ~ 70-100 ms out of the 300ms total time, maybe we could bypass it somehow? or maybe the way I combined the buffergeometries were not ideal?

I don’t think that’s possible. But it doesn’t matter, I’m sure you can do several hundred million sphere instances :slight_smile:


1 Like

Just tried it to make sure that it doesn’t work :smile:
Or maybe I did something wrong :smiley:

Also, there’s an interesting topic about spheres: Coding jam: Pixel-perfect spheres without high-res geometry