Is it possible to have a generic "buffergeometry to instancedbuffergeometry" converter?

Being quite the newbie regarding Threejs and just starting to get into the harder stuff, I absolutely cannot figure out a way to go from a shape (or a simple Vector2[]) to an instancedbuffergeometry.

From what I’ve read, it seems unavoidable to write the shaders for it, however my knowledge of how that works is Very limited. I need to draw about 1M instances with “acceptable” framerate. So far I’ve been trying the mix of InstancedMesh + ShapeBufferGeometry + InstancedBufferAttribute, which gives me Ok results for up to ~100k instances, with non-random position and color for each instance. The lag from there on just gets unbearable. I’ve been able to run other demos of 1 to 5M instances with about the same lag as my own 100k instances.

So, my question is, is there a converter for (or a tutorial on using) instancedbuffergeometry with non-trivial shapes ?

  1. If you are a newbie shaders are a big no-no. Don’t do this to yourself. :slightly_smiling_face:
  2. Without code it’s hard to understand what exactly is happening with the 1M instances you are creating, but if you are updating them, or generating during the rendering, try using Offscreen Canvas
  3. If the geometry is non-trivial in a way that it is… well, complex models, then regardless of optimisation you are unlikely to get a reasonable performance in WebGL (I can’t find any real number to tell, but this demo with just 10k instances already drops to 20fps on a regular MBP.)
  1. Haha maybe you’re right. However I really need to squeeze out as much performance as possible.

  2. Offscreen canvas is not a possitbility for me because of the browser compatibility issues. Here’s a little snippet of what I’m doing:

     const _color = useMemo(() => new THREE.Color(), [])
     const _position = useMemo(() => new Vector3(), [])
     const _object = useMemo(() => new THREE.Object3D(), [])
    
     const [arrows, setArrows] = useState<{ x: number, y: number }[]>([])
     const [needsRepaint, setNeedsRepaint] = useState(true)
    
     let mesh = useRef<InstancedMesh | null>(null)
    
     const update = useCallback(nbArrows => {
    
         _object.matrixAutoUpdate = false
    
         let newGeometry = new InstancedBufferGeometry().setFromPoints(ARROW_SHAPE3)
         newGeometry.setIndex(ARROW_INDICES)
    
         // Initialize colors array
         const colors = new Array(nbArrows).fill(0).map((_, i) =>
             ARROW_COLORS[i % ARROW_COLORS.length]
         )
         const colorsArray = new Float32Array(nbArrows * 3)
    
         // Initialize positions array
         const positions = new Array(nbArrows).fill(0).map((_, i) =>
             [Math.floor(i % 4) - 1.5, i * 0.000005, 0.]
         )
         const positionsArray = new Float32Array(nbArrows * 3)
    
         // Fill arrays
         for (let i = 0, len = nbArrows; i < len; i++) {
             _color.set(colors[i])
             _color.toArray(colorsArray, i * 3)
    
             const pos = positions[i]
             _position.set(pos[0], pos[1], pos[2])
             _position.toArray(positionsArray, i * 3)
         }
    
         // Create the attributes
         let newColorAttrib = new InstancedBufferAttribute(colorsArray, 3, false)
         newGeometry.setAttribute('color', newColorAttrib)
    
         let newPositionAttrib = new InstancedBufferAttribute(positionsArray, 3, false)
         newGeometry.setAttribute('position', newPositionAttrib)
    
         let newMaterial = new MeshBasicMaterial()
         newMaterial.vertexColors = true
         newMaterial.depthTest = false
         newMaterial.fog = false
    
         let newMesh = new InstancedMesh(newGeometry, newMaterial, nbArrows)
         newMesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage)
    
         const parentNode = mesh.current.parent
         parentNode.remove(mesh.current)
         parentNode.add(newMesh)
    
         // Update mesh
         const temp = []
         for (let i = 0; i < nbArrows; i++) {
             temp.push({ x: someX, y: someY })
         }
         setArrows(temp)
    
         mesh.current = newMesh
     }, [])
    
     useEffect(() => {
         update(props.count)
     }, [props.count])
    
     useFrame(({camera}) => {
    
         camera.position.y = someValue
         camera.updateProjectionMatrix()
    
         if (needsRepaint) {
             for (let i = 0, len = arrows.length; i < len; i++) {
    
                 const { x, y } = arrows[i]
    
                 // Update the dummy object
                 _object.position.set(x, y, 0)
                 _object.updateMatrix()
                 // And apply the matrix to the instanced item
                 mesh.current.setMatrixAt(i, _object.matrix)
             }
    
             mesh.current.instanceMatrix.needsUpdate = true
             setNeedsRepaint(false)
         }
     })
    
  3. Sorry, I used “non-trivial” in a confusing way. Although the demo you linked is included in what I meant, I’m focusing on shapes with like ~10’ish vertices, just not the default things like Box, Cylinder, etc. As you can see in the code snippet, mine are arrows made with a few points and indices.

** The code isn’t fully functional, but I hope it displays the intention well enough. In that snippet, the 'position' InstancedBufferAttribute is the latest thing I’ve tried without success, to avoid the whole for loop that updates the positions.

I’m not sure why InstancedMesh would perform any differently than InstancedBufferGeometry, the technique is essentially the same. Is that what you’re seeing? Something else may be going on there, perhaps you have a cheaper material assigned to the faster case?

A useful converter (I think) would be something like:

const instancedMesh = InstancedMesh.fromMesh( normalMesh );
1 Like

The thing is I’m very likely to be stuck in a XY problem where I think I’m actually focusing on the right things to optimize but I’m simply off the tracks.

I’ve read many examples on the ways to draw a lot of objects (in my case, at least 100k, preferably 1M), and chances are I’m getting lost in the pool of techniques. Here’s a brief explanation of my situation:

  1. I have a 2D shape made of 9 points
  2. I have 2 arrays: colors and positions, both of size NB_INSTANCES * 3
  3. The instances’ position should be updated every frame according to time
  4. I must be able to add or remove any specific instance

Anyway, this is already off-topic for the title I gave this thread, but I’m hoping someone can guide me a little, as I haven’t been able to do 100k instances without noticeable lag after many days/weeks of reading/coding.

Apologies if you’ve already done this, but the first thing I’d do is to run a performance profile in Chrome/Firefox dev tools and see if the bottleneck is CPU-side or GPU-side. Based on the number of vertices you’re describing I would venture a guess that it will be mostly CPU, and it’ll point you to the specific hot code to focus on.

My hunch would be that the bottleneck is the matrix composition occurring for all objects every frame. If it’s just positions changing, you may be able to avoid full matrix composition and just copy x/y directly into their positions in the instanceMatrix (indices 12 and 13 I believe?)