PAX West 2017Showing the game at the IMB Minibooth went great. My goal was to test out the new book and game flow and it seems the changes worked well. Some players embraced the book and some players ignored it but just about everyone was able to proceed through the game without getting stuck. Visually, the presentation was helped by the small-ish monitor mounted away from players' faces. Small physical pixels means the dithering works as it should and the low resolution isn't a distraction. As far as I was concerned, everything was hunky dory.
Having finished the script and sent out character audition scripts before PAX, the moment I got back home there were hundreds of voice actor auditions. That kept me busy for a while but I was able to stop by TGS in mid September to chat with a few fellow indies. One of those indies was Justin from Gattai Games. They're making a black & white horror VR game,
"Stifled", which at first glance looks similar to Obra Dinn. I played their demo at PAX and it was great fun. They were showing the same build at TGS and I had a chance to ask Justin what he thought of the Obra Dinn build he played at PAX. His main comment was that it gave him a headache after a few minutes so suit up b/c there's nothing I love more than a problem that needs solving.
Shark JumpingThis is the point where, as I put this post together, I wonder wtf I'm doing and why I don't just finish the damn game already. Moving on.
Fullscreen Still Looks BadA few months Three years ago I made a
long post about how the game looks bad in fullscreen. I addressed this problem in a few ways (better dither, optional soft filter, etc) but resisted actually increasing the resolution, the obvious choice. The main reason for that is that I'm leaning heavily into the grungy dither-punk presentation through all levels of the art direction. Everything was designed with a minimum size for details and adding extra fidelity makes the game look worse.
Still though, it's a valid complaint that the game is all-day headaches and I really wanted to spend a little more time on that.
Shades and LinesBroadly, the game's visuals are split into two separate elements: 1) Dithered texture, and 2) Wireframe lines.
The basic visual components
Zoomed
The game renders natively at 640x360. Doubling to 1280x720 gets finer geometry, more effective dithering, and thinner wireframe lines:
As mentioned I don't like the extra detail, but I also don't like the thinner wireframe lines. The chunkiness is almost totally gone and that's just no good. You can also just start to see how low-poly the geometry is, which I'd rather avoid.
Now's a good time to reiterate that the visuals are really only a problem at fullscreen, large physical sizes. It's hard to communicate it clearly in little screenshots like this so just take my word for it. You can get a slightly better sense by seeing the swimming, flickering pixels in motion while zoomed.
Flickering dither and swimming lines
After thinking about this, it seemed to me that it should be possible to make the visuals more comfortable without adding actual detail, and without giving up the 1-bit limitation. My previous attempts were limited to upscaling the final buffer. That didn't give great results so I expected to try a little harder this time. It occurred to me that I could upscale each visual element (texture, lines) separately and combine them afterwards. Sounds harder already; good start.
Upscaling Dithered TextureAt 640x360, the game uses three separate dithering techniques: error diffusion, bayer pattern, and blue noise pattern:
The three dither techniques at 640x360
Each dithering technique is appropriate for different circumstances and their selection is important at these super low resolutions. With low res and bigger individual pixels, the eye has a harder time merging the dot patterns into shades of gray and I expect this is a major cause of eye strain. Doubling the resolution of the dithering (but not the underlying render) gives a much easier time of combining the dots into shades of gray:
Nearest-neighbor scaled from 640x360 to 1280x720, then dithered
Face closeup
.. Easier to see the grays, RIP chunkiness. It all looks a bit soft and the contrast between small dither pixels and thick line pixels clashes. Now that we've decided it's ok to cheat with mixed resolutions, and we've got all these extra pixels, why not try a coarser and more stylish dither pattern?
WB, chunk.
Looks rough but still captures the texture well, even without error diffusion
Rotating the dither pattern. Bad printer, classic newsprint, or tasteful woodcut? Choices.
Ok well that looks great and was a lot easier than I expected. Job half done.
Upscaling LinesThe wireframe lines were a little trickier. The goal was to upscale the lines into something that looks better and A) is still 1-bit (no blending) and B) doesn't add any information (no higher-res rendering). Since I played around with pixel-based scalers before I started by trying those again:
Upscaling the wireframes with pixel-scalers
There's a lot more algorithms than Scale2x and xBR but the others I tried end up with pretty similar results. Scale2x has the benefit of being fast and simple, xBR is slow and complex. Scale2x doesn't look good here, xBR is promising. After some research I found that xBR is a rule-based technique, designed for general color content. A significant amount of the work is spent on color separation and edge detection. 1-bit wireframes should be faster to handle, and if you're willing to write a custom pixel scaler, indeed they are.
Custom Line ScalerI made a brief stab at simplifying the xBR algorithm but quickly decided there was too much going on there and it'd be better to just design something new from scratch. Without going into too many details, the algorithm I came up with does a case-by-case matching of the 3x3 pixel neighborhood to determine what the 4 new subpixels would be if the line was rendered at x2 with the same endpoints.
Pixels that match a left-hand pattern are expanded into the right-hand subpixels
I wrote a small Python tool to work this out and there are 24 possible left-hand patterns to match, which is perfectly manageable in shader code. The game takes the x1 wireframe render (640x360) and expands it to x2 (1280x720) in one shader pass. After this initial match-and-expand pass a little more cleanup is required. Line endpoints can be a lot more varied than just the 3x3 square neighborhood, and the border between matched patterns in these lines can leave out subpixels here and there. These missing subpixels can be hack-patched in a following pass that compares new and old pixel positions.
Original x1 line on the left, upscaled x2 on the right
The algorithm handles short lines well and long lines less-well. It rounds corners and when summing all possible matches in each 3x3 neighborhood, adds a satisfying "bleed" around joints:
Custom line scaler result
Comparison with nearest neighbor and xBR
If we just wanted a higher resolution line we'd be done but in Obra Dinn's case pixels carry information about wireframe color (inDarkness or not) separate from the wireframe on/off channel. This means that the changes to the wireframe channel need to be back-applied to the other channels. That's again a fairly simple pass that updates the subpixel colors based on neighboring wireframe values.
All told, the custom method is much faster than xBR and looks better for this special case. I have a hunch the technique could be modified to handle full color data and used as a general-purpose pixel scaler. Maybe I'll play around with that after the game is done.
So, never.Putting It All TogetherCombining the higher resolution dither pattern with the custom line scaler makes a big difference. There's still swimming during motion due to the low resolution input data but it's way more comfortable to look at. The old display mode will remain as a selectable option and this new business will probably be the default.
I'm gonna gently close the book on this again. The final result:
Old/new comparison
Moving