Repository navigation
Release 1.51.0: characters and motion - #503
Merged
Merged
Conversation
…chy, shape) + rest-corrected retarget core - humanoid/humanoidMap.js (leaf): the VRM 1.0 bone table, a name reader for Mixamo / KayKit / Meshy / VRoid (UniRig) / Unreal-Quaternius conventions, the spine order read off the hips->head path (Meshy writes it top-down), a structural fallback for numbered UniRig bones, overrides - humanoid/retarget.js: SkeletonUtils.retargetClip's method (world rotations sampled at a fixed fps, hips travel scaled by size) with the rest-pose correction and a facing correction, on plain snapshots so every peer computes the same clip locally - unit: 10 map + 6 retarget cases (A-pose target, -Z facing, 2x size, .bones[] tracks) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- rigs WITHOUT clips (a VRM, a Meshy / UniRig character, the KayKit avatars) ride the raw-bytes
objectfile path like animated ones (fileHandler / importOptimize hasSkin)
- animatedImports: rest pose captured at registration, RUNTIME clips (computed per peer, never in
the bytes), registration listeners; humanoid config / provenance / VRM expressions ride the bytes
- humanoid/humanoidRig.js (lazy): map = file's (VRM) or detected + user fixes; Apply clip from a
scene model or the built-in KayKit motions; config replicates (objectParameters 'humanoid'),
saves, undoes; every peer + late joiner retargets for itself
- Apply clip dialog, Inspector > Character (rig, applied clips, licence, VRM meta + expression),
object menu Character > Apply clip..., Animation window 'Apply to...'
- vrm/vrmRuntime.js (lazy chunk, @pixiv/three-vrm 3.5.5 MIT): VRM 0.x + 1.0 (.vrm or a .glb with
the extension), spring bones + expressions ticked after the mixers, own humanoid map, licence meta
-> provenance; Set Expression flow node (deterministic on the trigger stamp)
- provenance.js (leaf): {tool, model, version, licence, prompt?, date} records, licence flags
(credit / NC / no-redistribution / ND), warnings, credits text, VRM meta -> licence
- unit: provenance (5); e2e: character-apply, character-vrm (fixtures built in-test)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…lication
A clip is its own small file (fork F1). Humanoid clips are valid VRM Animation
(VRMC_vrm_animation 1.0), written in VRM's normalized space so three-vrm reads them back
unchanged and one file plays on any mapped humanoid; other clips are a plain GLB of the
source skeleton + one animation, bound by node name. Both carry extras.theprototype
{format, kind, skeleton {sig, humanoid}, fps, frames, loop, rootMotion, provenance}.
- src/lib/tpanim/: glb.js (hand-written deterministic GLB container), format.js
(encode/decode, three clip <-> TpClip, toHumanoid / humanoidFrames / bindHumanoid,
signatures, fit), humanoidBones.js (VRM 1.0 bone set), humanoidMap.js (the detector
seam 57-chars M2 registers into; Mixamo/VRM-name fallback), clipAssets.js (lazy binder:
hash -> decoded -> bound action, hudImages-style pull + retry, scene-asset source),
convert.js (lazy: .vrma / .bvh / animation-only .glb + .fbx -> .tpanim on import),
clipOps.js (resample, in place, seam, loop, trim/split/join — used by M3/M8).
- animatedImports: userData.clipRefs [{hash,name,speed?,loop?,mode?}] ride the
objectfile message, save entries and undo; one objectParameters 'clipRefs' message + one
'clipRefs' undo entry per change; the model bytes never change. Rest pose captured at
register; crossfade-ready transport (at/from/fade) and local pose drivers.
- Rigged models with no clips import as animated imports so clips can be assigned.
- Explorer: 'anim' kind (Animation clips), conversion on import, drop a clip on a rigged
model to assign + play it.
- Tests: unit tpanimFormat (round trip, determinism, humanoid bind on other rigs, VRMA
read by three-vrm's own loader incl. a foreign non-identity-rest VRMA), tpanimOps,
wireValidate clipRefs; e2e tpanim-clips (host + peer + late joiner, real KayKit assets).
- devDependency @pixiv/three-vrm-animation (unit test only).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… writer; M7 warnings in Export/Download/Publish - download/glbEdit.js (leaf): edit a GLB's own JSON + BIN (append / strip animations) so the model and a VRM's extensions stay byte-for-byte what was imported - download/clipFiles.js: a clip as its own file in the 57 M1 .tpanim shape (skeleton + one animation + extras.theprototype; humanoid = VRMC_vrm_animation), layout (a) clips embedded in the original bytes - download/bvhWriter.js (lazy): world rotations folded into ZXY channels, offsets from rest, root offset zero - round trip through three's BVHLoader puts every joint where the rig does - download/characterDownload.js + DownloadDialog: (a) one GLB (default), (b) model + .tpanim zip, (c) model only, one clip as .tpanim / .glb / .vrma / .bvh, FBX / USD listed off with the reason, several objects as one zip with CREDITS.txt; Object > Download..., Inspector > Character - M7: LicenceWarnings in Publish / Export / Community gallery / Download (warn, never block); sceneLicences.js counts applied clips' licences; exports write CREDITS.txt - unit: characterDownload (8); e2e: character-download Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… the Skeleton view - rig/rigOps.js: rename / move in the rest pose / add / delete a bone, mirror left <-> right, write skin weights - each edits the GLB itself and KEEPS THE BIND (newIBM = inv(newWorld) * oldWorld * oldIBM), so every skinned vertex stays where it was - rig/weightsCore.js (leaf) + weightsWorker.js: automatic weights = bone-heat seeding (nearest bone) diffused over the welded SURFACE (never across air), smooth, normalize (<= 4), the brush - rig/rigFixes.js: one operation = one new file, replaced on every peer (objectfile replace:true) and undoable (history 'rigbytes'); a brush stroke paints locally and is written on lift; test pose local; skeleton helper at the scene root - SkeletonWindow (floating, lazy): Humanoid map (per-bone select, detected vs fixed, reset; a replicated config), Bones, Weights + brush; Inspector > Character > Skeleton..., object menu - animatedImports: glTF primitive stamps per skinned mesh, replaceAnimatedBytes - unit: rigFixes (10: bind kept for every op, surface heat); e2e: character-skeleton Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…nitized .L/.R) and skip UE5 leaf tips + finger metacarpals - the UAL 1 Standard rig is Rigify (DEF-upper_arm.L -> 'DEF-upper_armL' once three drops the '.'), UAL 2 is Unreal-named with _leaf_ tip bones that were read as finger roots - unit: a Rigify case (raw + sanitized) and UE5 leaf / metacarpal bones Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… positions; converter tests - toHumanoid: a rig whose REST hips sit at the floor (a BVH keeps the height in the motion) stands at its first frame — VRMA scales the hips by the rest height, and zero means nothing - convert (BVH): centimetres detected from the first frame's root height, not the rest pose - tpToThreeClip(tp, root): each node name resolves against THAT model (GLTFLoader sanitizes 'Door Left' -> 'Door_Left'; an object named in the app keeps its name); clipFit matches both - nodePositionsAt(tp, t): the file's own skeleton posed at t (thumbnails, model-less previews) - unit: tpanimConvert (BVH -> humanoid in metres, an animation GLB split per animation, .vrma keeps its humanoid, a model GLB is left alone), nodePositionsAt Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rce Avatars registry) - avatars/osaRegistry.js: the registry at a PINNED commit through the CDN and the CC0 list cache; CC0 collections by default, CC-BY only on request; VRM + public + https rows only - Add to scene places the VRM as a character with a provenance step naming the collection's licence and author (fileHandler importFile opts.provenance), so a CC-BY avatar's credit reaches the M7 warnings and CREDITS.txt - unit: osaRegistry (2); e2e: character-avatars-source (routed registry + VRM fixture) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rData.humanoid is 57-clips' map key)
- 57-clips M1 reads root.userData.humanoid as a plain {vrmBone: node} map; this lane's config
({map fixes, applied clips}) moves to userData.character, and its wire parameter to 'character',
so the two never read each other's shape at the union
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… by raw node names The Explorer's clip cards get a 2D stick-figure thumbnail (lazy, no WebGL context per card); double-clicking a clip plays it on the selected character; bindClip passes the model so node names that sanitize differently still bind; the unsupported-file toast names clips. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…poke; character e2e measure what they claim - SkeletonWindow: the helpers effect depended on `info` (derived from sceneRevision) while refreshHelpers pokes the scene, so it re-ran until svelte stopped it and the window never rendered; it is keyed on a string of the bones, their rest places and the live root id now - the Bones fields (rename, rest) follow the picked bone and the rig, not every scene poke, so a test-pose drag no longer wipes a name being typed - e2e: FULL_RESOURCE_TIMING init script (the 250-entry buffer fills during a dev boot, so every "lazy chunk fetched" check read false and every "not loaded at boot" check passed vacuously) - character-vrm: a two-bone spring chain is ONE three-vrm joint; the swing is sampled while the body moves (the damped spring settles before a 600 ms sample) - character-download: the (b) zip carries CREDITS.txt, crediting the applied clip's source - character-skeleton: prints the window's DOM + page errors when the map does not show Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… two halves - merge origin/feat/57-clips-m1 (PR #425): conflicts in animatedImports (rootDataFields carries character/provenance/vrmExpressions AND clipRefs; both register hooks), commandsHandler (the 'character' / 'expression' / 'clipRefs' parameters side by side, M3's stamp + crossfade kept), fileHandler (both lanes routed clipless rigs to the bytes path), wireValidate, package-lock - a humanoid clip asset binds through the M2 retarget: clipAssets loads humanoidRig before the first humanoid bind and calls retargetTpClip (rest pose + facing corrected, hips scaled, the character's own map incl. the user's fixes); bindHumanoid stays the fallback - humanoidRig registers its detector through setHumanoidDetector (a VRM's file map, else the detected map with the user's fixes) - tpSnapshot (retarget.js, a leaf): a decoded .tpanim's skeleton as a retarget snapshot - Apply clip… lists the Explorer's clip files; applying one assigns it as a clip asset on each target (one mechanism per kind of source; non-humanoid targets are refused with the reason) - Download: .tpanim / .vrma are written by 57 M1's encodeTpanim(toHumanoid(tpFromThreeClip(...))) so every clip file the app writes has one shape; the single-clip .glb keeps the rig's skeleton - unit tpanimRetarget: a clip through encode -> decode -> retarget comes back onto its own rig as the same pose (arms resting turned 35 degrees) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ontrols A KayKit rig's IK targets, poles and foot-roll controls hang off the root; drawn, they fanned out from the floor over the figure. The card now draws only the bones below the root / armature container that are not helpers (or under one), fitted to those. Unit tpanimThumb 4 (KayKit, Mixamo under an Armature, VRM hips at the top). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…n states, animate parts
One commit for four phases: their runtime shares the same mixer/driver plumbing in
animatedImports + flowRuntime, and splitting those hunks would leave commits that do not build.
M3 — game-ready clips
- per clip ref (no new file): In place / Moving (root travel stripped at bind), loop or once,
speed; fps · frames · length + the LOOP SEAM check; Loop it (a new file, ends blend into start)
- crossfade transport: setAnimationState {at, from, fade} — every peer blends the same two clips
from the same synced stamp; Play Animation gains Crossfade (s)
- Blend by Speed node: idle/walk/run 1D blend by speed (its own speed when unwired, from the
replicated transform via the avatars' tracker), eased, as a LOCAL pose driver on every peer
M8 — split and join
- clip tools panel in the Animation window: Extract clips (+ use the files instead: embedded clips
switched off, model bytes untouched, one undo), Trim by in/out markers, Split at the playhead
(local preview playhead = a held pose driver), Join with a crossfade, Rename / Remove
M11 — Animation states
- userData.animGraph (normalized; objectfile/save/undo + one 'animGraph' message) evaluated on
every peer (animGraph.js pure + animGraphRuntime.js lazy); motion driver = speed/vy/grounded
from how the object moves; synced params ('animParam', latest-wins); Locomotion preset
(Move blend / Jump / Fall); the Animation states window (ToolboxWindow); Set Animation
Parameter + On Animation State nodes
M12 — animate imported parts
- parts models: any object takes clip refs, bound by node name (a sweep registers arrivals);
animateParts() + 'animate_parts' in executeAiTool; Attach to Socket node + attachToSocket
(a per-frame follow constraint, one 'attachTo' message + undo)
Also: double-click a clip = play it on the selected character; .tpanim/.vrma/.bvh dropped on the
viewport go to the Explorer and onto the rig under the cursor.
Tests: unit tpanimOps, blendSpeed, animGraph, partsClip; e2e tpanim-motion, tpanim-states.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- crossfade: AnimationAction.stop() keeps its weight, so a clip interrupted mid-fade replayed half-strength — every transport start resets it to 1 (measured: 0.3 after stop) - ref-only changes (mode / loop / speed) and graph edits poke the clip store, so the panels follow - the Animation states window is a lazy chunk (animGraph.js stays out of the boot bundle) - Locomotion preset: a travelling walk/run tells the blend its own pace and is set In place - BVH: the height lives in the motion (rest hips at the floor) — centimetres detected from the first frame, and toHumanoid stands a floor-rest rig at its first frame (VRMA scales by it) - M12: "Save as a clip file on <model>" bakes keys authored on a model's parts into one node-name .tpanim on the model (clipEdit.partsKeysToClip) - Explorer: clip cards draw their skeleton (a 2D stick figure + trail, lazy); double-click plays a clip on the selected character; .tpanim / .vrma / .bvh dropped on the viewport go to the Explorer and onto the rig under the cursor - unit: tpanimConvert (BVH -> humanoid in metres, animation GLB split, .vrma, model GLB left alone), nodePositionsAt Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…new instance) re-registers and keeps its transport Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
On a short window the tools below the fold (Animation states…, extract) were cut off by the window with no way to reach them; tpanim-states caught it as an off-viewport click. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… when it arrives A peer's clipRefs / animGraph / transport message could reach a receiver while the object was still in its ingest queue (an agent imports a model and animates it at once) and was dropped, so the receiver never played the clip. They are parked by uuid, applied refs-first when the object lands, and expire after 30 s. tpanim-states delivers that order by hand on B. The test also waits for A to bind a parts-keys clip (binding is async), and the click that fires the jump trigger pulses for 1 s so a late nodetrigger on a loaded runner still lands inside the window. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…panim-states prints each peer's clip state A blend's tail (run at 0.0004 beside walk) cost a whole skeleton evaluation per frame and showed nothing; below 1 % a clip now sits out and the rest renormalize (when every weight is that small the strongest still plays). The Quest-profile probe had the Locomotion graph at p95 2.6 ms for 8 knights (CPU x4, load 16). tpanim-states logs what each peer holds for the parts model (refs, transport, bound clips, waiting decodes) and B's view of the jump trigger, so its remaining reds say which link broke. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ates window's selects take the UI font Squeezed into their own scroll box, the clip tools shrank to a single line on a short dock (the docs screenshot showed it); the pane now scrolls and the channel list keeps a floor. The Animation states window's selects fell back to the browser's larger default font. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…y undo resumes on redo A peer's copy of a parts model is built one object at a time, so its clip ref can arrive before the part it animates: the sweep now notices a parts model's node count change, gives the new nodes a rest pose and retries the clips not bound yet; a no-fit toast waits until the model has settled. A playing clip whose ref goes (undo) stops and comes back playing when the ref returns (redo), as on the peers that never bound it. tpanim-states: B-side click sampler + node names in the parts dump. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… lands ~3 s later) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
# Conflicts: # src/components/editors/AnimationWindow.svelte # src/lib/animatedImports.js # src/lib/commandsHandler.svelte.js # src/lib/flowRuntime.js # src/lib/nodeCatalog.js # src/lib/nodeDocs.js
…plorer kinds, drop paths, import opts, deps) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ry list Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ce, Locomotion preset fills a humanoid's gaps; characters-proof suite Nothing in core read motion-kit: a clipless VRM could not use the preset or the pack. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eer (about 30 s on cloud builds, 1.50 too) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Roadmap 57 union on v1.50.0: lanes #425, #427, #436, the packs PR (characters-kit, motion-kit), plus what the proof needed: the Motion pack wired into the app (Asset Browser tab, Apply clip, Locomotion preset) and a real-pack proof suite (characters-proof).
Battery vs v1.50.0, proof on preview-57 and A/B notes are in the integrator's status. Changelog: 1.51.0 in CHANGELOG.md.
🤖 Generated with Claude Code