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#
- 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.
- 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.
- Export one mesh per editor run, with its clips (
FERRET_CLIPS=1) on a scratch level. - Clips (
characters.mjs clips): one skeleton with the N clips; keep only what the game plays (--keep). - Assemble a character from parts (
characters.mjs assemble): one skin, morphs baked by name, four influences, tints. - Size per tier (
characters.mjs optimize): triangle budget, textures capped, morph targets dropped. - Animation Blueprints (
animbp.mjs): the pose chain, blend spaces, slots, variables to a graph; what it cannot read goes tounknown. - 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.pyputs 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) andmetahuman_base_skel(87) share bone names: the clips bind by name, no retarget map, norestRotation. 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 TintR 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;
assemblemakes 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);
assembleputs 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-additiveor--keepthem 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.