Accelerometer demo - pushing the limits of physical immediacy

I'm working on some Lua bindings for Chipmunk2D and I was curious what I could get if I wedded that to the accelerometer, so while I was stuck on the bindings project for a while I decided to see how far I could push accelerometer input to achieve a convincing sense of an actual physical object under glass without any meaningful collision detection/restitution code. Tuns out: pretty far! I started with the AccelerometerTest SDK sample and just kept bolting tweaks on till it felt like there was a real thing sliding around inside the Playdate. I think this came out pretty cool, and gestures at some untapped potential in the device. It's not too impressive in the simulator, but in the hand I think it's kind of remarkable.

accel.pdx.zip (26.0 KB)

What I had to do to that SDK demo before it looked genuinely convincing:

  1. The object on screen and the accelerometer forces should be scaled to Earth gravity and the scale of the screen. In the Wii days we used the accelerometer as sort of an abstraction of forces to be imparted on something represented by the image on the screen, but with the Playdate you have an opportunity to set the accelerometer's 1.0 value (which represents 1g) to something that, if the device is held vertically, actually accelerates an object on-screen 9.8 m/s^2. This has cool side effects, like if you slide the Playdate laterally, the object on-screen stays still in real space, like a vase on a pulled tablecloth.

  2. A simple Coulomb friction model with separate sliding/static friction values is pretty trivial to do. Fluid drag (a function of cross section and velocity) is even easier. Multiply the absolute value of the accelerometer Z axis by g and mass, and you have a reliable normal force for friction. Less massive objects will naturally be more affected by fluid drag than friction, which looks correct; also you can shift a heavy disc by tilting slightly and tapping the bottom of the Playdate (inducing the console to accelerate downward for a moment on rebound, relaxing normal force and releasing friction). D-pad up/down changes static friction, left/right adjusts dynamic friction, B/A adjusts drag coefficient (effectively the viscosity of the working fluid), and the crank changes the disc size (and mass).

  3. There are a few physics stability issues (mostly owing to that this is not a real physics engine, and the edge-bounce logic is naive) when the physics only update at about 50Hz. So turn off the frame limiter, and don't make playdate.update() run the graphics every time through. Instead, run the physics and graphics on separate rate limiters. With this, when the disc isn't too big you can run physics at 200Hz easily, and run graphics around 60Hz. So long as you keep track of how much time has actually passed since the last physics update, you can pass that interval back into your update code to avoid having the world slow down when frame rates start to falter. I don't know why it took me so long to realize I didn't actually have to update the screen with every update(), and that having my own screen update limiter within update() would only be a few lines of code. Cool notes: you can totally poll the accelerometer that fast, though I haven't actually tested how often the reported values will actually change. Less cool: every time you call update() the other inputs get polled, and the value that getCrankChange() returns will be flushed. So I have to query and deal with user input every time we enter update(), even if we're not touching physics or graphics that cycle, which isn't awesome.

  4. This produces physically stable, familiar results, but even at 60Hz a fast-moving small disc will have stroboscopic effects -- you see a series of widely spaced out discs. Fortunately you can get a really effective motion blur just by linear interpolating between the last position and the current position, drawing the disc four times with four different 25% opaque alpha patterns that overlay to form an even black. Even when the disc is tiny and moving really, really fast, persistence of vision holds up and this maintains the appearance of a continuous streak. At 60fps, this works astonishingly well, and the code is pleasingly trivial. A 4x blur is a bit demanding for larger sprites, though, and a more adaptive solution would help. Still, I'm shocked at how nice this looks for next to no implementation:

playdate-20230405-003037

This looks remarkably impressive on the Playdate -- all this put together really sells the actual-object-under-glass effect I was trying to achieve. I'm sure I'll have to ask a lot less of my update rates (and blur method) when I have meaningful image detail and an actual game and physics model running, but it gives me hope that that pinball-ish world-under-glass sense is still going to be within reach.

4 Likes

The idea of using interfering dither patterns for motion blur is absolutely brilliant. Bravo!

Thanks! I don't know about absolutely brilliant, but it definitely feels like how the Playdate wants to draw things, and 4x motion blur at 60Hz looks pretty dang good. It would probably be worthwhile doing some sprite extensions that handle this type of blurring for the dev and do it as an adaptive thing based on object speed, desired blur quality, and available overhead.

The most Playdate-specific blurring idea I can think of is actually doing adaptive interlacing when the system starts to bog down. Has anyone messed with this? I understand that the Playdate can hit 50-60 fps redrawing the full frame now, but at least in my naive demo it seems to eat a lot of that 16-20ms doing so, not leaving much headroom for other stuff. If the major component of screen update is still flushing individual line buffers to the display (from some old prerelease sdk discussion on this forum), and there's no tax to doing non-contiguous lines, interlacing seems like a fairly compelling option, at least when most of the movement is horizontal or you're drawing double (or double-height) pixels.

Anybody try shaking this around yet? The effect of light taps to the bottom of the device is pretty cool and I was just really impressed by the performance of the accelerometer.

This is so cool on device!

In the Sim I got this error:

main.lua:75: attempt to perform arithmetic on a nil value (local 'accelerX')
stack traceback:
	main.lua:75: in function 'applyExternalForces'
	main.lua:122: in function 'updatePhysics'
	main.lua:269: in function <main.lua:264>

The display is sent dirty rows so you will need to render, say, odd rows only over a series of frames to get a benefit. Rather than rendering alternate rows every other frame which would still mean the whole screen is dirty and redrawn.

Thanks for the bug. I’m on a slightly old SDK version (owing to weak character I guess) so I didn’t see that error. Also the code is hideous — just bashing a few concepts haphazardly into the simplest drawable thing, to see what it would look like in hand. But it’s not my fault the Playdate makes it so easy to get half-considered slapdash code running on a handheld before you have time to think better of it.

Now I’m confused. If an interlaced drawing routine were to only draw odd-numbered rows on odd frames and even-numbered rows on even frames (like old TV sets), why would every row be considered dirty on every frame?

you're correct, that should work and not be always dirty. if you try it please do report back.

(i was recalling something else similar that NaOH and myself tried in their 3D engine, so ignore me)