Preventing dither flashing/flickering on moving objects by snapping to even pixels

Quantizing motions to 2-pixel increments solves the annoying flicker for 2x2 dithers (like checker and pinstripes), and reduces it for 4x4 or larger patterned dithers. The result feels much smother and nicer.

It's easy to do, even after your motion is already coded—you can still use easing, the crank, or any other arbitrary method to define the motion.

In case it helps anyone, here are some code examples.

Do these at the last minute before rendering the change. If you quantize earlier in the process, you may be rounding off values too soon, with unintended consequences.

For instance, if you are tracking a player position based on the crank, physics, or other arbitrary factors, you probably need to track position and changes in fractional pixels behind the scenes, so that curves and accelerations and the sums of multiple changes all stay accurate. Don't quantize those behind-the-scenes values—only the visible result at render time.

Example 1: Taking a progress value between 0 and 1 (from an animator with easing, for instance) and snapping it to quantized 2-pixel steps between a start point and end point (which need not be quantized themselves):

function interpolate2px (startX, startY, endX, endY, progress)
	local newX
	local newY
	
	newX = startX + 2 * math.floor((endX - startX) * progress / 2 + .5)
	newY = startY + 2 * math.floor((endY - startY) * progress / 2 + .5)
	
	return newX, newY
end

Example 2: Taking an arbitrary x, y coordinate (like a screen position—or a change in position) and snapping it to quantized 2-pixel steps:

function coordinates2px (oldX, oldY)
	local newX
	local newY
	
	newX = 2 * math.floor(oldX / 2 + .5)
	newY = 2 * math.floor(oldY / 2 + .5)
	
	return newX, newY
end

Note: the purpose of adding .5 is to make the quantizing happen by rounding to the nearest step. In some cases that's not necessary—omit the .5, and you will simply always snap lower (floor). In other cases, though, that will cause a little jump at the end of a movement, or will cause the object to end up slightly short of where you want. The .5 fixes that.

These examples make things move in 2-pixel increments to prevent flicker. They can still end on an odd pixel coordinate if they started on one. So if you also want all dithered objects to line up with each other and never be at odd coordinates, make sure they start that way: don't position an object starting at 3, 3, say.

If your dither is larger than 2x2 and you want to quantize to greater increments, just change the "2"s.

I'm new at Playdate... improvements and variations on these techniques are welcomed!

10 Likes

Thanks Adam! This sort of works for me when moving the player, but because of the 'move by 2' the coordinates/camera move double the speed than the player (makes camera move gradually off of the player). Should I be only updating the move every other update or is there a way to slow it down while still preserving the dither fix?

1 Like

That shouldn't happen (the examples above both multiply AND divide by 2) as long as the snapping is applied as the LAST step. That saves all kinds of complexity.

So calculate the player position (or delta) as usual first, with no snapping. That will set the speed. Then apply the snapping. And you may need to STORE the unsnapped "real" position to use as the basis for the next motion. Don't feed the snapped position back into the game.

In practice, then, some frames may have no motion—but you don't need to worry about that.

(And for some simple scenarios, halving the framerate may indeed give the result you want.)

1 Like

Thanks! I think I was doing something wrong. I'm finding that this especially works well for every other pixel dithering. If there are four empty pixels between two, I would think this would work as well, but those seem to flicker a tiny bit as a pixel is moving into the empty space between them.

1 Like