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);
  • \$\begingroup\$ Can you please include a visual representation of this issue? An image, or a screen recording maybe, so that we can better understand the problem. \$\endgroup\$
    – liggiorgio
    Sep 5, 2021 at 20:34
  • \$\begingroup\$ Sure, let me do that now. One moment \$\endgroup\$ Sep 5, 2021 at 20:38
  • \$\begingroup\$ @liggiorgio Okay, I tried to upload a video showing the difference. Not sure how well it is coming across though because recording and uploading makes it blurry so it's probably not as easy to tell compared to seeing it in person \$\endgroup\$ Sep 5, 2021 at 20:46
  • \$\begingroup\$ Looks like you have undesired interpolation when rendering pixel art (not actual blur). This may be due to the camera position not really rounding up to the player's position (like 0.5 or 1 pixel behind). Do you experience this issue with a 1:1 scaling ratio as well? \$\endgroup\$
    – liggiorgio
    Sep 5, 2021 at 21:05
  • \$\begingroup\$ The pixel art always has rendered fine though. This is the first time I'm having an issue. And yeah, entity positions have always been floating point. And yes if I change the tile scale from 3 to 1 then it is also blurry when I use the lerping camera. \$\endgroup\$ Sep 5, 2021 at 21:31

1 Answer 1


The blurry visual artefacts you are experiencing may be caused by how the rendering stack treats floating-point numbers when it comes to drawing sprites: world positions are floating-point numbers, pixels on the screen are not.

As a (blind) attempt to solve this issue, I suggest to change your interpolation function from this:

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

To this:

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

The latter version guarantees to return b when t = 1, whereas the former doesn't due to floating-point arithmetic error.

In your particular case, b is the player position which the camera is trying to reach; small decimal differences may lead to rounding errors when rendering on the screen, which is the alleged cause of your current issue.


The external answer you included in your question refers to a piece of code which contains a comment:

// No need for the "if" - we'll practically never reach exactly 0 distance anyway.

This is an expected behaviour for two reasons:

  • Interpolation, from a mathematical perspective, converges to a constant value after an infinite number of steps. I mean an exact value convergence, which can never be reached after a finite number of steps: 100000000000000000000000000000000 lerping steps are a huge amount, yet still finite. However, convergence occurs on computers due to machine error caused by the limited number of bits used to represent individual values
  • Interpolation of a value towards another that changes over time (player is moving around) cannot really converge, since the target value changes more or less unpredictably each step

If you want to smooth your camera movement, you could fix its behaviour when it is close enough to the player, such as:

  • Snapping camera position to the player's if the distance is smaller than a value such that it prevents undesired visual effects
  • Taking explicit control of t, and change its value over time rather than over distance to make sure it equals 1, and best follow the player object
  • \$\begingroup\$ Thanks for the attempt, but there's still the same blur issue even after replacing the old lerp function with this new one you provided. Any other ideas? \$\endgroup\$ Sep 5, 2021 at 22:28
  • \$\begingroup\$ Also, I'm pretty sure (not entirely though) that floating-point locations are fine, because my default camera (this.x = player.x; this.y = player.y;) works perfectly (no blur) and I'm console logging the player positions with that and they're all floating point. \$\endgroup\$ Sep 5, 2021 at 22:38
  • \$\begingroup\$ What about logging camera position over time too? Blurry visuals may imply different values between camera and player position, which can be verified with logs. \$\endgroup\$
    – liggiorgio
    Sep 5, 2021 at 22:51
  • 1
    \$\begingroup\$ Here's an image of the logs when using your lerping function and moving the player character left and right: i.imgur.com/T16ktuF.png The logs for the other case when I just assign the camera position to the player position are uninteresting because in that case both values are always identical. \$\endgroup\$ Sep 5, 2021 at 22:55
  • \$\begingroup\$ I updated my answer with some additional considerations about this issue and the new information you provided. \$\endgroup\$
    – liggiorgio
    Sep 5, 2021 at 23:20

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .