#StackBounty: #javascript #camera #pixel-art How to prevent blurry pixel art when the camera is following a character?

Bounty: 50

I’m developing a top-down pixel-art game and previously I was having the camera follow the player like this in the camera’s update function (called every frame):

this.x = playerPos.x;
this.y = playerPos.y;

And that worked to have the camera follow the player. It would directly stick onto the player and cause no blurry visuals.

However, I read online that lerping the camera to make its movement smooth is often more desirable to the player. I found this excellent answer by DMGregory and implemented it like so:

let followSharpness = 0.1;
let blend = 1 - Math.pow(1 - followSharpness, Globals.deltaTime * 30);

let xOffset = playerPos.x - this.x;
let yOffset = playerPos.y - this.y;

this.x = Helpers.lerp(this.x, playerPos.x + xOffset, blend);
this.y = Helpers.lerp(this.y, playerPos.y + yOffset, blend);

Which also worked. The issue though is that now the character looks very blurry when the camera is following it. I’m not sure what is causing this, or even how to debug it. Some things I’ve noticed:

  • When the camera catches up to the player sprite, the sprite is no longer blurry
  • The player sprite seems more blurry than every other entity on the screen
  • If the player stops moving, he is no longer blurry
  • The player sprite only seems blurry when moving
  • All entities have float positions, and so did the camera previously, so I don’t think it has to do with that.

Anyone have any clue on why this might be happening, or how to solve it?

Edit: I’ve tried to take videos showing the difference:

Lerping: https://streamable.com/i7gxbe

Instant: https://streamable.com/n6awlz

Basically, the lerping video is using the aforementioned code in the post, and the instant code just does this:

this.x = playerPos.x;
this.y = playerPos.y;

My lerping function:

function lerp(a, b, t) { return a + (b - a) * t; }

My shader for drawing the sprite looks like this:

Vertex Shader:

void main() {
  gl_Position = vec4((u_cameraMatrix * u_transformMatrix * vec3(a_position, 1)).xy, 0, 1);

  v_texcoord = (u_textureMatrix * vec3(abs(u_flip - a_texcoord.x), a_texcoord.y, 1)).xy;
}

Fragment Shader:

void main() {
  outputColor = texture(u_texture, v_texcoord) * u_tint;
}

Here’s the code that loads the image texture:

// creates a texture info { width: w, height: h, texture: tex }
// The texture will start with 1x1 pixels and be updated when the image has loaded
static loadImage(gl: WebGL2RenderingContext, url: string): TextureInfo {
  var tex = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, tex);
  // Fill the texture with a 1x1 transparent pixel.
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([ 0, 0, 0, 0 ]));

  // let's assume all images are not a power of 2
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);

  let textureInfo = new TextureInfo(1, 1, tex);

  var img = new Image();
  img.addEventListener('load', function() {
    textureInfo.width = img.width;
    textureInfo.height = img.height;

    gl.bindTexture(gl.TEXTURE_2D, textureInfo.texture);
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img);
  });
  img.src = url;

  return textureInfo;
}

And here’s the code that binds the texture:

gl.activeTexture(gl.TEXTURE0 + 0);
gl.bindTexture(gl.TEXTURE_2D, texInfo.texture); // this binds tex to TEXTURE0+0
gl.uniform1i(attribs.textureLocation, 0);


Get this bounty!!!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.