The Era-1 Dug-out Canoe (Sketchfab, Matt LeMoine, CC-BY-NC-SA-4.0) is the most pathological asset the Factory has eaten. It forced six real pipeline defects into the open. This guide records what each one looks like from the outside, so the next node-animated Sketchfab model doesn’t cost a full day.
byteStride on animation bufferViews (illegal).
SharpGLTF/glbconv hard-fail → the multi-material atlas silently falls back to single-material
(“every part samples material 0”). Repair: strip byteStride from the animation bufferViews only
(mesh ones legitimately keep theirs), rewrite the JSON chunk, pad to 4 bytes. Swap the repaired data
into the original filename — a Factory/Lab window overwrites registry edits on every Save/Bake.| Symptom | Cause | Fix / knob |
|---|---|---|
| Whole model wears material 0, rest grey | glbconv crashed on byteStride → single-atlas fallback | repair the GLB (above) |
| Multi-material atlas missing one material after config changes | extraction cache was mtime-only; a deploy-converted (part-stripped) extraction looked “fresh” for the raw file | fixed: <name>.mtl.src source stamp |
| A rigid decor part (sail) dragged off its mount after bind | bone-parent→skin bound it to bones whose animated frame ≠ static layout | Static parts (no bone-bind) in the Animation Lab (staticParts) — keeps listed mesh/material substrings weightless at their authored position |
| deploy_convert silently deletes half the model | its default strip list contains "polysurface" (howitzer-era default) |
put anything in Deploy strip parts (e.g. camera) — a non-empty value replaces the default list |
| Exported clip is a statue though the source animates | multi-slot action: a glTF import packs every object’s animation into ONE action with a slot per object; slots[0] is not the armature’s slot |
fixed in assign_action — gated to localNodeAnim, see below |
| Sail/parts collapse to a clump after “fixing” the slot | the legacy static path’s captures (flatten, bone-parent) depend on the frozen rig — un-freezing it relocated every capture | the slot fix + REST-hold only run under localNodeAnim; the static path is byte-faithful to every proven bake |
convertRig + keepTranslations + staticParts: Cloth +
clip Take 001[120..360]. Sail raised on the mast, textured, floating correctly in-game. No motion.localNodeAnim (“Node animation → bones”) — an evaluated-world transplant
that samples each mesh’s per-frame world matrix from the inspection-FBX round-trip (the only
composition of this file that is provably correct) and keys one flat bone per mesh. The sampling,
keying, slot handling and double-bind guard all work; the bind/vert-fold arrangement is still wrong
(parts collapse into the hull). Do not enable it on a shipping model yet.Rather than keep fighting the source’s take, the Vehicle Lab now covers this whole class of model:
Ignore role (I key) deletes parts from the output.
For the canoe: mark the outrigger logs, lashings, mast/spars and sail Ignore → 66 meshes / 21.7k tris
collapses to 10 meshes / 4.8k tris, 4 materials to 2 (sharper atlas at the same size), and the entire
sail problem class disappears. A bare dugout with paddles is also the better historical read for a
level-0 transport — the outrigger-and-sail rig is markedly later technology.Rock amplitude (deg) + Rock cycle (frames). Authors a slow idle sway on a
RootHull bone inserted between Root and everything else, so the engine’s root anchor stays
identity and the whole vessel rocks as one rigid unit. Roll = A·sin(2πt) about the longitudinal axis;
pitch = 0.4A·sin(4πt) — a figure-8 that reads as riding swells, not a metronome.
All four shape knobs are exposed: amplitude, cycle (= the speed; fewer frames = faster), axis
(Auto/X/Y) with a continuous heading offset for a hull that is not axis-aligned or a swell taken on the
quarter, pitch amount (0 = pure beam roll) and pitch rhythm (2x = swells, 1x = rocking horse). Rotation-only
(no Keep-translations needed) and frame 0 is the rest pose, so it satisfies bind==frame0 and loops
seamlessly. Sampled at 24 steps because the pipeline keys LINEAR.wheels > 0 || rockDegrees > 0, so a floating unit can mark
nothing but Ignore and still rig. (The button is now Generate rig — it was “Vehicleize”, which stopped
being true once boats used it. “Transform” was considered and rejected: in a 3D tool that word already
means position/rotation/scale.)Two INDEPENDENT sine waves on the RootHull bone — no ratios or multipliers, each stated in its own
terms, because coupling them made every adjustment move something unintended:
Clip length (frames) 120 = 5.0s at 24fps
Roll — amount (deg) / swings per clip 6 / 1 side to side, about the hull's length
Pitch — amount (deg) / swings per clip 2.4 / 1 bow up and down, across the hull
— offset (deg) 90
Axis — hull length / heading Auto / 0
Cycle counts are integers so the loop always closes without a pop. The offset is what keeps an
equal-speed rock two-dimensional: at 0 the two swings stay in lockstep and the hull tilts along one fixed
diagonal (reads as a single axis); at 90 it traces an ellipse, the hull circling as it bobs. Frame 0 is
then a slightly heeled pose — the loop still closes exactly and the bake’s rest-normalize adopts it, so
bind == frame 0 holds. Roll 0 with a pitch value is a valid pitch-only rock.
Also in the Lab: Orientation (X/Y/Z straighten, applied to the DATA before anything measures the model, so axle/tread/hull-length inference all read the corrected pose), a waterline grid in the preview to judge it against, New model to clear a session, and a window scrollbar.
VERIFIED IN-GAME 2026-07-31 — the canoe floats and rocks. Registry: modelFile = the generated
_Wave.glb, animClip = Spin, staticParts empty, localNodeAnim off.
Flow for a floating unit: Vehicle Lab → New model → Browse + Probe parts → mark unwanted parts
Ignore → set the rock → Generate rig → point the Model Factory entry’s Model file at the
generated GLB with clip Spin, animated + Convert-raw-rig → Bake. This bypasses a broken source
animation entirely: the motion is authored, not transplanted.
Trap — a stale generated GLB. The rig step is a separate button from the bake, so the GLB can lag the
Lab’s settings silently. Spin’s frame range should equal your Rock cycle exactly (120 → Spin (0,120));
if it reads anything else, the GLB predates the current settings — re-run Generate rig. Older files also
carry the source’s own clip (e.g. Take 001) beside Spin: picking it bakes a unit that never moves,
with nothing in the logs to explain why. Newly generated rigs purge it.
Iterate headless, without Unity, and look at the result:
blender -b -P Tools/rig_anim.py -- <glb> <out.fbx> 24000 "" "<clip>" "" 1 "0,180,0" 1 "" 0 "" 1 "<staticParts>" <localNodeAnim 0|1>