Inconsistent sprite scaling

I’m having a problem with sprite scaling - I get really inconsistent results for the sprite size.

I’m using sprites to put nameplates above the character’s heads. I create a canvas element, CanvasTexture, SpriteMaterial, and Sprite, and then use measureText to determine how big the canvas needs to be to contain the character’s name:

    let ctx = this.canvas.getContext('2d')!;

    ctx.font = NAMEPLATE_FONT;
    const textSize = ctx.measureText(name);
    const width = Math.max(
      GAUGE_WIDTH + 4,
      Math.ceil(textSize.width) + NAMEPLATE_OUTLINE_WIDTH * 2
    );
    const height = Math.ceil(
      textSize.actualBoundingBoxAscent +
        textSize.actualBoundingBoxDescent +
        GAUGE_HEIGHT +
        GAUGE_TOP_MARGIN +
        NAMEPLATE_OUTLINE_WIDTH * 2
    );

    this.canvas.width = width;
    this.canvas.height = height;

Then I create a new drawing context and render the text into the canvas:

    const x = Math.round((width - textSize.width) * 0.5);
    const y = textSize.actualBoundingBoxAscent + NAMEPLATE_OUTLINE_WIDTH;
    if (showName) {
      ctx.font = NAMEPLATE_FONT;
      ctx.lineWidth = NAMEPLATE_OUTLINE_WIDTH * 2;
      ctx.strokeStyle = '#000';
      ctx.strokeText(name, x, y);
      ctx.fillStyle = '#fff';
      ctx.fillText(name, x, y);
    }

Finally, I set the scale of the sprite based on the aspect ratio of the canvas:

    this.sprite.scale.set((width / height) * 0.05, 0.05, 1);
    this.sprite.center.set(0.5, 0);
    this.sprite.visible = true;

However, what I am seeing is:

  • Sometimes the sprites look correct.
  • Sometimes they are way too small.
  • Sometimes they are stretched or squashed (incorrect aspect ratio).
  • Sometimes they are not centered properly above the character’s heads (offset horizontally).

Note that in some cases, even just doing a page refresh - without changing any code - will cause the sprite to look different. I’ve also seen cases where two characters are standing next to each other, and one has a tiny nameplate while the other has a normal-sized nameplate.

BTW I have considered having the nameplates be rendered as an HTML overlay instead of sprites in the 3D scene, however the problem there is jitter - because the DOM doesn’t update at the same frequency as the 3D view. When I tried it, it looked pretty bad when the camera was moving.

If the result changes when you refresh the page, then the issue probably isn’t with the code you shared.
As an example, it might be something to do with the font loading. If you draw your text before the font is loaded, it might be rendered with a different font. You can try to wait for font load and see if that give syou consistent results, this packages i quite useful: fontfaceobserver - npm

But in general, using canvas sprites for text has never given me satisfactory enough results.

I would either:

  • go back to using HTML labels. If there’s jitter, the problem is not with the rate at which the DOM updates, it’s with your code (check out the THREE.js CSSRenderer, three.js docs, smooth as butter ). When I’ve had issues syncing DOM and WebGL before, I spent months looking for the issue before realising that I was rendering the 3D scene first in the render loop, before updating the HTML labels, so they always were a frame behind.

-the other option is to use a better text rendering method, generating signed distance fields so you can get super sharp looking text which will look much better than canvas sprites. This library does it all: troika/packages/troika-three-text at master · protectwise/troika · GitHub

2 Likes

I will take your suggestion and do some experiments.

Note that I am also using the same canvas to render the character’s health gauge and other 2d status indicators - if I was to adopt Troika I would need to come up with an analogous solution for those as well. Any suggestions?

OK that worked pretty well - the key was using translate instead of setting left - I should have remembered this, translate is always faster :slight_smile:

Thanks!