Building/manipulating grayscale images (and general offscreen drawing) in lua?

Just want to make sure I'm not missing a trick before I do something moderately painstaking:

I was playing the Analogue Pocket's Spacewar! reproduction and it got me thinking the PDP-1's ultra-high-persistence phosphor -- like 2 seconds to fully fade -- might actually look cool on the Playdate. I did a bit of testing and it's surprisingly legible.

The test version is slow though, because it reassembles the whole phosphor trail from scratch every frame. What I'd hoped to do is maintain an 8-bit half-res grayscale bitmap of the whole screen to represent the phosphors, drawing copies of my sprites to the phosphor bitmap and fading that bitmap a little bit every frame, then dithering that out to screen. If I'm exceedingly clever, I might be able to manage dirty rects just for the places that the trails cross the dither pattern thresholds, but the other draw tasks are simple enough and the Playdate draws fast enough that I could probably spit the whole thing out every (or every other) frame.

Trouble is, while the Playdate APIs pass images through grayscale bitmap manipulation, they never expose those bitmaps to the caller, so I can't use successive passes of fadedImage() and blendWithImage() to assemble and maintain that phosphor map. So I think I need to just keep my own 200x120 table of bytes and update/render it out myself? As far as I can tell the only way to read/write images to/from my table is with image:drawPixel(x,y) and image:sample(x,y), which seems... well, it seems like a lot of calls, but I guess they're fast. Won't be too miserable as long as drawPixel treats dither patterns as the "current color", but it would certainly be nice to be able to treat bitmaps as tables and vice versa now and then.

This all seems sensible enough and somewhat tractable, but it feels a little bit too difficult for things that I'd think of as fairly basic image manipulation. Is there a better way that I'm missing?

(I saw librif, but that doesn't look like you can draw to the grayscale bitmaps, just render existing ones out with transformations.)

In case you haven't seen the PDP-1's awesome display, this is a real nice demo:

Cool video! Interesting idea.

How about this... given that dither patterns impose a limit on the number of perceivable steps between on/off, don't fight it and simply store a stack of, say, 8/16/32 layers (assuming you'll use Bayer8x8).

You'd draw the current image to the top layer and then every so often rotate the stack. Each image could be drawn with a different dither pattern/"opacity".

I use this method in my games but with only a couple of layers. The benefit is that you don't have to do any per-pixel work.

If I'm off target, feel free to shoot me down! :rocket:

1 Like

Huh. That's a pretty good idea. I had already considered doing the phosphor update at half-rate, but this would effectively reduce the frame rate of the dimming effect even more, and so several frames of animation would fade out each glowmap update. Making the glow change slowly would detract from one of the cool things in my prototype (attached below -- it's just my PONG-like, which now has 'XL' and 'XXL' phosphor trail options), where twitchy paddle movements gain a sort of instant-replay effect. But I wasn't going to do something that fast-moving anyway.

I don't actually need to match the number of layers to the number of dither patterns available -- there's nothing saying I can't repeat patterns across layers, and 8x8 is probably too big to work well with smaller details. And I don't think the phosphor falloff is linear anyway.

If the Playdate will actually let me get away with laying down 32 layers of phosphor memory (I guess I can if I'm smart about what I mark dirty) then even if I wanted the PDP-1's lackadaisical 2-3 seconds of fade (and I don't know for sure that I do) that gets me 11-16 FPS in the glow refresh. It might be subtle enough to get by. I'll try it out.

ball.pdx.zip (39.3 KB)

1 Like