Engineering Journal

← all posts

Part of Building the 3D Club Scene

The Plant That Walks

DebuggingTesting & QA3D & motion

My extractor labeled 15.5 seconds of a 30-second take as a single foot "plant". Across that span the performer's ankle traveled 1224mm and her foot heading swung 179.5 degrees. She was walking, the script said standing, and every downstream system inherited the lie. Five solver campaigns died on the symptoms before anyone audited the data, and telling the truth made things measurably worse before it made them better.


I own exactly one pole-dance motion-capture take: thirty seconds, one performer, bought outright, and no second one to buy. Around it I built an extractor that pulls out what I call the essence, a semantic contact script saying which hand grips the pole in which height band over which span of time, which foot is planted, which leg wraps, plus style curves for how she moves between those facts. Three bodies with quite different proportions (ch33, ch02 and medea) then solve their own motion against that script.

The usual approach, classical retargeting, copies the source skeleton's joint rotations onto the target bone for bone and lets the limb-length differences come out in the wash. Against a pole they do not. A grip is a claim about where a hand is in space, and a copied elbow angle on longer arms puts that hand somewhere the pole is not. Contract, not rotations.

The hard part was legs going through the pole. This post is about the stupidest thing I found while killing that defect class.

The span that lied

In the shipped contact script there is a right-foot span of class floor. A plant. It runs from 16.84 seconds to 2.77 seconds, wrapping across the loop seam: 929 frames at 60Hz, about 15.5 seconds of a 29.53-second take. Slightly more than half the performance is, according to my own extractor, one continuous instance of a foot standing still. computePlantTargets() believes it and freezes exactly one anchor at the span's onset frame, which is the correct implementation of the contract as written.

Here is what the performer actually does across those 929 frames. Her ankle's positional spread is 1224mm, measured in ch33's stage space. Her foot heading swings 179.5 degrees at maximum deviation, essentially the full compass. She is not standing. She walks, pivots, and comes back.

These are the three spans plotted against the heading each one started at, from the shipped script's own published yaw envelopes. A plant freezes one anchor at its onset frame, so every degree here is error handed to the solver before it does anything.

The solver has a notion of a side corridor: per frame, where around the pole a contact is allowed to sit, derived from the source's own azimuth. I wired a probe to dump each plant's worst in-span deviation from it. A healthy right-foot span in the same take (8.34 to 10.26 seconds, 115 frames, 136mm of ankle spread) had zero frames outside. The mega-span had 482 of 929 outside, worst deviation 165.8 degrees against a corridor 39.1 degrees wide. So "place this plant on the correct side" was not unimplemented for that span, it was unsatisfiable. One frozen anchor cannot sit inside a plus-or-minus-39-degree window whose center roams 165 degrees.

The origin of the lie is boring and defensible, which is the part I keep chewing on. The extractor releases a floor contact when the foot rises above GND_OFF_HARD_ABS, 0.2m. That replaced a 0.075m threshold for a documented reason: the old value sat directly on a real 7.41 to 7.53cm foot bounce in this take and chattered. But in a step, drag, restep passage the foot never rises 20cm, so the passage collapses into one contact. The log entry making that change flagged the consequence out loud. Nobody, me included, connected it to anything for weeks.

Five campaigns of chasing the symptom

Five solver campaigns went after these crossings before anyone audited the script.

Grip-azimuth yield caps went first. Falsified: crossings went up rather than down, and two figures came out byte-identical at every cap strength, which should have told me the solver was not the thing choosing them. Per-frame side-corridor projection was built out fully and then killed by geometry. A per-frame azimuth clamp moves a segment along an arc, my swept-collision standard interpolates joints linearly, so any rotation big enough to fix a side violation chords straight through the pole. One foot went from 6.4mm of penetration to 122mm. Window-level leg routing: null result, three measured walls, two of them the mega-span in disguise.

Then press-not-cross yield, which produced my favorite one-line bug in the project. The penetration-relief rung it was meant to replace does this:

_target.set(_C.x * (1 + push / r), _C.y, _C.z * (1 + push / r))

It scales the ankle outward along its current azimuth. Once a leg has drifted to the far side of the pole, that pushes it further out on the wrong side and reports the penetration relieved. The topology error is not just unfixed, it is preserved.

I do not regret any of those one at a time. Together they add up to a week of treating a data defect's symptoms as bugs in a solver.

The audit that should have gone first

I found the mega-span by hand, once, with a stderr probe. To make the defect class detectable without hand work I wrote tools/span-audit.mjs. It reads a script and a style file: no bake, no body, no solved root, no browser, seconds to run.

What mattered was scoring each span against its own type's contract rather than one global rule. A floor plant freezes one anchor, so its contract is stationarity and travel is the defect. A floor-drag is never pinned, so travel is the point. A grip legitimately slides, so its test is seatedness, not stillness. One "must not move" rule would have been wrong for four of the five span types. It also triggers on excursion rather than path length, so a real plant that jiggles for four seconds without going anywhere is correctly left alone.

First run against the shipped script: two pathological spans of sixteen. One was the mega-span I already knew. The other was new and nobody had ever named it, a left-foot "plant" from 17.98 to 26.68 seconds across which the source's toe travels 3.5 meters and 42% of frames sit outside their own corridor. It had sat there through the entire prior campaign, on the other foot, in the same passage.

The repair splits a roaming plant into dwell segments separated by transit gaps, from the source's own toe kinematics, both thresholds calibrated on this take's own genuine plants. They at least sit on plateaus rather than on a knife edge: the output is identical for seed speeds 0.18 to 0.30 m/s and for radii 0.15 to 0.25m. Calibrated on this take and then checked on this take, though, with no held-out passage and no second take to try it against. Then a validation I had not designed for: the six dwell onsets it produces (16.84, 18.53 and 1.67 right, 17.98, 21.51 and 23.31 left) land exactly on six span boundaries in the legacy height-based extraction at the old 0.075m threshold, which shares no constant and no line of code with it. Two methods agreeing on one toe trajectory, not two independent looks at her feet, but I did not expect them to land on the same frames. Sixteen spans became twenty-two, zero of them pathological, every plant now sitting 70 to 147mm from its anchor rather than 617 and 671mm.

And the predicted defects died. Four of the five stubborn support-limb crossings collapsed outright, the fifth survived 69% shallower (22.8mm to 7.0mm), and a sixth, unpredicted one went too, a routing window I had already spent two failed strikes on. Contact-classification F1 rose on all three bodies (ch33 .931 to .949, medea .921 to .936, ch02 .910 to .936). That metric scores each solved body's rendered contact state against the script it was handed, so what rose is agreement between script and solve. Nowhere in this project is there a hand label of what her contacts actually are, which is the check I still owe it.

Separately, a foot-orientation investigation converged on the same span from a different direction. Sideways-turned feet, which I had spotted by eye and chased as an orientation bug, are at 18.1 to 18.3 seconds and 20.3017 seconds. Both sit inside the mega-span, whose heading data is meaningless by construction given that 179.5-degree spread. The orientation solver was faithfully tracking a signal that could not mean anything.

The twist: honesty made it worse

With the segmented script in place, plus one mandatory matching solver change (a short wrapped plant must not have its anchor averaged across the wrap, because the midpoint of a short wrap is a point the foot never stands on), the crossing count went from 15 to 17.

Worse. Reproducibly worse, on a script that measures more truthful on every test I have. The two mechanisms do price separately, for the record: segmentation on its own also lands at 15, but it fails four ratified gates on medea, so the wrap-anchor change is not optional and its two crossings are the honest cost of the pair.

The regression is one window. 25 of ch33's 34 newly emerged frames sit in 24.93 to 26.20 seconds, inside a newly correct left-foot dwell from 23.31 to 26.40 seconds. That dwell's source ankle passes closer to the pole than any other plant in the take, 183mm minimum radius against 236 to 511mm for the rest, and the plant's radial safety floor (rMin = POLE_R + 0.1, about 123mm) bounds the ANKLE point, not the foot segment. So the ankle sat exactly on its floor while the foot capsule swung within 2mm of the pole axis, roughly 113mm into a 115mm keep-out an ankle-only floor cannot see. The lying script had hidden that by pinning the foot to an anchor eight seconds earlier and hundreds of millimeters away. Never correct. Just wrong in a direction that happened not to cross.

So I fixed the floor: a per-dwell radial floor from the target body's own pre-solve posed geometry, binary-searched for the smallest anchor radius where every dwell frame's predicted foot and shin clear their keep-out. On ch33's target span that finds 142mm from the foot term alone, 245mm with the shin term. The prediction is pre-solve, so the number that counts is the re-baked inventory afterwards: crossings fell to 13 across the corpus, below the 15 baseline, medea at zero, the best number the project ever measured.

And that tripped the next layer down. With the shin term active, medea fails a ratified continuity gate at 18.94 seconds: left knee, 23.8 m/s against an 18 m/s cap, a 727mm jump in one 60Hz frame. Not a local chord violation at a dwell boundary, which I checked, but a cross-span propagation: a smaller push on a different span earlier in the take changing a later frame's outcome. That knee became its own investigation, five fix attempts across three tasks, the other half of this same week, written up separately in The 727mm Teleport Was the Fix, Not the Bug.

Three layers, each load-bearing for the one above. The dishonest script masked an ankle-only clearance flaw. That flaw masked a cross-span propagation bug. Peel one and the next shows up wearing the first one's clothes.

What I would do differently

Audit what the data claims before fixing what the code does. span-audit.mjs took a few hours to write, would have redirected the entire week, and on its first run found a second instance of the defect in a script five campaigns had already been optimized against. A semantic contract is a claim, and claims can be checked without running the thing that consumes them: "this foot is stationary here" is testable against the source's own toe positions, nothing solved and nothing rendered. I had a whole suite of detectors for what the solver produced and not one check on what it was being told.

Test coupled fixes as a stack, not piecemeal. Measured one at a time, each of these three inherits the other two unfixed bugs as its acceptance baseline and dies on them. Revert-first isolation is still right for diagnosis; it was the wrong instrument for shipping decisions on mechanisms this entangled.

Ship the detector even when its own repair does not. The segmentation repair is frozen default-off in my tree, because the stack of it plus the floor fix is still one gate short. The audit ships anyway, report-only, as a standing check on the script. When a second take arrives, from video mocap rather than a purchase, it runs first. That is the real deliverable from the week: not a fix, a question I can now ask cheaply.