AshForge Motion · the whole tool, one course
The curve you drew is on screen.
It is not in the file.
Motion authors animation clips as tracks of keys. Most of it behaves exactly as you would expect — and one part does not, in a way that is invisible until something downstream reads the file and finds nothing.
What it is
A clip is tracks; a track is keys; a key is a value at a time. That is the whole model.
Modules 1–4 are the model, 5–8 are interpolation and its limits, 9–12 are where clips come from and how they ship.
The one that costs you
Curve shape is a screen-only concept. The file carries the contract you actually have, and it is smaller than what the editor shows.
Modules 5–8. Read them before you spend an afternoon shaping curves.
Motion is a focused tool. Its content is 12 things worth knowing, not three escalating tiers — the old split implied the later material needed the earlier, and it does not. Read straight through, or jump to the module that matches your problem.
The three nouns
Clip, track, key
The whole model is three nested things, and their defaults are worth memorising because you will meet them constantly.
| Thing | Holds | Default |
|---|---|---|
| Clip | A name, a duration and tracks. | 2.0 seconds, no tracks |
| Track | A name, a type, and keyframes. | Track, typed Position |
| Keyframe | A time, three numbers, an interpolation. | Linear |
A key always carries three numbers, whatever the track is for. Position uses all three
as X, Y and Z. Rotation uses them as euler angles in degrees. Scale uses them as factors. And a
Float track — a single scalar, like an opacity or a blend weight —
uses only the first, carrying two unused numbers alongside it.
That is worth knowing mainly so the other two do not confuse you when you read a file: on a Float track, Y and Z are noise.
Routing
The name is an address
A track's name is not a label for your benefit. It is parsed: everything before the first slash is a bone name, and that is how a clip finds the part of the rig it is supposed to move.
| Track name | Bone it addresses | Note |
|---|---|---|
Hips/Position | Hips | The intended form. |
Spine/Rotation | Spine | The half after the slash is not read. |
Head/Something/Else | Head | Only the first slash counts. |
Track (the default) | Track on export, Hips in preview | The two disagree. |
The last row is the trap. With no slash there is no bone name to take, and the two code paths resolve that differently: the preview substitutes the rig's hips so that something moves and you can see your curve, while the export writes the name out verbatim as the bone.
Both behaviours are reasonable on their own. Together they mean an unrenamed track animates the hips on your screen and ships addressed to a bone that probably does not exist. Nothing warns, at either end.
Figure 01 — one name, two answers Give every track a slash and the ambiguity disappears entirely.
Bone/Property. It costs nothing, it makes the two
paths agree, and it is the only form where what you preview is what you export.The other half
The type is the instruction
If the bone comes from before the slash, what does the part after the slash do? Nothing. It is decoration, for you to read.
What a track actually does is decided by its Type dropdown, and the two are never
cross-checked. A track named Hips/Rotation but typed Position exports
as a position track carrying three position numbers — verified — and the name
sits there saying otherwise.
This is a comfortable mistake to make: you rename a track to describe what you want, without changing the dropdown that decides it. The clip then moves the right bone in the wrong way, and the name in the timeline reassures you every time you look at it.
On disk
What reaches the file
Export a clip and open the file. It is small, flat and readable, and two details are worth pointing out.
| Track type | Numbers per key | Meaning |
|---|---|---|
| Position, Scale, Float | 3 | The values as you authored them. |
| Rotation | 4 | A quaternion, converted from your euler degrees. |
Rotation is resolved at export. You author euler degrees, and the file carries a
quaternion — four floats — so whatever reads it applies them directly and does no
euler-order guessing. A 90° yaw comes out with y = 0.7071, as it should.
That matters because euler angles only mean something alongside an order, and this one is fixed: X, then Y, then Z. Resolving it at the boundary means the order is Motion's problem and never the game's.
| Symptom | What it means | Fix |
|---|---|---|
| Preview animates the hips, game animates nothing | The track has no slash, so the two paths disagreed. | Rename it Bone/Property. |
| The right bone moves the wrong way | The Type does not match what the name claims. | Fix the Type; the name has no effect. |
| A Float track's numbers look odd | Only the first of the three is used. | Ignore Y and Z; they are unused. |
| Rotation values in the file look unfamiliar | They are quaternions, not the degrees you typed. | Expected — four floats, converted at export. |
The choice
Two curves
Every keyframe carries an interpolation, and there are exactly two values
it can take. New keys are Linear.
| Setting | Between two keys | Reads as |
|---|---|---|
Linear (default) | A straight line at constant speed. | Mechanical. Right for machinery, doors, rigid objects. |
Cubic | A smooth curve that eases through the key. | Weighted. Right for anything with mass or muscle. |
The setting is genuine authoring data, and it is worth saying so before the rest of this page: it saves to the project file and comes back unchanged on reload — verified by saving an all-Cubic clip, reloading it, and finding every key still Cubic.
So it is not decorative. It is a real property of your keys, correctly stored. The question the rest of this page answers is who reads it.
The preview
On screen: all or nothing
The setting is per key. The playback engine underneath is not: it holds one interpolation per track. Something has to give, and what gives is your per-key choice.
bool anyCubic = tr.Keyframes.Any(k => k.Interp == Interpolation.Cubic);
anim.TrackSetInterpolationType(tIdx, anyCubic ? Cubic : Linear);
One cubic key anywhere on a track makes every key on that track cubic in the preview. Twenty linear keys and one cubic one is a fully cubic track, and there is no combination of settings that produces a track which is cubic in one place and linear in another.
A per-key transition hint is passed alongside each key — a value of 0.5 for cubic keys against 1.0 for linear ones — which softens the result slightly but does not restore the distinction. The track-level choice is what governs the shape.
The export
In the file: nothing
Export the same clip twice — once with every key Linear, once with every key Cubic — and compare the two files.
| Clip | Exported file |
|---|---|
Three keys, all Linear | identical |
Three keys, all Cubic | identical |
Three keys, one Cubic among Linear | identical |
Byte for byte the same. The exported key format has a time and a list of values and no field for a curve at all, so there is nowhere for the setting to be written even in principle. The file does not mention interpolation, cubic or linear anywhere.
Which produces the exact inversion this page is named for. That one cubic key is the most powerful setting on the track in the preview and the least powerful in the file — it changes everything on screen and nothing at all in what you ship.
Figure 01 — one setting, three readings Stored faithfully, played coarsely, shipped not at all.
What you can rely on
The contract you do have
If the curve does not travel, it is worth being precise about what does. The exported animation carries exactly five things.
clip Name, Duration track Bone, Type key Time, values
That is the whole contract. Everything the game does with your animation is built from times and values on a named bone — which means the way to control the feel of a move is to control the keys themselves, not the curve between them.
Concretely: if you want a movement to ease, put the easing in your key timing and spacing. Keys bunched near the end of a move produce a deceleration that survives export, because it is made of times and values. A cubic setting produces a deceleration that does not.
| Symptom | What it means | Fix |
|---|---|---|
| Setting one key to Cubic changed the whole track | The preview holds one interpolation per track. | Split into two tracks if you need both. |
| The game's animation feels different from the preview | Interpolation was never exported. | Express the timing in key spacing instead. |
| Cubic seems to do nothing in the game | Correct — it is not in the file. | Nothing to fix; use more keys. |
| A colleague's cubic keys came back intact | The project file stores it faithfully. | Expected — authoring data is preserved. |
Assembly
Where clips come from
Opening a mod runs two passes in order, and the second can overwrite the first.
| Pass | Source | Contributes |
|---|---|---|
| First | assets/assets.index.json |
One clip per entry typed animation, or ending .glb / .gltf. Name and paths only. |
| Second | animations/*.motion.json |
Whole clips, complete with tracks and keys. |
The index pass is deliberately generous about what counts as an animation: an entry is taken
if its type says animation or if its output filename ends in a glTF
extension. Other asset types are ignored entirely — a texture in the same index
contributes nothing, verified.
So the clip list you see on opening a mod is Refinery's inventory of animation files, with your own work laid over the top of it.
The placeholder
The stub, and what it ships
An indexed clip carries a name, a source path and an output path. That is all. Two things it does not carry are worth being exact about.
| Property | What you get | What you might expect |
|---|---|---|
| Duration | Always 2.0 | The glTF animation's real length |
| Tracks | None | The bones the glTF animates |
| Name, paths | Correct | — |
The glTF is never parsed for its animation during this pass. Its duration is not read,
its bones are not read, and the 2.0 is a constant that has nothing to do with the
file. The tool's own source calls it a stub.
That would be harmless if such a clip could not leave the tool. It can. Exporting a project of nothing but stubs produces a runtime file with a named clip and an empty track list — verified — so the game receives a two-second animation that animates nothing, and nothing anywhere between the index and the file says so.
Figure 01 — a reference, exported as an animation The track count is the only thing that distinguishes this from real work.
The join
Replace, not merge
Author a clip with the same name as an indexed one and it takes its place entirely. Not merges with it — takes its place.
| Authored clip's name | Result | Clips in the project |
|---|---|---|
| Matches the index entry | Replaces it outright | 1 — yours, with tracks |
| Differs by anything | Adds a second clip | 2 — yours, and the stub |
Because it is a replacement rather than a merge, the index's paths do not survive the override — verified: an authored clip that replaced an indexed one had an empty output path afterwards. Your file is the whole truth about that clip.
The second row is the trap. Rename an authored clip — to Walk_v2, say,
while iterating — and the join by name silently breaks. You now have your real animation
and the two-second placeholder it was meant to replace, both in the list, both
exporting.
.motion.json that fails to parse is skipped with a log warning, and
the load succeeds — leaving the indexed stub standing in its place. On screen that is a
clip with the right name and no animation in it, which looks exactly like a clip you have not
got round to yet. If a clip you know you authored has no tracks, check the log.The habit
The order that works
Clip list to game data
- Name authored clips exactly as the index names them. the join is the name, and a mismatch ships the placeholder alongside your work
- Give every track a
Bone/Propertyname. a track with no slash previews on the hips and exports elsewhere — tier one - Put the feel into key timing and spacing, not interpolation. the curve is not in the exported file at all — tier two
- Check every clip's track count, then export. zero tracks is a placeholder that ships as a silent animation
Motion's route into the game exists and is entirely data — but this file is not yet on it.
Everything an animated creature needs is authorable: a RigRendererProps naming
a prefab, a material and an anim tree, and an AnimTreeDec that is nothing but
resource paths and float timings. Because those are paths, the asset bridge can serve
a mod's own rigged model. The real cost is having a genuinely rigged model and knowing its
bone names — demanding authoring work, not a missing mechanism.
⚠ What is missing is the plumbing at this end: the runtime export lands in the tool's
own output folder, which nothing scans. Echo, Catalyst, Equilibrium and Chronicles now write a
bundle inside the mod, where discovery looks; wiring Motion the same way is the
outstanding step. See ashforge_shared/docs/TOOL_ROUTES_TRIAGE.md.
| Symptom | What it means | Fix |
|---|---|---|
| A clip is in the list that you never made | Refinery indexed an animation file. | Expected. Author over it using the same name. |
| Every imported clip is 2 seconds | The duration is a stub; the glTF is not parsed. | Set the real duration when you author it. |
| Two clips, one real and one empty | The authored name stopped matching the index. | Rename yours back to the index's name. |
| A clip you authored has no tracks | Its file failed to parse and was skipped. | Check the log; the stub is standing in. |
| The game plays a clip that does nothing | A placeholder was exported. | Track count zero — author it or remove the index entry. |
Course complete
What you now know
Check yourself
- A clip defaults to 2 seconds; a new track is called
Track, typedPosition; a new key isLinear. - Every key carries three numbers, and a
Floattrack uses only the first. - The name before the first slash is a bone address, and only the first slash counts.
- The part after the slash is decoration — the Type dropdown decides what the track does, and the two are never cross-checked.
- A track with no slash resolves differently in preview and export — hips versus the
whole name — so always name tracks
Bone/Property. - Rotation exports as a quaternion, four floats, converted from euler degrees in a fixed X-Y-Z order.
- Keys are sorted by time on export, and the export overwrites rather than accumulating.
- There are exactly two interpolations,
LinearandCubic, and new keys are Linear. - The setting is real authoring data — it saves and reloads per key, unchanged.
- In the preview it is per track, not per key. One cubic key makes the whole track cubic; a per-key transition hint softens but does not restore the difference.
- To mix curves on screen, split the move across two tracks.
- The runtime export carries no interpolation at all. All-Linear, all-Cubic and mixed clips export byte-identical files.
- The exported contract is five fields: clip Name and Duration, track Bone and Type, key Time and values.
- Because of that, timing and key spacing are the only feel that reaches the game.
- A project is assembled from two sources: Refinery's asset index, then authored
.motion.jsonfiles. - The index contributes a clip for every
animationentry or glTF file — name and paths only. - An indexed clip's duration is a hard-coded 2.0 and it has no tracks. The glTF is never parsed for its animation.
- A stub exports as a named clip with an empty track list, so the game gets an animation that does nothing, silently.
- Authored clips are matched by name, and a match replaces rather than merges — the index's paths do not survive.
- A renamed authored clip stops overriding, leaving you shipping both the real animation and the placeholder.
- A malformed authored file is skipped with a warning and the stub stands in for it.
- Track count is the one number that separates real work from a placeholder.
That is Motion. Its clips reach the game through an animation tree and the asset bridge — demanding, because it needs a genuinely rigged model and its bone names, but not blocked.
