PixelPerfect Orthographic Camera with Blocks for a PixelArt 2D Look

I am trying to make a 2D pixelart scene with threejs.

I would like a pixel perfect matrix of 64x48 blocks each 5x5 pixels (rendered to 320x240 actual pixels). The blocks should be colored 4x4 so that we have a 1px frame around them.

image

Using an orthographic camera it’s not pixel perfect: there appear larger than 1px gaps occasionally.)

image

BONUS Question: Is there a better way of managing “pixels” other than as cubes, perhaps with a shader to ensure fine, pixel control over how they are rendered. Perhaps adding glow etc.

Set the renderer antialiasing to true

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

This is not the most optimized way to render a grid, you should use a Shader or a LineSegments, take a look at the GridHelper source code for a LineSegement implementation, or this InfiniteGridHelper for a Shader implementation.

Or you can simply use one of them.

1 Like

That definitely helped - I always thought antialiasing causes blurring and is the “enemy” of pixel art.

I would also like the grid to be full screen but it is somehow “zoomed out” with a large black margin.

image

The grid is 320x240px and I positioned the camera in the middle (x=160, y=120). I then set the camera’s fulcrum to -160, 160, -120, 120 which should encompass the entire matrix.

let aspect = 1;
let w = 320;
let h = 160;
let viewBlock = w / 2;
const camera = new THREE.OrthographicCamera(-viewBlock * aspect, viewBlock * aspect, viewBlock, -viewBlock, 100, -100);

I then set the renderer size renderer.setSize(320, 160); but for some reason the canvas is bigger than the size set (width=440):

Unfortunately antialiasing is just a trick to the eye - ultimately we are still not pixel perfect:

image

try with this:

renderer.setPixelRatio(1)
renderer.setSize ( 320, 240, false ) // set update style to false

Again this is not an efficient way to draw a grid, you are basically drawing 3072 Meshes each frame that’s a lot of Meshes for simple a grid, here is a working example of an infinite grid helper with Grid component - CodeSandbox (docs)

4x4 pixels + 1 pixel border means your blocks have to be 6x6.
What you mean, probably, is 1 pixel gap between blocks. In this case, on the width, there are 64 * 4 (blocks) + 63 * 1 (gaps) = 319 in total. The same maths for the height, thus 239.

1 Like

I used a mesh of PlaneGeometry and ShaderMaterial, got this (64 x 48 tiles):

1 Like

@Fennec :laughing:

Here is that attempt:

2 Likes

I am not trying to draw a grid: I am trying to draw large pixels on the screen. My game will be generating pixelart sprites which I want to render using threejs. So currently, yes, we have a grid. But later we will have a scene of positioned boxes:

    +
    +
   +++
 +++++++
 ++   ++
++  +  ++
++ +++ ++
++  +  ++
 ++   ++
 +++++++
   +++

Yes, I mean “gap” not border or margin: 4px color, 1px gap => repeat.

I have switched antialiasing off and used a pixelRatio of 1:

image

Here an example of a “scene” with a floor and a roof:

It’s a tiles grid :man_facepalming:, sorry for the misunderstanding, I though it was just a background grid.

In any case, you should consider using @prisoner849 `s solution, as it offers a significant performance improvement over using a Mesh grid.

If you only intend to update the colors of each tile, then the implementation is straight forward, if you also intend to update the texture of the tiles, then things may become a little more complicated, but nothing a good texture atlas system couldn’t handle.

Good luck with your project.

1 Like

@cawoodm

Somehow I managed to make the original JSFiddle to show perfect pixel alignment. It appears Windows is messing up how canvases are rendered. There are 4 lines changed, all marked by comments. Line PB-1 is removed, lines PB-2,-3,-4 are added.

PB-3 is the magic. If you Windows is set to zoom 100%, use scale(1,1); if the zoom is 125%, then use scale(0.8,0.8), for zoom 150%, use scale(0.6667,0.6667) and so on for zoom X% use scale factors 100/X. The zoom factor is found in the Windows display settings. My setting was 125%, so the scale factor is 0.8 for me.

image

The final result is this:

image

Here is the code:

var camera, scene, renderer;
var geometry, material, mesh;

init();
animate();

function init() {
  scene = new THREE.Scene();
  renderer = new THREE.WebGLRenderer({ antialias: false });
//PB-1  renderer.setPixelRatio(window.devicePixelRatio);
  renderer.setSize(window.innerWidth, window.innerHeight);
  document.body.appendChild(renderer.domElement);
  renderer.domElement.style.imageRendering = 'pixelated'; //PB-2
  renderer.domElement.style.transform = "scale(0.8,0.8)"; //PB-3
  window.addEventListener("resize", onWindowResize, false);

  scene.add(new THREE.AmbientLight(0x101010));

  let viewBlock = 320 / 2;
  aspect = window.innerWidth / window.innerHeight;
  camera = new THREE.OrthographicCamera(-viewBlock * aspect, viewBlock * aspect, viewBlock, -viewBlock, 1000, -1000);
  camera.position.set(0, 0, 100);

  // Matrix of 64x48 "bloxels" each 5x5 pixels
  const color = new THREE.MeshStandardMaterial({ color: 0x33dd33 });
  let W = 5;
  let H = 5;
  let X = 320 / W;
  let Y = 240 / H;
  for (let x = -X / 2; x < X / 2; x++) {
    for (let y = -Y / 2; y < Y / 2; y++) {
      const box = new THREE.Mesh(new THREE.BoxGeometry(W * 0.8, H * 0.8, 2), color[0]);
      box.position.set(W / 2 + x * W, H / 2 + y * H, 0);
      scene.add(box);
    }
  }
}

function onWindowResize() {
  camera.left = window.innerWidth / -2;
  camera.right = window.innerWidth / 2;
  camera.top = window.innerHeight / 2;
  camera.bottom = window.innerHeight / -2;

  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
}

onWindowResize(); // PB-4

function animate() {
  requestAnimationFrame(animate);
  renderer.render(scene, camera);
}

– Pavel

PS. Instead of BoxGeometry you can use PlaneGeometry to reduce 6 times the amount of triangles. Or, better, use Points with size 4 and turned off sizeAtenuation.

2 Likes

I wish it was just my monitor settings but I am on 100%.

I cannot reproduce your success however - it’s pixel perfect but not “accurate” in that some blocks are bigger/smaller than others.

image

When I run your code directly (PB-3 => 0.8) and screenshot it I see:

image

If I set PB-3 to 1 I see:
image

Ultimately I want these “pixels” to have physics later so you can blast them away.

I also want the grid to fill the screen - setting camera.zoom has no effect, neither does setting camera.position.z. How can I avoid the black margins?

image

1 Like

Hi –
While trying the code, I also got similar issues – some squares were little bit larger (or smaller), something the gap was 2 pixels, not 1. If you have time, you could test different scale factor values, maybe you could stumble upon the value that works for you (e.g. 1.00, 0.99, 0.98, 0.97…). However, this is not a solution, because you’d like to have such precision on your clients/users machines, not just on your own machine.

When I found 0.8, it was by accident. I was testing some random values.

If I were you, I’d work with larger pixels, so that a deviation of 1 pixel would not be obvious to the users.

– Pavel

I’m going to move forward with antialiasing for now. It’s not pixel perfect but it does please the eye.

I would like to have my grid fill the screen though. The idea is a 2D game with ThreeJS.

Larger pixels doesn’t help much since the eye notices the jump.

Here is scale 1 - 4x4px boxes in a 5x5px grid:
image

Here is scale 4: 20x20px boxes in a 25x25px grid:

By scale I mean I am drawing larger geometry not scaling with the canvas/camera.

1 Like

Yes –

Antialiasing will make sizes and gaps appear the same.

– Pavel