Engineering Journal

← all posts

Part of Building the 3D Club Scene

Topology as Ground Truth

Testing & QA3D & motion

Depth of intersection was the wrong severity axis. A shin 5mm inside the pole that arrived from the far side is impossible motion; 20mm of pressed contact is ordinary technique. The performer could not pass a leg through steel, so her own paths tell me which side each limb belongs on, and the verdict stops being a number to tune and becomes a yes or a no. The detectors that produce it still have constants, and whether a shorter-armed body can get to the right side is a different question again.


I bought one 30-second pole-dance mocap take and built a system that reads its essence rather than its joint angles: a semantic contact script (which hand grips the pole in which height band over which span, which foot is planted, which leg wraps) plus style curves. Three bodies with different proportions (ch33, ch02, medea) then solve their own motion against that contract instead of wearing the source's copied rotations. What fought me all campaign was legs going through the pole, and what fixed my thinking was not a better penetration metric. It was noticing that the performer is a topological oracle I already own.

Millimeters argue, sides do not

For a whole campaign I ranked crossings by depth: 22mm is worse than 5mm, drive the maximum down, call it clean. That produced a defensible zero and taught me nothing, because depth does not distinguish possible motion from impossible motion. The pole here has a radius of 0.023m (meta.pole.radiusM in script-pole1.json), so a limb pressed hard against it is ordinary technique. A shin 5mm inside that pole which arrived from the far side is something no body can do. Which side the segment came from separates the two, and the verdict is a yes or a no rather than a number I get to argue about. The classifiers that produce that yes or no are still numerical, and they carry constants I picked: a 5mm emergence floor, a 0.2mm float-noise floor on the swept check, 64 interpolation substeps. What goes away is the tuning knob on severity, which is the one that had been lying to me.

Two schematic cross-sections of the pole. On the left a shin overlaps the pole by 20mm and stays on the side it arrived from, labeled ordinary technique. On the right a shin reads only 5mm inside at each of two consecutive frames, but sits on opposite sides of the pole axis in them, labeled impossible motion.
Schematic, not measured: pole and shin are to scale (23mm and 45mm radius), the frame spacing is illustrative. The point is that the depth number ranks these two backwards, which is why no threshold on it was ever going to work.

The performer is the ground truth

She is made of meat and could not pass a leg through a steel pole. Her paths therefore name the correct homotopy class (informally: the paths you can bend continuously into one another without ever passing through the obstacle). I do not have to author correct routing for ch02's longer shins. I already have a witness for which routing is right.

Which is not the same as a witness that ch02 can get there, and that distinction cost me a whole repair campaign later. When I actually tried to route the 21 windows below, 14 of the 19 in-scope leg crossings turned out to sit inside a span the solver had already recorded as reach-infeasible with its yield ladder exhausted: the body cannot reach the clearance her path demands while also honoring its own plants, grips and bone lengths. The source says which side. It does not say the side is available.

I checked the witness rather than assumed it, because if the source ever crossed the idea collapses. pole1.glb on ch33's rig with &no-solver, all 1773 frames:

Three detectors, no leg crossing found. That is weaker than it looks, and worth saying plainly: the source has no offline solve, so its scan can only come from a browser capture, and I later measured that path repeating a byte-identical bake four times and reporting 3, 4, 5 and 5 frame-visible windows and 0, 1, 2 and 2 between-frame ones. A single capture reading zero is not a zero. The result is consistent across three methods that share one scan, which is enough to work from and not enough to call proof. Taking it, all 21 crossings in my inventory (15 frame-visible, 6 between-frame, three bodies) are artifacts of my solver, not choreography demanding them.

Two tiers, because one tier was not honest

side-plan.mjs reports which side of the pole each leg segment is on, when it genuinely wraps, and with what winding. My first version used a single keep-out radius of pole plus measured flesh and returned 9 to 11 near-pole episodes per leg segment, an order of magnitude more than the whole crossing inventory and almost all of it ordinary grip-wrap proximity: the performer is within centimeters of the pole nearly all the time.

So I split it. The primary tier uses the bare pole radius (23mm), the actual topological boundary, and it is the tier that finds the source crossing-free. The second keeps the flesh-inclusive radius and asks something else: does the source genuinely wrap here? It does, routinely, with orbit episodes spanning up to about 196 degrees of azimuth. A crossing inside a real source wrap has a choreographic reason to be near the pole, so the repair is to route the transit the way she does; a crossing where the source is clear at both tiers just gets pulled back. An axis-only report would have called all six genuine-wrap windows "no source excuse", a lie about what the performer is doing at that instant.

The 21 windows split 6 route, 12 pull back, 2 out of scope (arm-only, 0.24mm and 4.67mm, inside a scripted two-hand grip), 1 ambiguous. All six route windows carry the source's winding of +1 (CCW), no mixed handedness anywhere. The ambiguous one is ch33's right-foot swept crossing at 15.9588 to 15.9755s, where strict overlap finds no source wrap but right-foot's next wrap starts 0.008s later, under one 60Hz frame. I left it unclassified rather than rounding it into a bucket.

What I built is a side-and-winding classifier, not a general homotopy solver. It answers "which side, and did it go around". That was enough.

The between-frame half

A frame-sampled check cannot settle a topological question: a leg can be clear at frame N, clear at N+1, and through the pole between them. swept-collision.mjs interpolates between consecutive frames at 64 substeps and calls a crossing when both endpoints read clear and the interior minimum dips past -0.2mm, a float-noise floor rather than a severity bar. It paid for itself at once: ch02, left-shin, 4.4858 to 4.5025s, endpoints clear at roughly 25mm, interior depth 92.71mm. The deepest crossing in the inventory by an order of magnitude, invisible to every frame-sampled check.

That 92.71mm is a depth under one specific motion model: straight lines between recorded joint positions. Substeps 16 through 256 all return the same crossing, so it is not a sampling artifact, but they are all converging on the same assumption. A renderer slerping bone rotations moves that shin along an arc instead, and an arc and its chord are not the same distance from the pole. The same arithmetic bites me from the other direction in the next section. What the linear standard settles is whether this passes the acceptance test the whole project is gated on. Whether the shipped clip visibly clips is a question for a camera, and the debug trails at 4.49s show the shin pressing through while the source's own trail arcs clear.

The measured negative that proves the principle

With the correct side known per frame, the obvious move is to clamp each frame's azimuth into a corridor centered on the source's own. I built that, calibrated it properly, and it is the cleanest negative of the campaign. Three formulations, measured on solver-emitted bakes against a baseline of 21 crossings: 28, 30, and 36. A width sweep on the second, at uniform floors of 90, 75 and 60 degrees, gave 35, 32, 36, and the third introduced a 4.3mm hips-spine intrusion, a zero-tolerance gate.

The cause is geometric and damping cannot touch it. A per-frame azimuth correction moves a segment along an arc around the pole. The swept standard interpolates joint positions linearly between frames. Any rotation big enough to fix a side violation therefore produces an inter-frame chord that cuts inside its own arc, straight through the disk the arc was going around, while both endpoint frames read perfectly clean. Bigger correction, deeper chord. Measured on ch02's left-foot: 6.4mm before the corridor, 122.2mm after.

The width calibration produced a second negative. Ambient |ourAz - srcAz| on the shipped bakes runs p50 7 to 31 degrees and p90 43 to 92, so the source-derived floors of 20 to 35 degrees bind on a quarter to half of all frames: they do not constrain the pose, they rewrite it. No width separates crossings from background. Only 90 degrees is defensible: "opposite side of the pole" is definitionally |dAz| > 90.

Azimuth had already failed from the detector side: four root-independent formulations, none separating crossings from background. What worked there was not an angle either, but axisSignedM (radial distance to the pole axis) compared source-relative. It reproduced 15 of 15 frame-sampled crossings and correctly missed all 6 between-frame ones, the right answer for a per-frame method rather than a hole in it. That is recall, and recall alone flatters a detector. It fired 48 windows in all, so 33 landed where neither existing check had flagged anything: 30 of those on arm segments the leg-only inventory never covered, plus a couple of genuinely new leg findings. Its threshold does sit in a measured gap (zero samples anywhere in the take between 10mm and 47mm of source clearance), but nobody has gone through those 33 to say which are real.

So: correct-side routing is trajectory-level, period. Plan the whole swing with both ends frozen so the inter-frame path is itself in the right class.

Takeaways