Reputation: 71
There is a three.js scene with some 3D objects and 200 - 300 small text labels (< 10% are visible to the camera at one perspective). Adding the text sprites decreased the FPS from 60 to 30 - 40 and its also very memory consuming.
Is there a way to make the sprites faster? I read about caching material, but labels are all unique - so this isn't possible.
Test: https://jsfiddle.net/h9sub275/4/ (You can change SPRITE_COUNT to see an FPS drop on your machine)
Edit 1: Setting the canvas size to the text bounds will decrease memory consumption, but not improve the FPS.
var Test = {
SPRITE_COUNT : 700,
init : function() {
this.renderer = new THREE.WebGLRenderer({antialias : true}); // false, a bit faster without antialias
this.renderer.setPixelRatio(window.devicePixelRatio);
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.container = document.getElementById('display');
this.container.appendChild(this.renderer.domElement);
this.camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 1000);
this.scene = new THREE.Scene();
this.group = new THREE.Object3D();
this.scene.add(this.group);
for (var i = 0; i < this.SPRITE_COUNT; i++) {
var sprite = this.makeTextSprite('label ' + i, 24);
sprite.position.set(Math.random() * 20 - 10, Math.random() * 20 - 10, Math.random() * 20 - 10);
this.group.add(sprite);
}
this.stats = new Stats();
this.stats.domElement.style.position = 'absolute';
this.stats.domElement.style.left = '0px';
this.stats.domElement.style.top = '0px';
document.body.appendChild(this.stats.domElement);
this.render();
},
render : function() {
var self = this;
this.camera.rotation.x += 0.002;
this.renderer.render(this.scene, this.camera);
this.stats.update();
requestAnimationFrame(function() {self.render();});
},
makeTextSprite : function(message, fontsize) {
var ctx, texture, sprite, spriteMaterial,
canvas = document.createElement('canvas');
ctx = canvas.getContext('2d');
ctx.font = fontsize + "px Arial";
// setting canvas width/height before ctx draw, else canvas is empty
canvas.width = ctx.measureText(message).width;
canvas.height = fontsize * 2; // fontsize * 1.5
// after setting the canvas width/height we have to re-set font to apply!?! looks like ctx reset
ctx.font = fontsize + "px Arial";
ctx.fillStyle = "rgba(255,0,0,1)";
ctx.fillText(message, 0, fontsize);
texture = new THREE.Texture(canvas);
texture.minFilter = THREE.LinearFilter; // NearestFilter;
texture.needsUpdate = true;
spriteMaterial = new THREE.SpriteMaterial({map : texture});
sprite = new THREE.Sprite(spriteMaterial);
return sprite;
}
};
window.onload = function() {Test.init();};
Upvotes: 1
Views: 5650
Reputation: 3837
You are correct, three.js is indeed causing the GPU to use a lot of texture memory this way. Keep in mind that every texture sent to the GPU must be as high as it is wide, so you'll be wasting a lot of memory by making a single canvas for each sprite.
A challenge here is the three.js design choice to have a single set of UV coordinates on a Texture
; even when you combine the label sprites in a single texture map and you .clone()
your texture for each material without some extra effort, it'll still send each Texture
to the GPU without sharing the memory. In short, it currently has no documented way of telling it these textures are the same, and you can't point each Material
at the same Texture
as it's not the Material
which keeps the UVs. https://github.com/mrdoob/three.js/issues/5821 discusses that problem.
I've worked around these issues by combining my sprites in one or more texture maps. For this I created a "sprite texture atlas manager" which manages the allocation of sprite textures as I need them, and I ported a knapsack algorithm to JS which helps me to (mostly) fill up these texture maps with my labels so not a lot of memory is wasted.
I've extracted my code for this out in a separate library, and it is available here: https://github.com/Leeft/three-sprite-texture-atlas-manager with a live example (which does not yet use sprites, but that should be easy to add) at http://jsfiddle.net/Shiari/sbda72k9/.
Fortunately, while this is not documented yet, I also found that it's quite easy now in recent versions (r73 at least, perhaps r72 as well) to force the textures to share the GPU memory by making sure they all have the same .uuid
value. My library makes sure to make use of this, and in my testing so far (with 2048x2048 sprite maps; I only need two of those at the size that I'm rendering) that this brings GPU memory down from ~2.6GB when not shared to ~300-600MB when shared. (2048px is far too large when you're only placing a single label there though, and reducing the texture size helps greatly when the maps are not shared).
Lastly, as per your own answer, drawcalls and culling are also a performance issue in r73. I never hit that problem though, since I was already batching my draw calls by grouping everything.
Upvotes: 5
Reputation: 71
This was a bug in the three.js WebGLRenderer (missing view frustum check for sprites in <= r73). It is already fixed in the dev branch. So you can expect it to be available in r74.
Issue and Details https://github.com/mrdoob/three.js/issues/7371
Fixed version with latest dev build: https://jsfiddle.net/h9sub275/9/
Performance Test with r73: https://jsfiddle.net/h9sub275/7/
(Click to see the performance difference between manually removing invisible sprites and not removing them)
Latest Dev Build:
<script src="https://rawgit.com/mrdoob/three.js/dev/build/three.js"> </script>
Upvotes: 4