In modeling the ship and testing things out in-game I got the distinct and unmistakable feeling that "this could use some screen-warping post effects to roughen up the low-poly straight lines everywhere." I'm sure you know the feeling.
I have been down a dark path. Let me take you there.
Starting with this innocent screen:
Testing screen, untouched
The first experiment is with Photoshop's "Wave" distortion filter:
Wobble things around a bit with Photoshop's "Wave" distortion filter
B+. Warps the lines around to make them look less polygonal. The problem is that Photoshop's algorithm doesn't work in 1-bit, so you need to apply it, then re-threshold or re-dither the result. Not a huge deal, but it does double some pixels and erase others. That hurts the legibility so what if there was a way to just shift pixels around and not affect their color at all?
Next try, just offset each pixel by a random amount, using some custom Haxe testing code:
Each pixel shifted by a random amount
Ok, too much. Let's offset large blocks in a random direction instead, to reduce the frequency:
Blocks of pixels shifted by a random amount
Not bad. In motion, there are large stretches of lost or doubled pixels which destroy the nice single-pixel-wide lines and looks more clearly like a post effect than a soft warping which I want. But, altogether a decent solution and solved very quickly... Too quickly. Let's identify an arbitrary problem with this randomized approach that we can spend days and days trying to solve.
With the low-res visual style here, and just 1-bit to work with, each pixel is important. The "wireframe" lines make lost or doubled pixels especially ugly. That leads to a rule: If we move one pixel, it shouldn't ever overlap another pixel or leave behind a hole.
... which sounds pretty dumb because of course a pixel can only move in 4 directions (ignore diagonals), and each direction already has a pixel there. The key is that the first pixel move has to go offscreen, leaving a hole - then another pixel can move into that spot, creating another hole - etc. Wind your way around the entire screen this way and you can move each pixel one spot and never overlap or double another pixel. How?
Build a special 2D maze (with no branches and no dead-ends) where the solution visits each square once, then while walking the solution pick up the pixel in front of you and put it down where you're standing. This shifts each pixel one space along a winding path without overlapping or doubling. If if the maze is sufficiently winding, then the direction you shift each pixel is nearly random. In pictures:
To start, build a maze using the trivial depth-first algorithm, with a bias towards changing directions at every possible step:
Simple depth-first maze. Note the branches and dead ends
Next, this needs to be converted to a unicursal maze - a single path through without any branches or dead-ends. Thanks to Thomas Hooper for the technique for this - it's pretty cool. To convert a normal maze into a unicursal one, first get a solution that visits every square in the maze. Any solving technique will work but the easiest is to just follow the left-hand wall as you walk:
Solving the maze by sticking to the left-hand wall
Next, take that solution and use it to insert a new set of walls between the existing ones:
Solution collapsed to a single line, then added as a new set of walls in-between the existing walls.
The new maze (double the size of the original one) can be solved by walking straight through without any decisions:
Unicursal maze - single solution that visits each square once
So, making a unicursal maze the size of the screen and shifting each pixel one spot along the solution path gives us this:
Shifting each pixel using offsets from a full-screen unicursal maze solution
Ok, that looks cool. Compare it to the random offset image above and it maintains a bit more order. Lines are a consistent width and there are no breaks or extra-thickened areas. But again it's too high-frequency to be considered a gentle warping. We need to reduce the frequency. I'll come back to this image below though.
Instead of generating a full 640x360 maze and applying it directly to the screen, generate a much smaller maze, scale it up, then apply it. This gets its own section...
The trick to scaling a (unicursal) maze in this case is that we need to maintain the property that the solution traverses the entire maze. For the mazes above, there's just one solution track so it's easy. When we scale the maze up, there are multiple independent solution tracks - each one must trace uninterrupted through to the end. I now realize that this is hard to describe in words. More pictures are needed.
If we take the unicursal maze above and represent its solution as a "flow":
A flow with one track that traverses the entire maze (starting in the top left)
Scaling that flow up arbitrarily yields this, which breaks our "must trace uninterrupted" rule at the turns:
Scaled flow with broken turns
Turns have to be handled specially during the scale:
Scaled flow with fixed turns - each track can be traced from start to finish
Scaled x5 and animated:
Each track runs through the maze independently. In the end every pixel is visited exactly once
Now we can generate a small maze, scale it up, and use that to shift the screen pixels at a lower frequency:
Small maze scaled up and applied to the screen
Hmm, ok. Every pixel is accounted for. That's good. But there are disjoints where the tracks pass by each other in the opposite direction. This ends up shifting adjoining pixels by 2 spots, which makes some lines appear to break. And with such a low resolution maze the structure is faintly evident. Luckily, we have multiple tracks and can apply a sine wave to the track offsets. Applied to the x5 animated gif above, that would mean shifting the white track by 0 spots, the green track by 1, the cyan by 0, the purple by -1 and the blue track by 0, roughly.
Tracks offset in a sine wave to reduce disjoints
Same effect, exaggerated and applied to a test grid
Multiple mazes at differing resolutions can be stacked. Still no pixels are lost or doubled.
Back in the game, just one low-frequency maze
In motion (rounding errors in my shader are eating some pixels)
Ok! That looks good... Well, it's what I wanted and it holds together pretty well in-game. There's a nice warping which definitely makes it feel more ropey than straight low-poly shapes. Unfortunately it's a bit too distracting. Maybe it'll only be applied in the flashbacks. Maybe not. No surprise then that this rabbit hole has only dirt at the end.
There was something cool in the middle there.
1-Bit Screen Blur
That intermediate, high-frequency maze-offset thing looks a lot like a blur - using just offsets, no blending:
If you don't think this looks cool then go back in time and unread this whole post
This 1-bit blur is something I never wanted or needed but hey let's see what we can do with it anyways.
Here the high-frequency maze is layered with lower-frequency mazes to add irregularity.
This maintains an important legibility over using randomized offsets
Another spot. This can be globally faded in and out, so may be good for a transition effect or something
Depth of Field
Scaling the effect with distance
Scaling between low and high-frequency mazes based on distance
Scaling the effect based on world distance from a hand-placed focal point
Same thing, different angle
These hold up surprisingly well in motion. I can't prove that though b/c the animated gifs are too big.
That last one might work especially well to highlight the killer/killed in each flashback. Or not. After all this work I'm still not sure where or if it'll end up in the game. I really just wanted an excuse to post those maze-solving animated gifs.