3
\$\begingroup\$

Context

I'm building a 2D game from scratch in JS. I'm drawing the map by using rpgmxp tilesets for the moment just to have some resources :

enter image description here

Each tile is 32*32 pixels. I'm using canvas API with ctx.drawImage method to display it :

ctx.drawImage(tile.tilesetImg, tile.sX, tile.sY, this.tile.w, this.tile.h, canvasX, canvasY, Engine.Camera.tile.w, Engine.Camera.tile.h);

I use different parameters for the image source size (this.tile.w & this.tile.h) and the image destination size (Engine.Camera.tile.w, Engine.Camera.tile.h).

This has the effect of scaling the game, like that each user have the same map exactly displayed (32 tiles * 24 tiles). The current tile aspect ratio distortion doesn't matter to me for the moment.

The result displayed to the player with a 4:3 ratio :

enter image description here

Problem

When I change window size if I don't keep the ratio 4:3 (because i display always 32 tiles * 24 tiles for the player) one of my tile types causes strange visual artifacts. This happens in chrome and firefox and probably all browsers.

It's strange because all the other tiles are drawn from the same method, but do not exhibit this behavior. This behavior only seems to occur when the ratio is very different from 4:3.

Here is an example of what I'm talking about.

enter image description here

There is some transparent lines which display the background at the right of each "bad" tile. Note this tile happens to be the first tile from the tileset (first image above).

The same thing happens if height alone is changed, except the line will appear on the bottom instead.

The thing which is again more strange is that if i change the tileset to draw a red line at the right of the first tile :

enter image description here

The transparent line is replaced by the red line when drawing the map :

enter image description here

From debugging (console.log), there doesn't apear to be any reason that tile source size would be 33px instead of 32px, but the artifacting effect makes it seem like this is the case.

Question

I appear to be using drawImage correctly by all accounts, so googled to see if this was a known problem. I was reading the w3 documentation when I found that : (especially the last note of part 12)

Thus, scaling an image in parts or in whole will have the same effect. This does mean that when sprites coming from a single sprite sheet are to be scaled, adjacent images in the sprite sheet can interfere. This can be avoided by ensuring each sprite in the sheet is surrounded by a border of transparent black, or by copying sprites to be scaled into temporary canvas elements and drawing the scaled sprites from there.

Does this explain the problem ? Does any one know what the problem is more in detail ?

Thank you in advance, best regards.

\$\endgroup\$
1
\$\begingroup\$

This is an unavoidable artifact of anti-aliasing. You have two options, both require a compromise. As seen in the example, the unfortunate reality is that some browsers don't handle disabled anti-aliasing as well as others.

1. Disable Anti-Aliasing

Disable anti-aliasing altogether. The compromise here is that the "smooth" look you get with anti-aliasing will change the perceived art style of your game.

This can be achieved in modern browsers with added CSS & JavaScript.

CSS

/* add this class to your canvas */
.disable-anti-aliasing {
  image-rendering: optimizeSpeed;
  image-rendering: -moz-crisp-edges;
  image-rendering: -webkit-optimize-contrast;
  image-rendering: -o-crisp-edges;
  image-rendering: optimize-contrast;
  -ms-interpolation-mode: nearest-neighbor;
}

JavaScript

// pass in the canvas' context to this function
function disableAntiAliasing(context) {
  // note: you must factor this into any other context.translate calls in the future
  context.translate(0.5, 0.5);
  context.webkitImageSmoothingEnabled = false;
  context.mozImageSmoothingEnabled = false;
  context.imageSmoothingEnabled = false;
}

2. Modify your assets

These artifacts occur when the canvas renderer samples neighbor pixels in the tileset. Since the tiles in your tileset have no padding around them, there will always be bleeding from these neighbors (with any sort of scale/rotation). If you add roughly 2 pixels in padding to your tilesets, you won't get such harsh bleeding.

As an added measure, you can offset the canvas by 0.5 pixels like in the JavaScript section of the first option. This throws off the anti-aliasing algorithm. As to why this is? Probably some sort of magic that only the graphics gods can explain.

The compromise here is that you are now spending more time editing all of your assets, but luckily some tileset editors can export with padding baked in.

function disableAntiAliasing(context) {
  // note: you must factor this into any other context.translate calls in the future
  context.translate(0.5, 0.5);
  context.webkitImageSmoothingEnabled = false;
  context.mozImageSmoothingEnabled = false;
  context.imageSmoothingEnabled = false;
}

function renderTile(ctx, tile, canvasX, canvasY, scale) {
  ctx.drawImage(
    tile.tilesetImg, 
    tile.sX, tile.sY, 
    tile.w, tile.h, 
    canvasX * scale, canvasY * scale, 
    tile.w * scale, tile.h * scale
  );
}

function render(time) {
  for (var i = 0; i < contexts.length; ++i) {
    var ctx = contexts[i];
    
    ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  }
  
  var offsetX = Math.cos(time / 6400) * 120;
  var offsetY = Math.sin(time / 6400) * 120;
  var scale = 0.5 + (Math.cos(time / 5000) + 1) * 0.5;
  
  for (var x = 0; x < 25; ++x) {
    for (var y = 0; y < 25; ++y) {
      var tile = tiles[(x + y * 16) % (12 * 16)];
    	
      var canvasX = x * tile.w - offsetX;
      var canvasY = y * tile.h - offsetY;
      
      for (var i = 0; i < contexts.length; ++i) {
        renderTile(contexts[i], tile, canvasX, canvasY, scale);
      }
    }
  }

  requestAnimationFrame(render);
}

const contexts = [].slice.call(document.querySelectorAll('canvas'))
  .map(function(canvas) { 
    return canvas.getContext('2d');
  });

disableAntiAliasing(contexts[1]);

var tiles = [];

for (var x = 0; x < 12; ++x) {
  for (var y = 0; y < 16; ++y) {
    tiles.push({
      tilesetImg: document.getElementById('tileset'),
      sX: x * 32,
      sY: y * 32,
      w: 32,
      h: 32
    });
  }
}

requestAnimationFrame(render);
img {
  display: none;
}

canvas {
  display: block;
  border: 1px solid black;
}

.disable-anti-aliasing {
  image-rendering: optimizeSpeed;
  image-rendering: -moz-crisp-edges;
  image-rendering: -webkit-optimize-contrast;
  image-rendering: -o-crisp-edges;
  image-rendering: optimize-contrast;
  -ms-interpolation-mode: nearest-neighbor;
}
<img id="tileset" src="https://i.stack.imgur.com/p8MLp.png">

<div>
  <strong> Regular </strong>
  <canvas width="600" height="600"></canvas>
</div>

<div>
  <strong> Anti Aliasing Disabled </strong>
  <canvas class="disable-anti-aliasing" width="600" height="600"></canvas>
</div>

\$\endgroup\$
  • \$\begingroup\$ Thanks :) The first technique works fine. Don't have time yet to try the second one even if it looks better solution to me. When you speak about the 2 pixels padding, it is the same as the transparent black border that the w3 documentation mention ? If yes, do you know what they mean by "copying sprites to be scaled into temporary canvas elements and drawing the scaled sprites from there." \$\endgroup\$ – ElJackiste Dec 21 '17 at 20:31
  • \$\begingroup\$ They are saying that you can separate each sprite from the sprite sheet by drawing each sprite onto a new canvas. From there, you can draw that canvas as if it was an individual sprite sheet. This will not bleed because the surrounding sprites don't exist. The problem with this solution is that performance will become a concern very quickly given how many different canvases will be created. If you came up with a way to split up all tiles in the sprite sheet to a separate canvas and use that for tile.tilesetImg, you might be able to get good results. I just suspect performance issues. \$\endgroup\$ – Austin Brunkhorst Dec 21 '17 at 21:12
  • 1
    \$\begingroup\$ Yeah, i was thinking about performance issues too. Thank you again for answers. \$\endgroup\$ – ElJackiste Dec 21 '17 at 22:14
  • 1
    \$\begingroup\$ I came back on this game project recently and i decided to implement the second solution "modify assets". If people like me arrive here, there is 2 tips : First, i thought i need to add transparent padding (or black transparent as w3 recommended). It doesn't fix the problem for me, you probably need to copy borders too, see : rotorz.com/unity/tile-system/docs/edge-correction-borders Second, to add padding and copy borders on an existing tileset, we can edit it by using this script with gimp which works very good : registry.gimp.org/node/24816 Hope this will help someone. \$\endgroup\$ – ElJackiste May 18 '18 at 21:16

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.