Rending very large (20-40k) sprite grid... ideas for optimizations?

I’m working on an “infinite” minesweeper project right now I’m making a sprite for each cell.

I’m using different uv coordinates per cell type (single texture atlas).
Since THREE Sprite class makes all instance share the same geometry, and since UV are stored under geometry.attributes.uv I’m making a different geometry per cell to be able to use sprite.geometry.attributes.uv.setXY() individually.

Sprite of a Grid instance to show a chunk of the board:

	for (var y = 0; y < this.h; y++) {
	    for (var x = 0; x < this.w; x++) {
		var sprite = new THREE.Sprite(material);
		var cell = get_cell_at(this.originX+x, this.originY+y)
		sprite.geometry = new THREE.PlaneBufferGeometry() = new THREE.Vector2(0,0)
		sprite.geometry.attributes.uv.setXY(0, get_uv(cell, 0, COOR_X), get_uv(cell, 0, COOR_Y))
		sprite.geometry.attributes.uv.setXY(1, get_uv(cell, 1, COOR_X), get_uv(cell, 1, COOR_Y))
		sprite.geometry.attributes.uv.setXY(3, get_uv(cell, 2, COOR_X), get_uv(cell, 2, COOR_Y))
		sprite.geometry.attributes.uv.setXY(2, get_uv(cell, 3, COOR_X), get_uv(cell, 3, COOR_Y))
		sprite.geometry.attributes.uv.needsUpdate = true;
		sprite.position.x = this.originX + x
		sprite.position.y = this.originY + y
		sprite.position.z = 0

And orthographic camera moves around and triggers the mapping/unmapping of chunks:


In red is the camera view moving around the board, in blue are the aligned chunks. At zoom = 1, the size of the camera view is the same as the chunk so I just need 4 chunks loaded at any time.

When a chunk goes out of view and a new one enters it, I just “move” and reset the old one like so:

move_to(originX, originY) {
	this.originX = originX
	this.originY = originY
	for (var y = 0; y < this.h; y++) {
	    for (var x = 0; x < this.w; x++) {
		    const i = y*this.w + x
		    var s = this.sprites[i]
		    s.position.x = this.originX+x
		    s.position.y = this.originY+y

		    var cell = get_cell_at(this.originX+x, this.originY+y)
		    s.geometry.attributes.uv.setXY(0, get_uv(cell, 0, COOR_X), get_uv(cell, 0, COOR_Y))
		    s.geometry.attributes.uv.setXY(1, get_uv(cell, 1, COOR_X), get_uv(cell, 1, COOR_Y))
		    s.geometry.attributes.uv.setXY(3, get_uv(cell, 2, COOR_X), get_uv(cell, 2, COOR_Y))
		    s.geometry.attributes.uv.setXY(2, get_uv(cell, 3, COOR_X), get_uv(cell, 3, COOR_Y))
		    s.geometry.attributes.uv.needsUpdate = true;

Although this all works, it’s not as fast and smooth as I’d like it to be. Especially as you zoom out and more chunks are needed to fill the view (red rectange >> blue ones).

How would you optimize this?

  • a) I could cache a chunk as 1 texture (is there a simple way to create texture from my three.js grid? or do I need to use some js image creation lib)
  • b) instanciante all cells to make it use different position and uv attributes. is this possible with three.js?
  • c) have 1 geometry for a complete grid chunk… but for a vertex shared by multiple cells I need different UV depending on which cell I’m filling… is this impossible?
  • any other ideas? what seems better to you, and are there any profiling tools for firefox/chrome/three.js for webgl

My current prototype is here
You can zoom & pan with the mouse (chunk loading at zoom < 1 is not fully implemented yet)

Yes, but actually you could also use points which are a geometry with just a position attribute. Points are actually quads which can be used as rectangles too if needed, but otherwise you can also use just a instanced quad/rectangle as well with InstancedMesh which is quite easy to use, and there is no issue in rendering millions then.

More Points:

Edit: looking at your demo you could also only use 1 quad for each chunk and store those sprites in a texture where each texel has the ID of the corresponding sprite, this is a bit more complex though, if there aren’t millions you should be fine using points or instances.

1 Like