UniMate Mesh Export
How UniMate Mesh Export turns motion into a playable GLB
- asset
- motion
- model_3d
- glb_path
- info
This is the payload node. UniMate Mesh Export takes the motion the sampler generated, writes it onto your mesh's bones as real glTF animation, saves an animated .glb into output/3D/, shows you the result inside the node, and hands you a FILE_3D_GLB you can wire into Preview 3D. It's an output node, so it always runs, and it's instant - all the expensive work already happened upstream.
The interesting part is what it doesn't use: Blender. That's not an aesthetic choice. UniMate's own mesh-driving path goes through bpy and it's broken on Blender 4.4+, because blender_rig.py calls action.fcurves.new, which the slotted-action API removed - you get 'Action' object has no attribute 'fcurves' on any recent Blender. So the pack writes the glTF animation directly instead. Practically, that means your ComfyUI venv never needs bpy, and this node works regardless of which Blender you have installed, or whether you have one at all.
What it's actually doing under the hood
There's a subtler problem than Blender, and the fix for it is the clever bit of the pack. rig_preprocess bakes a canonical GLB whose rest pose matches the motion's T-pose almost exactly (measured at 3e-07), but whose per-bone axes are Blender's - so each joint's rest rotation differs from the motion's by about 90°. Write the motion's local rotations straight onto those nodes and every bone twists.
The exporter sidesteps the whole axis-convention question by driving world transforms:
world_glb(f) = world_motion(f) @ inv(world_motion(rest)) @ world_glb(rest)
i.e. it applies the motion's delta from its own rest pose to the GLB's rest pose, so whatever convention each rig uses cancels out. The node-local transform it writes is then inv(world_glb(parent, f)) @ world_glb(f). Skinning stays correct because the GLB's inverse bind matrices are relative to its own rest pose, which is exactly the pose this formula leaves untouched at frame zero. Rotation tracks are LINEAR and written for every joint; translation tracks are only emitted where a joint genuinely moves. Verified to 3.6e-07 – 7.4e-07 against the forward kinematics on a 53-joint horse, a 41-joint spider and a 71-joint dragon - the kind of detail you only bother measuring after a 90° rotation bug has cost you an evening.
One trap worth knowing even though the node handles it: a UniMate cond carries both offsets (parent-relative translations) and tpos_offsets (bone vectors in the canonical frame), and the forward kinematics wants tpos_offsets. Pass offsets and you get a pose rotated ~90° with no error raised - a level quadruped decoding as a vertical one.
Inputs and outputs
- asset - the
UNIMATE_ASSETfrom Mesh Preparation, not from the sampler. It carries the canonical GLB the animation lands on. - motion - the
UNIMATE_MOTIONhandle from the sampler. - filename_prefix - default
3D/UniMate_. Same convention as Save Image, so a subfolder in that string is a subfolder underoutput/. - fps - default 30, range 1–120. This retimes playback of the tracks; it does not change the motion that was generated. Leave it at 30 unless you have a downstream reason, because everything upstream was sampled at 30.
The outputs are shaped the way you'd hope. model_3d is a FILE_3D_GLB per clip - wire it to Preview 3D and you get a scrubbable, rotatable animated model right there in the graph. glb_path is the same files as paths relative to output/, for anything that wants a filename rather than a file object. And info is one string describing all of them: frames, duration and track counts, e.g. 60 frames, 2.00s, 71 rotation + 12 translation tracks.
Because the first two outputs are lists, one Preview 3D node runs once per clip. Sampled four repetitions, got four numbered GLBs and four previews - the numbering runs consecutively rather than skipping, so you can tell which is which.
Install, for the record
The export node itself needs nothing new: no Blender, no pip package beyond the pack's single tyro dependency, no compiled CUDA extensions. That is genuinely rare in this corner of the ecosystem, where the usual 3D-pack install story is pinned Python/CUDA wheels and a build toolchain.
cd ComfyUI/custom_nodes
git clone https://github.com/visualbruno/ComfyUI-UniMate
pip install -r ComfyUI-UniMate/requirements.txt
You still need the UniMate checkout on disk and discoverable - the node imports its animation utilities at export time - plus a checkpoint run in ComfyUI/models/unimate/<run>/. But you can export on a machine where preparation would never work.
The two ways this fails
"No canonical GLB in <asset dir>." Your source was a skeleton-only rig. rig_preprocess writes a canonical mesh GLB only when the source had a skinned mesh, so there's nothing to animate - the motion exists, you just can't see it. Nothing to fix downstream; go back to the rig.
Nothing to export, because the sampler produced zero clips. Then this node writes zero GLBs and reports 0 GLB(s), which is technically correct and maddening. Check the preparation report for a joint count over the checkpoint's limit - that's the usual culprit.
And the licence, one last time, because this is the node that produces the file you might try to ship: the pack's code is MIT, but the UniMate checkpoints are CC BY-NC 4.0, their model card explicitly gives no warranty that motions generated with them are free of third-party rights, and the animal portion has disputed provenance. The authors' own words were that they wouldn't recommend shipping them in a commercial product. Nice tool, non-commercial output. Plan accordingly.
Inputs (4)
| Name | Type | Default | Description |
|---|---|---|---|
| asset | UNIMATE_ASSET | — | |
| motion | UNIMATE_MOTION | — | |
| filename_prefix | STRING | 3D/UniMate_ | — |
| fpsopt | FLOAT | 301–120 | — |
Outputs (3)
| Name | Type | Description |
|---|---|---|
| model_3d | FILE_3D_GLB | The saved animated GLB as a 3D file: connect to Preview 3D. One per clip. |
| glb_path | STRING | GLB path relative to the output folder. One per clip. |
| info | STRING | What was written: frames, duration and track counts. |