Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

86 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Surface-Stable Fractal Dithering on Playdate

Rune Skovbo Johansen has a really sweet Surface-Stable Fractal Dithering technique, where the dither dots "stick" to 3D surfaces, yet the dot density adapts to the view distance and zoom level.

Some people have asked whether this would be a good technique for Playdate, given that the screen is one-bit color. And so I had to try it out! Here's a video:

Surface-Stable Fractal Dithering on Playdate

I wrote a blog post about this whole porting and optimization journey: aras-p.info/blog/2025/02/09/Surface-Stable-Fractal-Dither-on-Playdate/

My impression: not practical, really. Playdate hardware is like a PC from 1995 - no GPU at all, one fairly simple CPU core. As such, it can do fairly simple 3D rendering (well, you need to write the whole rasterizer on the CPU), but can barely do more than a handful of math operations per rasterized pixel. Rasterizing with screen-space fixed Bayer or Blue Noise dither patterns is the way to go due to their simplicity.

Screenshot

However! It was fairly fun hacking on this. Amusingly enough, I have never written a perspective-correct interpolating triangle rasterizer, etc. So this was a good learning experience. For reference, a non-perspective-correct vs. correct texture coordinate interpolation:

Not correct Correct
Screenshot Screenshot

Current Status

  • The really simple scene in the first image runs at 22 frames per second on a Playdate. Initially I had it running at 1.5FPS. Currently it has some simplifications and behavior changes compared to full Rune's technique:
    • Dot spacing is fixed (6.0), contrast is fixed (1.0), size variability too (0.0).
    • Dither pattern is either 2x2, or 4x4 but with texture XY resolution reduced twice (i.e. 3D texture is 32x32x16).
    • No anisotropic derivatives handling nor contrast tweaking based on that.
  • I have two rasterizer approaches:
    • Traditional "scanline" one, very much like in Chris Hecker's 1995/1996 article series. Perspective correct UV interpolation is done every 8 pixels. The dither pattern spacing is calculated only at triangle vertices and interpolated across. Runs at 46ms/frame, code in draw_tri_dither3d_scanline.
    • Halfspace/barycentric one, processing 2x2 pixel blocks in one iteration, and doing perspective correct UV interpolation every 2 pixels horizontally. Runs at 57ms/frame, code in draw_tri_dither3d_halfspace.
  • It is entirely possible that there's a ton of low hanging fruit w.r.t. optimizations that I have overlooked so far.

Most of the code layout and structure is taken from Everybody Wants to Crank the World demo I made in 2024. As such, it compiles on Playdate, and on PC as well (via Sokol).

Application Controls

  • Crank (playdate) / Mouse scroll wheel (PC) orbits the camera,
  • Up/Down moves camera closer / further,
  • A toggles wireframe (default off)
  • B toggles between "real scene" (default) or "unit test" scene that I was testing the rasterizers with
  • Left/Right toggles between different rendering techniques, indicated by a: number in the upper timings line:
    • 0: scanline rasterizer with simple dither pattern,
    • 1: scanline rasterizer with blue noise dither pattern,
    • 2: scanline rasterizer with checkerboard based on UVs,
    • 3: halfspace rasterizer with checkerboard based on UVs,
    • 4: scanline rasterizer with Dither3D effect (default),
    • 5: halfspace rasterizer with Dither3D effect.

License

Everything I wrote myself is Unlicense / Public Domain. However some 3rd party libraries are used too:

About

Surface Stable Fractal Dithering on Playdate

Topics

Resources

Stars

40 stars

Watchers

1 watching

Forks

Contributors

Languages