Welcome, Guest. Please login or register.
Did you miss your activation email?

Login with username, password and session length

Advanced search

1331110 Posts in 60346 Topics- by 51721 Members - Latest Member: PasqualeBoi

February 21, 2018, 10:50:28 pm

Need hosting? Check out Digital Ocean
(more details in this thread)
TIGSource ForumsCommunityDevLogsReturn of the Obra Dinn [GDC 2016 Demo Build]
Pages: 1 ... 39 40 [41]
Author Topic: Return of the Obra Dinn [GDC 2016 Demo Build]  (Read 517768 times)
Level 10


View Profile WWW
« Reply #800 on: November 23, 2017, 01:24:48 pm »

Brilliant, thx for sharing!

Level 4

View Profile
« Reply #801 on: November 23, 2017, 01:32:16 pm »

Great work, man! I really do appreciate the work here and I'm sure others working on lo-fi games will benefit from this.

Living and dying by Hanlon's Razor
Level 10

View Profile
« Reply #802 on: November 23, 2017, 04:24:23 pm »

Fullscreen, Round 3

Thanks everybody for all the suggestions. I tried everything, literally, and concluded that the best way to maintain the game's style and fix the fullscreen discomfort was to stabilize the swimming dither and subdue the flickering dots. I got there in the end, with a few compromises. This is the 3rd full devlog post I've written on this. For each previous version I'd get an idea or find something else to try while checking over it. At this point, don't even care.

Dithering Process

First, a quick explanation. Obra Dinn renders everything internally in 8-bit grayscale then converts the final output to 1-bit in a post-processing pass. The conversion from 8-bit to 1-bit is handled by comparing each source image's pixel to the corresponding dot in a tiling dither pattern. If the image pixel value is greater than the dither pattern dot value, the output bit is set to 1. Otherwise it's 0. The output gets reduced to 1-bit and the viewer's eye will merge the pixels back together to approximate more bits.

Thresholding a source image by a dither pattern

The two components of this process are the source image and the dither pattern. Obra Dinn uses two distinct patterns for different cases: an 8x8 bayer matrix for a smoother range of shades, and a 128x128 blue noise field for a less ordered output.

bayer / blue noise

In-engine result without wireframe lines. Bayer on the sphere, blue noise everywhere else.

Hold Still Please

The basic dithering process works great for static images and much less great for moving or animated images. When the source image changes from frame-to-frame the static dither pattern and low resolution output become a major problem. What should be solid shapes and shades now read as a wiggling mess of pixels.

Moving the sphere

These days, dithering is mostly used when the source image is either static or the output has a high resolution. The first thought when seeing this low-res swimming dither effect is not "yeah that's how dither works" but "what is this warping shaking effect and how can I turn it off."

Exhibit A. Reduced contrast for your comfort.

Try to focus on something here when it moves and behold the crinkled heart of Obra Dinn's fullscreen problems. There are ways to fix this that mostly boiling down to "this style doesn't work, change it." I went pretty far down that path, experimenting with different styles, before swinging back and wondering if maybe I shouldn't let these bullshit little pixels push me around.

Stabilizing The Dither

To give your eyes the best chance at recombining everything, dithering works best when the dither pattern dots have a 1:1 correlation with the output pixels. But, correlating only with the output means that as a scene post effect there's no connection between the geometry being rendered and the pattern that thresholds it. Each frame, moving scene elements threshold against different values. What I want instead is for the dither pattern to be "pinned" to the geometry and to appear stable as it moves with the rest of the scene.

The core of this is a mapping problem. As told by the length of this post, there's a conflict between the ideal dither pattern mapping (1:1 with the screen) and the ideal scene mapping (x:1 with the geometry) so get ready for some compromises. Most of my work was focused on mapping the input dither pattern into different spaces that better correlate the pattern with the scene geometry. Everything here is done at the pre-thresholding stage.

Texel Space

My first try was to map the dither pattern in texel space. This is equivalent to dithering the object textures during scene rendering instead of in a post-processing pass on the 8-bit output. I didn't expect this to work well but wanted to see what a perfectly scene-matched mapping looked like anyways.

Dither pattern in texel space

Ok well, expectations solidly met. The objects are all mapped differently so their pattern scales don't match. Those could be unified. The real problem is the aliasing. Any resampling from one space to another like this will result in aliasing, and dither patterns can't be easily mipped or filtered like traditional textures. Still, to carry it through:

Applied to the moving scene

This isn't a total loss - the pattern is nicely pinned to geometry. The aliasing produces its own swimming effect and unifying or scaling the mappings won't help with that. Texels change size with distance from the camera so there will always be dither pattern pixels that alias badly when resampled to the screen.

Motion Warping

If I want the dither pattern to track the moving geometry beneath it, why not just warp the pattern using the change in position of each rendered pixel in the scene? Indeed why not. This is a bit like a motion blur, where each pixel tracks its movement from the previous frame. In this case, I update the dither texture to keep its pattern moving with the scene. If a scene pixel was not represented in the previous frame, the dither pattern is reloaded there. This technique is made much simpler by the game's static-ness - I only need to worry about the movement of the camera, not individual objects.

Warping the dither pattern to maintain frame-to-frame coherence with the scene

This was a pretty quick & dirty try but a few things are clear. First, it kinda works. Second, a dither pattern needs a neighborhood - it can't be individual pixels. If you consider each pixel individually, as this method does, then you'll get breaks and discontinuities in the pattern which are obvious. I shifted the camera in this test scene to highlight those on the chest here. Viewing the warped dither pattern itself makes this a little easier to see.

Thresholding solid gray with the warping dither pattern

These discontinuities are down to the differing pixel depths and thresholds that I chose. I reasoned an elaborate fix based on tracking regions, averaging their depth and shifting all dither pattern dots in that region by the same amount. A discontinuity along a region boundary could be hidden by sharp lighting changes or a wireframe line. This would've been enabled by the game's existing setup of colored regions for the wireframe generation. When I sat down to implement all that, the depth term dropped out of the first equation I came up with and gave me a much simpler alternative:

Screen-mapping With Offset

When putting together the equations for the warping dither, a very simple transform fell out:
DitherOffset = ScreenSize * CameraRotation / CameraFov

Shifting the screen-mapped dither pattern based on camera rotation

Basically, this expresses that I want the screen-mapped dither pattern to shift by exactly one screen when the camera rotates through one field of view. That maintains a 1:1 mapping with the screen while also considering a simplified transform of the scene geometry in view. This really only matches the movement at the center of the screen but bless this fucked up world because it's nearly good enough.

Offsetting the dither pattern to track one screen per camera fov rotation

Note how the dithered pixels on the chair appear to mostly move with the geometry. Likewise for the sphere. Planes more perpendicular to the view don't match very well; the floor is still a mess.

So while not being perfect, simply shifting the screen-mapped dither keeps the overall pattern and scene movement close enough that the eyes can better track them together. I was pretty happy with this. While cleaning up the code and committing everything, maybe writing a devlog post or two, the idea of a perfectly-pinned dither kept nagging at me:

World Space - Cube Mapping

The experiments so far suggest that any correlation between the dither pattern and scene geometry would have to ignore depth information from the scene. What this means practically is that the dither can be pinned to the geometry during camera rotation but not during camera translation. This isn't such a bad thing for Obra Dinn considering the slow pace of the game and the observational role of the player. You're normally walking around, stopping, and looking at things. When walking, so many things are changing onscreen that the swimming dither isn't as obvious.

With that in mind, my next attempt was mapping the dither pattern to the geometry indirectly by pre-rendering it onto the sides of a cube centered around the camera. The cube translates with the camera but stays oriented to the world. In the mix: little bit of screen, little bit of scene.

Dither pattern mapped to a cube centered around the camera

Camera's view looking up into a corner. Mapping scaled up for clarity.

The cube's mapping works well when looking directly into the sides, and not so well when aimed into a corner. Still, the dither pattern stays perfectly fixed in 3D space as the camera rotates. Even rough, the result is promising.

Thresholding scene with the cube-mapped dither pattern

Now we're talking. Being a post-processing pass makes this more general than texel-space mapping, which is good. The problem is now down to the particular cube mapping. An ideal mapping would have one texel on the cube always resolve to exactly one pixel on the screen, regardless of the camera rotation. That's not possible with a cube...

World Space - Sphere Mapping

...but I got pretty close with a sphere.

Mapping the dither pattern onto the inside of a sphere

Finding this particular spherical mapping took some time. There's no way to perfectly tile a square texture onto a sphere. It would've been possible to redefine the dither matrices in terms of a hexagon grid or something else that does tile on a sphere. Possible maybe, I didn't try. Instead, I just hacked on the square tiling until this carefully tweaked "rings" mapping of the original dither pattern gave good results.

Applied to the scene

Better than the cube. Still lots of aliasing. The spherically-mapped dot size is very similar to the screen pixel size - off just enough to cause moire patterns. I could feel the closeness, and a very simple fix for this kind of aliasing is to supersample: apply the dither thresholding at a higher resolution and downsample.

Spherically-mapped dither pattern at 2x and downsampled to 1x

Thresholding at 2x, then downsampling to 1x

This is the best I got. There are a few compromises:

    1 The dither pattern dots get larger and less effective at the edges of the screen
    2 The pattern isn't aligned up-down-left-right for most camera rotations
    3 The output is no longer 1-bit due to the final box-downsample

But the upside is pretty lofty:

    1 The dithering is perfectly pinned for all camera rotations. This feels slightly uncanny in-game.
    2 Discomfort from swimming dither is totally gone, even at fullscreen
    3 The pixellated style of the game is preserved

It's possible eliminate compromise #3 by reducing the output back to 1-bit with a simple 50% threshold. The result is still better than without supersampling (the triple comparison directly below is thresholded).

Side by side, by side

In the game's default palette


It feels a little weird to put 100 hours into something that won't be noticed by its absence. Exactly no one will think, "man this dithering is stable as shit. total magic going on here." I don't want to give people problems they didn't know they should have though so it was worth fixing.

The screenspace mapping with offset works best at 1x and the sphere mapping works best at 2x. All scene rendering is at 800x450 now (up from 640x360), which helps legibility without sacrificing the low-res style. The final game will have two display modes: 

Border-boxed, screenspace offset dither, 1-bit output

Fullscreen, sphere-mapped dither, softened output

Quoting because pagination, and I'm sure there will be some useful discussion. For starters:
I guess the non-square pixels in the final output are also an inevitable compromise? It kind of breaks the suspension of disbelief.

I just realized that this compromise may not need to be inevitable, if you apply the treshold at the original pixel resolution (instead of this 2x blow-up). Of course, that would re-introduce the aliasing on the sphere, but if you only apply "lower-res tresholding" to the non-Bayer parts you would avoid that.
Canned Turkey
Professional Otter
Level 10


View Profile WWW
« Reply #803 on: November 23, 2017, 09:45:35 pm »

Once again proving yourself as probably the most inventive developer alive. Wonderful work  Smiley

Level 0

View Profile
« Reply #804 on: November 24, 2017, 02:27:50 am »

Amazing writeup!

Level 0

View Profile WWW
« Reply #805 on: November 24, 2017, 05:19:39 am »

Extremely fascinating project. I remember reading about it when it started some years ago. Glad to see you're still in the saddle. (Not much more to add, just wanted to cheer you on and to follow this thread) Gentleman

As always,

as always
Level 0

View Profile
« Reply #806 on: November 29, 2017, 02:16:17 pm »

Well done with the "screen-mapping with offset" solution. It honestly looks indistinguishable from the original dithering, but is already a lot easier on the eyes.

However, the shape-mapping stuff looks wrong to me. It's still way better than what you had though, and maybe at a larger screen resolution and with a little bit of softening, it doesn't break the illusion?

View Profile
« Reply #807 on: December 02, 2017, 02:14:00 am »


If there will be no lateral camera movement, only rotation, then you can render whole scene as spherical panorama and then show only needed part. This will guarantee that all pixels will stay in place.

View Profile
« Reply #808 on: December 03, 2017, 10:39:47 am »

Fullscreen, Round 3
Thanks everybody for all the suggestions. [...]

Thanks Lucas for all the work! Would you get some celebration time and show us a short video with the title screen and some already disclosed views within the ship? I'm very curious about seeing the new dither-like effect in more frames Well, hello there!
Level 0

View Profile
« Reply #809 on: December 21, 2017, 07:25:31 am »

Thanks for sharing all those details about your process! I think those 100 hours will be repaid tenfold in playtime thanks to the improvement. It would be interesting to know if anyone / any game has done anything similar in the past to achieve this or if you are the first.

View Profile
« Reply #810 on: December 26, 2017, 05:53:49 am »

Just played the demo, and I have to say it does feel magic being able to play 1bit game on a retina screen and it feels awesome, so totally worth it Smiley (try and do that with non-optimised 1bit games!) But all that apart, and god is in the details for sure, the gameplay and camera and story feels really compelling, really want to discover more about what happened to Obra Dinn. Can't wait for the next release. Thanks for sharing!
Pages: 1 ... 39 40 [41]
Jump to:  

Theme orange-lt created by panic