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 ?
If you are a newbie shaders are a big no-no. Don’t do this to yourself.
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
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.)
Haha maybe you’re right. However I really need to squeeze out as much performance as possible.
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)
}
})
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:
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:
I have a 2D shape made of 9 points
I have 2 arrays: colors and positions, both of size NB_INSTANCES * 3
The instances’ position should be updated every frame according to time
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?)