ivãstival

ESP32 · 02-face

Animation asset formats

Note: This document was generated by an AI agent.

The character has two ways of storing an animation, and they are opposites. One records what the face does and re-renders it every frame; the other records what the panel showed and replays it. Both are compiled into the firmware — there is no filesystem — and the choice between them is a real design decision with a measurable cost.

Two format diagrams. The keyframe record UserKey is four bytes: a uint16 timeMs covering 0 to 65,535 ms, and an int16 value multiplied by the track's scale. The run-length encoded bitmap stream starts black and flips colour on every run, with a highlighted pair showing that 255 followed by 0 continues the same colour because the zero-length run flips twice. A comparison table below gives the measured costs of each.

Keyframe tracksRLE bitmap clips
StoresChannel values over timePixels, frame by frame
Rendered byThe 2.5D rig, liveUnpacked straight to the panel
ResolutionNone — it is not pixelsFixed at 128 × 32
Shipped size0 bytes (no user scenes)~57 KB

1. Keyframe tracks

Eight scene slots sit alongside the built-in collection, sampled from fixed-point keyframe data compiled straight in. The device never parses JSON, mounts a filesystem, or allocates at runtime for this.

The checked-in generated asset supplies zero scenes, so the firmware uses the built-in collection. It is included unconditionally — deleting it is a compile error rather than a silent fallback.

The record

Four bytes per keyframe, and each field is chosen:

  • uint16_t timeMs caps a scene at 65,535 ms — about 65 seconds. For comparison, the built-in head turn is 9.6 s and an ambient slot is 30 s, so the ceiling is six times the longest thing the device otherwise plays.
  • int16_t value with a per-track float scale is the fixed-point arrangement. The scale lives on the track, not the key, so a yaw track in radians and a handX track in pixels each get the full ±32,767 of resolution across their own range. One shared scale would waste most of it on whichever channel had the smallest range.

Capacities and what they cost

LimitValue
User scene capacity8 scenes
Track capacity per scene24 tracks per scene
Authorable channels26 of the rig's 41
Bytes per keyframe4

A fully-populated scene — 24 tracks of 20 keys — is 24 × 20 × 4 = 1,920 bytes. All eight slots filled that way would be 15,360 bytes — 0.8% of the application partition. The format is cheap; the limit is authoring effort.

Two properties that are easy to miss

Tracks are additive, not absolute. Applying a user channel adds to a default Pose. A track with no keys contributes nothing, and a scene that animates only yaw leaves every other channel at its neutral value. This is why a sparse scene is a valid scene.

Only 26 of the 41 channels are exposed. The withheld fifteen are the eye shape channels — widthL, widthR, heightL, heightR, radius, spread, pinch, squint, slant — plus mouthVisible, spiral, the particle emitter (spray, emitX, emitY) and phase, which the sampler sets itself. A user scene can move the character and drive its effects, but cannot reshape its eyes into something unrecognisable. The face stays the same face.

Playback

Authored time runs 100/78 = 1.28× faster than real time at the tracked speed, so a scene authored as 10 s plays in 7.8. Looping wraps; non-looping clamps, holding the final pose indefinitely — the same distinction the ambient scenes draw between a loop and a one-shot.

Walking the keys uses the same quintic minimum-jerk curve the built-in scenes use, so an authored scene moves with the same character as the rest of the rig rather than looking imported.


2. RLE bitmap clips

The other format stores finished pixels. Two clips ship:

ClipFramesSizeAuthored rate
anim_turbo_gauge256128 × 3216 ms/frame
anim_bug_sneeze288128 × 3216 ms/frame

The encoding

Each frame is a list of run lengths over the pixels in row-major order, starting with a black run, and every run flips the colour. One detail is worth pausing on:

A run of 255 followed by a zero continues the same colour.

A zero-length run of the other colour flips the colour twice, which lands back where it started — so the continuation needs no escape code and no special case in the decoder. The source comment says it "falls out of the flip naturally", which is exactly right.

What it saves

544 frames × (128 × 32 / 8)  = 544 × 512 = 278,528 bytes raw
RLE run data                              =  56,462 bytes
offsets, (256+1) + (288+1) uint16         =   1,092 bytes
                                            ─────────────
                                   ratio          4.9 : 1

About 57 KB, roughly 4% of the 1,399,376-byte firmware. Uncompressed they would not be worth shipping.

Why 128 × 32 on a 128 × 64 panel

The clips are half-height artwork. A fill-height setting doubles every row to fill the glass, which stretches every vertical proportion the animator chose; turning it off centres them at native size with a black band above and below. The tracked default fills.

The frame rate they cannot have

The clips are authored at 16 ms per frame — 62.5 fps. That is not achievable, and the reason is the bus rather than the CPU:

one full panel transfer   ≈ 24.7 ms      (derived in the hardware page)
authored frame interval     16.0 ms

At 100% speed every other frame would be dropped and the motion would rush. Doubling the playback speed to 200% doubles the interval to 32 ms — about 31 fps — which the panel can actually sustain, with ~7 ms of slack. Slower, and every frame drawn is a frame seen.

This is the clearest illustration in the project of the 25-of-33-milliseconds constraint: the artwork was authored for a display this device does not have.


Choosing between them

Use keyframe tracks whenUse a bitmap clip when
The motion is the character's own faceThe artwork is not a face at all
It should scale, tilt or compose with effectsIt is a fixed illustration
You want it to move like the rest of the rigIt was drawn frame by frame elsewhere
Flash is tightYou have the flash and no way to parameterise it

A face animation stored as pixels loses everything the rig gives it — the projection, the eased channels, composability with cues — and costs far more flash to do so. The bitmap path exists for artwork that was never parametric.


Producing the assets

The desktop authoring tool that generated the compiled-in keyframe asset has been removed from this tree. What remains is the contract a replacement must satisfy: the struct layout above, the per-track fixed-point scale, and times in milliseconds from scene start. Nothing in the runtime model needs to change for a new producer.

The bitmap-clip header names its generator script in a do-not-edit banner, but that script is not in this tree either. Both authoring tools have been removed and only their outputs remain. Regenerating either asset means writing a new producer against the documented layout; editing the generated headers by hand works but leaves the banner lying.

Failure modes

SymptomCause
Compile error about a missing headerThe generated user-animation header was deleted; it is included unconditionally by design
A user scene does nothingIt's marked disabled, or its duration is zero — both return the neutral pose
A user scene plays but the face is wrongTracks are additive; something assumed they were absolute
Animation runs 28% fastThe 3D playback speed setting applies to user scenes too
A non-looping scene freezes at the endWorking as designed — it clamps and holds the final pose
Clip playback rushes and skipsBitmap-clip speed back at 100% against a 16 ms authored interval
Clip looks vertically stretchedFill height is on, doubling 32 rows into 64
Decoder output drifts after a long runThe 255 then 0 continuation is being treated as a colour change

Configuration reference

SettingDefaultMeaning
Bitmap-only test modeoffPlay only the bitmap clips — a bring-up switch
Fill heightonDouble the 128 × 32 artwork to fill the panel
Bitmap clip speed200%100% is authored timing; 200% is what the panel can sustain
3D playback speed78%Applies to user scenes as well as built-in ones
User scene capacity8Scene slots
Track capacity per scene24Tracks per scene