FERRETGame engine over Babylon.js
Ferret

Porting characters and animation from Unreal

Characters go through ue/inventory_characters.py (what exists), ue/characters.py (export), characters.mjs (clips, assembly, size) and animbp.mjs (Animation Blueprints to graphs). The engine part knows no game; which parts make a character and with which values is the game's spec. The overview of the pipeline is in Porting from Unreal Engine 5.

The order that works#

  1. Inventory every skeletal mesh, skeleton, Animation Blueprint, sequence, blend space and montage: sizes, bones, morph targets, which skeleton each clip is on. It shows which families share a skeleton (then they share clips) and which have their own.
  2. Read the game's data, not its meshes: a playable character built by a customizer is a preset (head, body, apparel rows with material options and tints, hair rows, scalars, face morphs) in data tables; an NPC Blueprint often holds one body component and builds the rest in its graph. Dump the presets and tables first.
  3. Export one mesh per editor run, with its clips (FERRET_CLIPS=1) on a scratch level.
  4. Clips (characters.mjs clips): one skeleton with the N clips; keep only what the game plays (--keep).
  5. Assemble a character from parts (characters.mjs assemble): one skin, morphs baked by name, four influences, tints.
  6. Size per tier (characters.mjs optimize): triangle budget, textures capped, morph targets dropped.
  7. Animation Blueprints (animbp.mjs): the pose chain, blend spaces, slots, variables to a graph; what it cannot read goes to unknown.
  8. Check in the character viewer (?characters=<index.json>): every file loads, every clip moves bones, no part without skin or material.

Traps found in real ports#

  • Clips export only through a playing component. An animation sequence is not exportable as an asset. characters.py puts one actor per sequence on a scratch level and exports that world; the scratch level keeps earlier actors, so one mesh per editor run or the file carries every earlier family's clips.
  • The same skeleton under two names. CC_Skeleton (70 bones) and metahuman_base_skel (87) share bone names: the clips bind by name, no retarget map, no restRotation. Compare bone names before writing a retarget.
  • The template's clips are shared. Seven families played the six ThirdPerson* clips of the UE template; only a few had clips of their own. Do not export the same clips seven times.
  • Morph targets dominate the size. A head with 104 targets was 65 MB; a body family came to 106 MB with them. Bake the few a preset uses (by name, from <mesh>.morphs.json, because the glTF targets have no names) and drop the rest.
  • Hair strands do not survive the bake. UE draws hair with groom shading, not the card's UV: a bob came out as stripes, a fade and a beard not at all. Make hair and beards one opaque colour (solid); eyebrows and lashes keep their alpha.
  • A hair tint is not a colour. The customizer's Hair Tint R and G are melanin and redness weights of UE's hair material, so (0.35, 0, 0) is dark brown, not red. hairColor() reproduces the engine's melanin function; check it against UE's own bake of the material.
  • Parts on their own skeleton lose bones. Hair on a hair skeleton has bones the body lacks; assemble makes such a bone follow its nearest body ancestor (else the head), so it moves with the head instead of standing in the rest pose.
  • One skin per part costs skeletons. The glTF loader makes a Babylon skeleton per skin (14-16 per person); assemble puts every part on the body's skin (2-3 per person). Parts whose bind pose of the neck or head differs by centimetres stay on their own skin.
  • Facing. The exported model faces +Z; a 180-degree "fix" made the hero walk backwards. Measure the toes against the walk direction instead of guessing.
  • Budgets are sums. A family made of many small parts (each under the size that is left alone) lands over the tier's triangle budget; count them.
  • Additive poses export as ordinary clips (aim offsets, leans): --no-additive or --keep them out unless a graph layers them.
  • Animation Blueprints with one name in several folders overwrite each other in a dump keyed by name; key by path.
  • A packaged game for OS Beroun relies on the system's /lib/ferret/ (the navigation WASM); a game that loads it from its own folder on the web stays on a black screen on the console when the system lacks it.

Check afterwards#

  • The viewer report: no file that fails, no mesh without a skin or material, every clip moves bones (clip bones the body lacks are noted, not counted).
  • In the game: the body shows in third person and hides in first, the clips follow the movement speed, and the hero walks forwards.
  • Sizes per tier against the budget, clips file included.