Skip to content

feat(cli): tres gltf --animations, clips from separate files - #1476

Merged
alvarosabu merged 4 commits into
mainfrom
feat/animations-clips-from-separate-glb
Aug 11, 2026
Merged

feat(cli): tres gltf --animations, clips from separate files#1476
alvarosabu merged 4 commits into
mainfrom
feat/animations-clips-from-separate-glb

Conversation

@alvarosabu

Copy link
Copy Markdown
Member

Summary

Mixamo, KayKit and Quaternius all ship the mesh in one file and the clips in others, so
tres gltf on a rig export produced a component with an empty actions and no hint as to why.
This adds -a, --animations <path>, repeatable, and merges the clips at generate time.

tres gltf public/models/Engineer.glb \
  -a public/models/animations/Rig_Medium/Rig_Medium_General.glb \
  -a public/models/animations/Rig_Medium/Rig_Medium_MovementBasic.glb \
  -a public/models/animations/Rig_Medium/Rig_Medium_MovementAdvanced.glb -o src/models

 ▲ ■ ● Tres  gltf  Engineer.glb

 ✔ Parse       51 named nodes · 8 meshes · 1 material · 37 clips merged   37ms
 ✔ Emit        6 slots                                                     1ms

 ✔ src/models/Engineer.gen.vue
   slots  Engineer_ArmLeft, Engineer_ArmRight, Engineer_Body, Engineer_Head,
          Engineer_LegLeft, Engineer_LegRight
   clips  Death_A, Death_A_Pose, Death_B, Death_B_Pose, Hit_A, Hit_B,
          … 31 more — rerun with --verbose

 Done in 87ms

Each file gets its own useGLTF (its own url inferred from public/, its own { draco: true }
when compressed) and the clips are concatenated, model first:

const { nodes, materials, isLoading } = useGLTF<ModelNodes, ModelMaterials>('/models/Engineer.glb')
const { state: rigMediumGeneral } = useGLTF('/models/animations/Rig_Medium/Rig_Medium_General.glb')
const { state: rigMediumMovementBasic } = useGLTF('/models/animations/Rig_Medium/Rig_Medium_MovementBasic.glb')
const { state: rigMediumMovementAdvanced } = useGLTF('/models/animations/Rig_Medium/Rig_Medium_MovementAdvanced.glb')

const animations = computed(() => {
  // The mixer resolves every track against a node name in the rendered tree and never
  // retries a miss, so the clips must not reach it before the model they drive.
  if (isLoading.value) {
    return []
  }

  return [
    ...(rigMediumGeneral.value?.animations ?? []),
    ...(rigMediumMovementBasic.value?.animations ?? []),
    ...(rigMediumMovementAdvanced.value?.animations ?? []),
  ]
})

That isLoading guard is the load-order bug this would otherwise ship: a clip library is a
fraction of the size of the model it drives, so it resolves first, and a mixer handed clips
before the tree exists binds every track to nothing — which three caches rather than retries.

ActionName becomes the union across every file, and the node names the external clips drive
survive pruning exactly like the model's own do.

Both files are parsed, so both can be checked

Track targets. Each external clip's track names are resolved against the model's node
names before anything is emitted — the one animation failure that is completely silent at
runtime. A clip that binds partially is reported; a clip nothing of which binds is left out of
ActionName entirely rather than sitting there doing nothing:

⚠ "Walk" in Rig_Medium_General.glb drives mixamorig:Hips, mixamorig:Spine and 47 more,
  and this model has no node by any of those names — nothing would play, so it is left
  out of ActionName.

Name collisions. Clip libraries overlap (the three above all ship a T-Pose). The array
decides: last file passed wins, ActionName lists the name once, and the CLI says which file
won instead of leaving it to be discovered.

A rig with nothing to play now points at the flag rather than generating a mute component:

⚠ This model is skinned but carries no animation clips. Pass --animations <path> to wire in
  clips exported to separate files.

Elsewhere

  • --dry-run counts each file on its own line plus the merged total, which is neither the sum
    nor the model's own count.
  • --instance works with it: the provider file owns the loads and the merge, the consumer
    reads animations off the injected context.
  • Playground demo at /cientos/loaders/gltf-animations — a KayKit Engineer rig with zero clips
    of its own, 37 merged from three libraries, in a dropdown.
  • Docs and CLI README updated.

Test plan

  • tres gltf public/models/Engineer.glb -a <clips>.glb writes a component whose actions
    is keyed by the merged union
  • Passing the same clip name from two files warns, and the last file passed is the one that
    plays
  • A clip whose tracks target a different rig is reported and stays out of ActionName
  • Generating a skinned model with no clips and no -a warns and points at the flag
  • A missing -a path errors by name, not with a bare ENOENT
  • --dry-run reports per-file counts and the merged total
  • -a combined with --instance puts the loads in the provider file
  • The playground page plays every clip in the dropdown, including after a hard refresh
    (load-order guard)

- Introduced the `--animations` option to allow merging of animation clips from separate files with the model's own animations.
- Updated documentation to reflect the new `--animations` feature, including usage examples for loading multiple animation files.
- Enhanced the CLI to parse and validate animation clip files, ensuring proper integration with the model.
- Added tests to verify the functionality of the new animation merging feature, including handling of overlapping clip names and reporting of loaded clips.
- Changed warning indicators in the documentation to use a consistent ⚠ symbol for clarity.
- Updated the command examples to reflect the correct paths and options for the Engineer model.
- Enhanced output messages in the CLI to provide more detailed information about parsed nodes, meshes, and animation clips.
- Adjusted the `--verbose` flag description to specify that it lists both slot and clip names.
@netlify

netlify Bot commented Aug 11, 2026

Copy link
Copy Markdown

Deploy Preview for cientos-tresjs ready!

Name Link
🔨 Latest commit ddd31f1
🔍 Latest deploy log https://app.netlify.com/projects/cientos-tresjs/deploys/6a7b6e9f0999ac00080d5a91
😎 Deploy Preview https://deploy-preview-1476--cientos-tresjs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify

netlify Bot commented Aug 11, 2026

Copy link
Copy Markdown

Deploy Preview for rapier-docs ready!

Name Link
🔨 Latest commit ddd31f1
🔍 Latest deploy log https://app.netlify.com/projects/rapier-docs/deploys/6a7b6e9f351a0b0008903db1
😎 Deploy Preview https://deploy-preview-1476--rapier-docs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify

netlify Bot commented Aug 11, 2026

Copy link
Copy Markdown

Deploy Preview for tresjs-docs ready!

Name Link
🔨 Latest commit ddd31f1
🔍 Latest deploy log https://app.netlify.com/projects/tresjs-docs/deploys/6a7b6e9f8c0b3b0008269857
😎 Deploy Preview https://deploy-preview-1476--tresjs-docs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify

netlify Bot commented Aug 11, 2026

Copy link
Copy Markdown

Deploy Preview for postprocessing-tresjs ready!

Name Link
🔨 Latest commit ddd31f1
🔍 Latest deploy log https://app.netlify.com/projects/postprocessing-tresjs/deploys/6a7b6e9feac8960008d82039
😎 Deploy Preview https://deploy-preview-1476--postprocessing-tresjs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify

netlify Bot commented Aug 11, 2026

Copy link
Copy Markdown

Deploy Preview for tresjs-lab ready!

Name Link
🔨 Latest commit ddd31f1
🔍 Latest deploy log https://app.netlify.com/projects/tresjs-lab/deploys/6a7b6e9f2ceb0600081a27e7
😎 Deploy Preview https://deploy-preview-1476--tresjs-lab.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@pkg-pr-new

pkg-pr-new Bot commented Aug 11, 2026

Copy link
Copy Markdown

Open in StackBlitz

@tresjs/cientos

npm i https://pkg.pr.new/@tresjs/cientos@1476

@tresjs/core

npm i https://pkg.pr.new/@tresjs/core@1476

@tresjs/eslint-config

npm i https://pkg.pr.new/@tresjs/eslint-config@1476

@tresjs/leches

npm i https://pkg.pr.new/@tresjs/leches@1476

@tresjs/nuxt

npm i https://pkg.pr.new/@tresjs/nuxt@1476

@tresjs/post-processing

npm i https://pkg.pr.new/@tresjs/post-processing@1476

@tresjs/rapier

npm i https://pkg.pr.new/@tresjs/rapier@1476

commit: ddd31f1

…tional tests

- Updated the GLTF command to explicitly type the `sources` array as `AnimationSourceInput[]` for better type safety.
- Added new tests to verify the correct binding of animation clips and node name sanitization in the build IR process.
- Improved documentation within tests to clarify the purpose of retarget checks and node name handling.
@alvarosabu
alvarosabu merged commit e69ee81 into main Aug 11, 2026
9 checks passed
@alvarosabu
alvarosabu deleted the feat/animations-clips-from-separate-glb branch August 11, 2026 18:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant