Editing captured motion as data

A two-performer capture runs 170.17 s and has a 53-second dead middle. Cut it to 52.9 s with a few hundred bytes of edit list — the capture itself is never modified.

The idea. Motion is data you can edit. A cut is not a re-bake: it is an edit list — a handful of spans over the captured clip — stored on the timeline next to everything else. Clear the list and the full capture plays again, byte for byte. A generated video clip cannot do this; a captured, rigged performance can.
What you'll need. A scene with two rigged performers that were captured together (so they share one world frame), each with a baked clip, on the Universal Timeline — for example $S('#a').at(0).play('clip-a') and the same for #b. Everything below is pasted into the Shell tab; nothing needs the viewport, so an agent can author the same cut headlessly.

1 Register the pair

Two performers captured in one metric frame are only coherent together. Registering them as a group makes every edit apply to both, atomically — same spans, same blend windows.

const sparring = $M.group('sparring', ['#a', '#b']);
sparring.collision();      // baseline over the full capture

The baseline report says how close the two hips ever get on the floor. For the capture used here: min 0.80 m, never under the 0.6 m floor for non-contact sparring.

2 Find the exchanges

In the measured capture, the close approaches cluster into 18 exchanges. They are front-loaded: ten land between 2.6 s and 43.2 s, then nothing from 43.2 s to 98.5 s. Padding each cluster by −1.0 s / +1.2 s and merging gaps under 0.4 s gives eight spans:

#inoutdurclosest
E011.609.007.40 s0.94 m
E0214.6022.107.50 s0.96 m
E0330.8044.4013.60 s0.80 m
E0497.50103.405.90 s1.11 m
E05104.80110.806.00 s0.93 m
E06113.70117.603.90 s1.08 m
E07134.70140.605.90 s1.10 m
E08158.40161.102.70 s1.17 m

That removes 44.4 – 97.5 s (53.1 s) and keeps 52.9 s — 31% of the source.

3 Splice them

splice joins the spans. A blend is mandatory: over the transition window (0.25 s ≈ 8 frames by default) every rotation is slerped from the outgoing pose to the incoming one, and the root is offset so the hips continue from where they were — otherwise the body pops or teleports at every cut.

sparring.splice([
  [1.6, 9.0], [14.6, 22.1], [30.8, 44.4], [97.5, 103.4],
  [104.8, 110.8], [113.7, 117.6], [134.7, 140.6], [158.4, 161.1],
].map(([from, to]) => ({ from, to })), { blend: 0.25 });

editor.timeline.duration;   // 52.9 (once the cameras are remapped — step 5)
sparring.asCommand();       // the declarative form of this cut
Why the pair moves as one. The continuity offset at each cut is computed from the pair's centroid and applied to both performers, so their separation survives the cut. Per-performer timeOffset and speedOf are refused until you call breakRegistration() — it warns what is lost (they can clip through each other) and is undoable. A directive on a single member, e.g. $S('#a').play('clip-a', { from: 1, to: 5 }), is refused for the same reason.

4 Check for collisions

No renderer will warn you that the fighters now overlap. The collision report measures the hip-to-hip floor distance over the edited timeline and flags every span under the floor:

console.log(sparring.collision());   // { min, minAt, ok, violations: [...], spans: [...] }
sparring.collision({ floor: 0.6 }); // the default floor, in metres

For this cut: ok: true, no span under 0.6 m. The closest approach sits inside a blend window (0.74 m) — the blend interpolates the two performers' positions, so it can bring them closer than either source span does; the check is what tells you.

5 Remap the cameras

Shots authored against the 170 s source need the new timebase. remapShots() keeps every event that lies wholly inside one kept span, moves it to the cut's clock, and drops (and reports) the rest:

const r = sparring.remapShots();
r.kept; r.dropped;          // what survived the cut
editor.timeline.duration;   // 52.9

6 Slow a beat, hold a pose

Everything is composed from the same span primitive and applies to both performers:

sparring.hold(20, 0.4);                       // freeze the pose at cut-time 20 s for 0.4 s
sparring.retime([[30.8, 1], [38, 0.5]]);      // from source 38 s on, run at half speed
sparring.rootYaw(15);                         // turn the whole performance 15° about its centre
sparring.rootOffset([0.5, 0, 0]);             // move it in the world, same vector for both

7 Undo, clear, commit

  1. Every call above is one Undo step — the same stack as the Animations tab.
  2. sparring.clear() drops the edit list; the full 170.17 s capture plays again, untouched.
  3. Open the Animations tab: the cut is one teal block per performer with a tick at every span boundary, and a Collision check / Clear cut bar underneath. Drag a block and both performers move together; the code panel shows the same cut as a $M('sparring').cut(...) line you can edit and save.
  4. Commit from the Git tab. The scene is written compact (no pretty-printing) with the animation floats quantized — see below.
Honest note on what is and isn't modified. The captured clip on the scene is only ever read. The cut is compiled in memory from the edit list each time it is needed (and for glTF export), the way the Timeline clip is, and is never saved. Saving the scene writes the capture as it was plus a few hundred bytes of edit list. Saved animation values are quantized (quaternions 5 decimals, positions and times 4) — a deliberate, deterministic, lossy size saving applied at save, independent of any edit.

Credits and licences

People

Assets

Tools

None used.