Introduction
The latest entry in my series of adventure games (currently in progress) is the first to have long backgrounds with scrolling and parallax. The game (with the exception of the cursor) is all pixel-perfect—everything is drawn on a 240x135 framebuffer, which is then upscaled to fit the game window.
As anyone who has tried to do pixel-perfect scrolling likely knows, moving layers at different speeds generally leads to choppy animations unless the speeds are carefully tuned (see below for an example).
A few weeks ago I had an idea on how to get pixel-perfect scrolling that is also smooth. I finally had time to implement it in my game and I’m really happy with the result, so I’m writing this post to document the technique.
I’ll start by reviewing the two usual approaches to parallax in pixel art games, then I’ll explain what I did and how I did it. If you only care about my approach you can jump there directly.
Regular approach
The usual approach is the simplest one: the layers are drawn in a pixel-perfect way and can only move in integer steps, as in the image above. The problem here is that, unless all layers move at a multiple of the same speed, there will be some choppiness due to the fact that some layers need to wait and “accumulate” time before they can move—this is very noticeable with layers moving slower than the main background, like the clouds and the sky above.
Until yesterday, this was the way that parallax scrolling worked in my game. It’s not terrible if the camera moves at constant speed, but it looks quite bad if the camera slows down to a stop.
The (non-pixel-perfect) smooth approach
I had already looked into this in the past, and I knew that there is another well-known method of dealing with the choppiness issue: bite the bullet and give up on having pixel-perfect graphics. We can draw each layer already upscaled, for example x4, and use the full resolution grid to draw the layers to the screen. This way we have 4 substeps between each game pixel, and the animation looks much smoother.
While the effect is much nicer, I really wanted to keep my game pixel-perfect for two reasons:
- I like the pixel-perfect aesthetic and I want to to stick to it as closely as possible. Sue me.
- Switching to this approach would break a lot of stuff in the way the game is currently rendered, which relies on drawing things in the framebuffer.
My approach
The way I solved the problem is by drawing to the framebuffer, but at non-integer positions (so they can move continuously). Counter-intuitively, I set the engine to use a linear filter instead of a nearest-neighbour filter to draw the images.
Normally this results in ugly, blurry images when pixel art is rescaled—but I’m not rescaling it! I’m drawing it with linear filter on the pixel-perfect framebuffer, which results in subpixel rendering where neighbouring pixels are blended with intensity depending on where we are between the two pixels. You can see this effect in the image below, which singles out a single in-between frame of the animation.
The framebuffer itself is upscaled with nearest-neighbour filter, as usual. In the end we get an animation that is pixel-perfect, and does not look choppy. Of course there was a price to pay: this introduces blurriness (essentially a motion blur), but I find it a good compromise, especially since it opened up the possibility to moving the camera at non-constant velocities wich makes things much less nausea-inducing (or so I’m told).
Some notes:
-
For best results (aka to avoid a blurry mess) the main layer (the one that moves at the same speed at the camera) should be drawn at integer steps (i.e., the camera should move at integer steps) so that it will never have any blurriness.
-
When the camera is not moving all layers should be drawn at integer steps to avoid blurriness. This can be done by keeping track of when the camera is moving and rounding the drawing coordinates if it has stopped.
-
The most noticeable side-effect of this technique is that not all layers are crisp anymore. As I said before, this is a compromise—smooth, pixel-perfect, crisp: choose two.
-
Something to keep in mind is that you have to be mindful of what colours are being blended. For layers with transparencies, the boundary pixels are blended with the neighbouring transparent ones, which are actually likely black with zero alpha—the blending happens for both the alpha and the colour channels! For lowe contrast things this is likely not noticeable, but can result in unwanted effects for hight contrast objects. For example, in an earlier version of this post I had made the animation using the stars on a separate layer from the sky, which led to considerable flickering when they were moved.
-
My approach uses a framebuffer because I was already using one before anyway. I *think* that you could get the same result by drawing things just as you would for the non-pixel-perfect method (nearest-neighbout rescale all layers, draw them on high resolution grid) and use a custom shader to draw the subpixel stuff. I didn’t try this so I’m not 100% sure it would work, and also it sound like a lot of work so I wouldn’t recommend it.
There, now my approach is documented. If you end up using this and find it helpful let me know!