Sprites are scaled when canvas is very large

I have a situation where I am rendering to a very large canvas. Maybe up to two-to-three 4K in some applications. Generally everything runs great, except when the canvas becomes a specific size, my objects begin to scale larger and also get positioned incorrectly. I have narrowed it down and have been able to replicate it on a playground site.

My target use case is using Chromium inside of Electron, but the repro also occurs within a standard Chrome web browser. Is there something I am doing which is causing THREE.js or WebGL to behave this way, and if it is a bug, does anybody have any suggestions how I can work around it?

For the repro, simply find the 100x100 red box in the center of the canvas. Take a screenshot and paste it into Paint to measure it. You will see when the canvas reaches about 5800x5800, the sprite begins to grow.

import * as THREE from 'three';

const spriteWidth = 100;
const spriteHeight = 100;

// Samples:
//    5700x5700 (or less) = sprite is a perfect 100x100
//    5800x5800 = sprite begins to scale, as shown by antialiasing
//    6000x6000 = sprite scaled an extra 4 pixels in both width and height!
//    11520x2880 = sprite is a perfect 100x100
//    11520x3880 = sprite scaled an extra 17 pixels in both width and height!
const canvasWidth = 6000;
const canvasHeight = 6000;

// init
const renderer = new THREE.WebGLRenderer();
const camera = new THREE.OrthographicCamera();
const scene = new THREE.Scene();
renderer.setSize(canvasWidth, canvasHeight);
renderer.setPixelRatio(window.devicePixelRatio);
document.body.appendChild(renderer.domElement);
camera.aspect = canvasWidth / canvasHeight;
camera.left = canvasWidth / -2;
camera.top = canvasHeight / 2;
camera.right = canvasWidth / 2;
camera.bottom = canvasHeight / -2;
camera.position.set(0, 0, 1);
camera.updateProjectionMatrix();

// add sprite
const geometry = new THREE.PlaneGeometry(1, 1);
const material = new THREE.MeshBasicMaterial({ color: 0xff0000 });
const sprite = new THREE.Mesh(geometry, material);
sprite.position.set(0, 0, 0);
sprite.scale.set(spriteWidth, spriteHeight, 1);
scene.add(sprite);

// animate
renderer.setAnimationLoop((time) => renderer.render(scene, camera));

Can you share a screenshot of what you get vs what would be an expected result?

Produced using the code I provided in the original post:

image

I would expect the sprite to always be 100x100 and not scale, per the code.

Hello ! Try renderer.logarithmicDepthBuffer=true; also play with camera.near, camera.far.

Thanks for the suggestion. I tried renderer.logarithmicDepthBuffer = true; but unfortunately it had no effect. I also tried different and narrow variations of camera.near/far but those also didn’t have any effect. Do you have any other suggestions?

Also, I did try specifying logarithimicDepthBuffer using constructor options as shown at three.js/examples/webgl_camera_logarithmicdepthbuffer.html at 09fe0527a9aa5aaca7aaf17531e6c0d0efaa8c59 · mrdoob/three.js · GitHub.

const renderer = new THREE.WebGLRenderer({ logarithmicDepthBuffer: true });

I created multiple slices and they all “scale” larger together, but their relative positioning stays the same. This tells me the objects are not being scaled, but there is some kind of camera bug. I revised the code below to include multiple sprites/meshes, and includes an easier way to demonstrate the issue. The code below shows the sprites are larger and not centered (i.e. sprites are much closer to top than bottom). If you change canvasWidth to 8000 suddenly the sprites are the correct size and also centered.

import * as THREE from 'three';

const spriteWidth = 2000;
const spriteHeight = 2000;

// Samples:
//    5700x5700 (or less) = sprite is a perfect (spriteWidth x spriteHeight)
//    5800x5800 = sprite begins to scale, as shown by antialiasing
//    6000x6000 = sprite scaled larger in both width and height!
//    11520x2880 = sprite is a perfect (spriteWidth x spriteHeight)
//    11520x3880 = sprite scaled larger in both width and height!
const canvasWidth = 11520;
const canvasHeight = 3880;

// init
const renderer = new THREE.WebGLRenderer();
const camera = new THREE.OrthographicCamera();
const scene = new THREE.Scene();
renderer.setSize(canvasWidth, canvasHeight);
renderer.setPixelRatio(window.devicePixelRatio);
document.body.appendChild(renderer.domElement);
camera.aspect = canvasWidth / canvasHeight;
camera.left = canvasWidth / -2;
camera.top = canvasHeight / 2;
camera.right = canvasWidth / 2;
camera.bottom = canvasHeight / -2;
camera.position.set(0, 0, 1);
camera.zoom = 1;
camera.updateProjectionMatrix();

// add sprites
const geometry1 = new THREE.PlaneGeometry(1, 1);
const material1 = new THREE.MeshBasicMaterial({ color: 0xff0000 });
const sprite1 = new THREE.Mesh(geometry1, material1);
sprite1.position.set(-spriteWidth, 0, 0);
sprite1.scale.set(spriteWidth, spriteHeight, 1);
scene.add(sprite1);

const geometry2 = new THREE.PlaneGeometry(1, 1);
const material2 = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const sprite2 = new THREE.Mesh(geometry2, material2);
sprite2.position.set(0, 0, 0);
sprite2.scale.set(spriteWidth, spriteHeight, 1);
scene.add(sprite2);

const geometry3 = new THREE.PlaneGeometry(1, 1);
const material3 = new THREE.MeshBasicMaterial({ color: 0x0000ff });
const sprite3 = new THREE.Mesh(geometry3, material3);
sprite3.position.set(spriteWidth, 0, 0);
sprite3.scale.set(spriteWidth, spriteHeight, 1);
scene.add(sprite3);

// animate
renderer.setAnimationLoop((time) => renderer.render(scene, camera));

Can help Mugen or mr.Doob. Its like when pixel at screen cant be placed exact in that pixel in this position and they move to heighbour pixel with small smoothing or something etc.

That’s quite an interesting observation. I have never used canvases that big – when I need a big canvas, I simulate it with a small canvas (this requires redraw of content).

Anyway, going back your observations, I have structured them in a table:

Dimensions (px) Size (Mpx) Status
5700x5700 30.98 MB sprite is a perfect
11520x2880 31.64 MB sprite is a perfect
32.00 MB Is this some threshold?
5800x5800 32.08 MB sprite begins to scale
6000x6000 34.33 MB sprite scaled larger
11520x3880 42.62 MB sprite scaled larger

It looks like 32 megapixels is some kind of threshold (1 Mpx = 1048576 pixels). Would it be possible that this is something done by the browser, or by the operating system, or by the video driver? When an element is too large, it is represented as a scaled-up version of a small element?

Also, I checked this:

image

Size is 37.13 Mpx, so there should be scaling and this matches your observation.
Interestingly, 37.13 is 116% of 32, and this also matches your observation of 117x117 (there is just one pixel difference, it could be caused by antialiasing)

Well, all the above is just some juggling with numbers. It might point the issue, but it might be just a coincidence. At least it gives another idea to explore.

1 Like

I have found this bug does not reproduce on Firefox, and therefore I believe it to be a Chromium bug.

Would anybody have any idea how I could work-around this in Chrome? My target environment is Chromium with Electron. Thanks!

I checked camera, sprite properties in browsers and gl_position are same in FireFox, Chrome. Then its maybe browser rendering issue like big antialising.