The avatar standing on my desktop looked like it was stuttering. One symptom — but it turned out to have three causes of completely different kinds.
Check them in the wrong order and you waste a lot of time. I started with interpolation, which was the wrong end.
1. First, suspect overlap
This was the big one. The next motion was starting before the previous one had finished.
The interval was set to 5 seconds. The motion was 9 seconds long. Calling it again mid-playback resets elapsed time to zero, so it snaps back to the first frame partway through. There's no chance to settle back into the idle pose before the next one begins. That reads as "stuttering".
The symptom is jitter, but the cause is the timetable, not the curve. No amount of smoothing fixes it.
Two things to check:
- Is the next run scheduled when the motion starts, or when it finishes?
- Does your "skip if already playing" guard cover every playback path?
I got caught by the second. The guard only consulted a flag used by the .vrma path, and the procedural-motion path walked straight past it. A gate on one of two doors is not a gate.
2. Then, suspect velocity discontinuity
Eyes pick up discontinuity in velocity, not position. Move at a constant rate and stop abruptly, and the stop itself looks like a jolt.
Two common sources:
-
Linear trapezoids (ramping with
t / rise) — velocity jumps at both the start and the peak -
Math.abs(Math.sin(x))— forms a sharp point at every zero crossing.(1 - cos) / 2is the drop-in replacement
Detecting a kink without picking a threshold
This is the practically useful part.
My first attempt divided the jump by the amplitude and compared against a threshold. That doesn't work. Larger motions are penalised, so legitimate movement trips the check.
Instead, double the number of samples and see what happens to the jump.
| When you double the samples | |
|---|---|
| A smooth curve | the jump halves — you were only sampling curvature |
| A real kink | it doesn't change — there is an actual corner there |
Independent of amplitude and of speed. And no threshold to guess at.
3. Last, check whether it's visible at all
If you've fixed the smoothness and something still feels off, the motion may not be visible in the first place.
Movement along the depth axis — leaning forward, for instance — barely changes the image from a front-facing camera. I measured one pose where the top of the head moved 10 px. That's 0.65% of the screen height.
"Something feels off" turned out to mean "effectively nothing is happening."
Don't measure in degrees. Measure how many pixels it moves on screen. However far it rotates in 3D, if the camera can't see it move, it didn't move.
Writing the lesson down doesn't stop the repeat
About that third one, a confession.
It was already written in a comment in the same repository.
Forward/back motion runs along the screen's depth axis, so from a front view it looks like it isn't doing anything
I hit it once with a folded-arms pose and wrote that down at the time. Then I hit it again, somewhere else, having written it myself.
Keeping the lesson in a note was not enough. Unless a machine fails the build, you repeat it.
Smoothness now has npm run check:smooth, implementing the sample-doubling test directly.
Visibility I still measure by hand. That hole isn't closed yet.
Takeaways
- Suspect overlap first. Jitter doesn't imply the interpolation is at fault
- "Skip if playing" must see every path. A gate on one door is not a gate
- Detect kinks by doubling the samples. Dividing by amplitude penalises big motions
- Visibility is measured in pixels. Degrees will lie to you
- Turn lessons into tooling. You will hit your own notes twice
Separately from this, I build a desktop AI agent called Wisp. It stands on your desktop, answers when you talk to it, and runs commands when you ask — always showing you what it's about to do first.
Top comments (0)