Cuteness Comes From Motion, Not the Model
I sculpted the rabbit's body out of spheres and capsules, but standing still it's just a prop.
There's no keyframe animation file, so the movement all had to be built in code too.
Inside useFrame, I compute the deformation directly every frame.
Not having keyframes turned out to be a feature
It started as a compromise, but the upside became obvious.
A keyframe clip is a recording of a fixed length, which makes it easy to fall out of sync with the actual game state.
Speed up and you have to scale the playback rate; move between running and jumping clips and you need blending and a state machine.
The moment those disagree you get that distinctive foot-sliding wrongness.
Building it procedurally removes the problem entirely, because the motion is derived from the state itself.
It reads the current speed, whether you're on the ground, and which way the angular velocity points, then computes the deformation from those.
By definition it can't disagree with the game state.
Push the speed from 12 up to 30 and the hopping simply gets faster to match.
What came out of the state
The baseline is hopping synced to speed.
I make an up-and-down bounce with Math.abs(Math.sin(...)), and the faster you go, the tighter the hop cycle.
Near the landing the body squashes flat, and in the air it stretches vertically.
It's the textbook animation principle, and it lands unusually well on a lump of primitives — the simpler the shape, the more legible the deformation.
When turning, it banks slightly into the direction.
The value driving that lean is the same angular velocity that drives the movement.
There's no separate animation parameter, so the controls and the visuals can never tell different stories.
With a top angular speed of 3.2 rad/s, the lean gets a proportional cap for free.
The ears lay back as speed builds, then flap in time with the hops.
It's just rotation on two capsules, but it carries a surprising amount of the sense of speed.
I even tucked in a blink, squeezing the eye scale about once every three seconds.
Eyes closing for an instant is enough to suggest something is looking.
The front paws got rebuilt once.
At first I moved the whole paw back and forth, and they read as detached, floating alongside the body.
Putting a rotation pivot up at the shoulder and swinging that instead finally made the arms look attached.
To look like there's a joint, you have to actually rotate at the joint.
The highlight is the glance
The highlight is the "glance." The runner camera sits behind the rabbit, so the player only ever sees its back.
Leave it like that and nobody gets to see the face I worked so hard on.
So every 5.2 seconds the head group sneaks a look toward the camera, then smoothly returns to place.
glanceTimer.current += dt;
if (glanceTimer.current > 5.2) glanceTimer.current = 0;
const gp = glanceTimer.current;
let glance = 0;
if (gp > 0.8 && gp < 2.6) glance = Math.sin(((gp - 0.8) / 1.8) * Math.PI);
if (head.current) head.current.rotation.y = glance * -1.6;
The numbers took a while to settle.
Too short a cycle and it's constantly looking around, which is distracting; too long and you never notice it exists.
5.2 seconds landed on an interval you catch a few times a run without it nagging at you.
Slicing the actual turn to the window between 0.8 and 2.6 seconds serves the same purpose: most of the time it runs facing forward, and once in a while it looks back.
Shaping the angle with a sine matters too.
Rotate linearly and the start and end snap, which reads as mechanical.
A sine approaches zero speed at both ends, so it eases in and out on its own.
One function does what you'd otherwise reach for an easing library to get.

It's all cheap
What all of this motion does each frame is overwrite position, rotation, and scale on objects already held in refs.
Nothing gets allocated, and no React re-render is triggered.
A handful of sines and multiplications costs essentially nothing — lighter, in fact, than playing back and blending animation clips.
Once it was all done, one thing was clear.
It's the exact same lump of spheres and capsules, but the impression before and after adding motion was completely different.
Cuteness comes from movement, not model detail.
A bouncy little blob of spheres was far more alive than a static Stanford bunny.
Next time: the toon shading that painted this rabbit and tunnel in twilight tones.