Porting from Unreal Engine 5
Ferret's first task is porting games made in Unreal Engine 5. Its features follow what UE5 games use, and tools/ue-export carries a level across.
The importer app (Caroline's tools)#
When Unreal Engine is on the person's own Windows computer, nobody runs the scripts by hand: the Ferret UE importer/exporter, a tray app there, does the work, and Caroline in the editor drives it.
ue_placeslists the account's apps (online or not) with their UE versions, projects and maps.ue_import {project, map, level?}brings a map in as a new level: geometry with baked materials, environment, collision, physics, landscape. A level of that name must not exist yet.ue_export {level, project, map?}writes a level into a UE project as a new map under/Game/Ferret/<level>/(models and textures by Interchange, the actors, the lights, aPlayerStart); nothing else in the project changes.ue_statusfollows a job (a finished import brings its level into the project),ue_cancelstops it. A job that runs longer than about 3 minutes answers with an id; askue_statuslater.- The result's report names what was not mapped; the Blueprints and the parity check remain the work of the steps in the porting skill.
If no app is online, the person installs it from https://ferret.partners.solutions/ferret/download/ue-bridge/ferret-ue-bridge-latest.exe (the editor's menu UE > Download the UE importer/exporter), runs it and signs in with their SW account in its panel. It is Windows only and needs Unreal Engine installed there; there is no cloud variant.
UE concepts and their Ferret equivalents#
| UE5 | Ferret |
|---|---|
| Level, World Partition | a level (Levels and packages), level travel |
| Actor, Actor Component, Blueprint class | entity, component, behaviour registered by name |
| Subsystems, GameMode / GameState | services; a game's rules as behaviours and services |
| Character + Character Movement Component | character, with the same parameters |
| Camera, Spring Arm | the camera rig of character, with spring-arm collision |
| Animation Blueprint, Blend Space, Montage, Notify | animation graph, blend spaces, montages, notifies |
| Behavior Tree, AI Controller, NavMesh | ai |
| Collision channels and presets, overlaps | physics layers and masks, triggers |
| Enhanced Input | input actions and bindings |
| UMG | ui, rewritten per widget |
| Physics Constraint, fracture | constraints and fractured in a level's physics block |
| Save Game | platform state |
What has no equivalent (Lumen, Nanite, Chaos destruction, MetaHuman rigs) is replaced per game: baked lighting, LODs, prebaked destruction, simplified rigs. The porting report lists each case.
The pipeline#
- Geometry and materials.
ue/gltf.py, run inside the Unreal editor, exports the level with UE's glTF exporter, materials baked to PBR. It needs rendering on (-AllowCommandletRendering) and compiles the level's materials first, otherwise the baked textures come out empty. - Environment, collision, NavMesh settings, landscape, physics. The other scripts in
tools/ue-export/ue/write the level's sun and fog, its simple collision shapes, NavMesh build settings, landscape layers and physical materials next to the glTF. - Blueprints.
blueprint_text.pyandblueprints.pydump each mapped Blueprint class as a spec, stored next to the Ferret behaviour that replaces it. A mapping from the game (Blueprint class → behaviour + params) turns the level's actors into manifest entities. Unknown classes are listed in a report, not dropped. - Post-processing.
postprocess.mjs(glTF-Transform) prunes, instances repeated meshes (EXT_mesh_gpu_instancing), simplifies foliage, scales and re-encodes textures per tier, and optionally compresses to KTX2 and meshopt. - Characters and mobs.
inventory_characters.pylists every skeletal mesh, skeleton, animation Blueprint, sequence, blend space and montage of a project (size, bones, morphs, which skeleton each clip is on).characters.pyexports named skeletal meshes and animation sequences to GLB with the same glTF settings (FERRET_ASSETS,FERRET_OUT,FERRET_ANIMS=1to carry a skeleton's clips with the mesh), one file per asset and anexport.jsonreport. What is game-specific stays in the game: which meshes make a character, preset and customizer data, material values.
Landscapes (layers, far backdrop, virtual texture)#
A level may have several Landscape actors (a near ground and a far mountain plane). ue/landscape.py writes landscape-<label>.json for each (heights from the collision, LAND_STEP_<label> per-landscape step). ue/landscape_layers.py writes the paint layers, one greyscale weightmap per layer over the landscape's bounds, the landscape material's texture parameters and the RuntimeVirtualTextureVolume settings; landscape-weights.mjs packs four layers per RGBA image. postprocess.mjs --landscape spec.json with { "landscapes": [{ "file": "landscape-Landscape3.json", "name": "Landscape3", "stride": 8, "far": true }, { "file": ..., "name": "Landscape", "layers": [...], "weights": [...], "virtualTexture": { "meshes": "^SM_rock" } }] } builds one mesh per landscape (the far one decimated, kept out of the collision) and writes them into environment.landscapes, which @ferret/levels applies at load (landscape.ts): a material plugin blends the layers per pixel by the weightmaps (optionally height-blended), the far plane is marked never walked or picked, and the blended colour is baked once into a texture the named props sample near their feet (the runtime virtual texture's job in Unreal). The layer textures, their tiling and the virtual-texture meshes come from the landscape material's graph (materials.py writes it as text), so the spec is written by reading that graph.
node tools/ue-export/postprocess.mjs level.glb out/ --tier all --ktx2 --meshopt \
--environment environment.json --collision collision_shapes.json --nav nav.json
The output is a level folder (and per tier) that the level service loads, or a .ferret package.
Porting a game#
- Inventory: list the levels, classes, plugins and UE features in use; the porting report says what maps as is, what needs a rewrite and what has no equivalent.
- Mapping of each UE class to a behaviour.
- Rewrite of behaviours, services and screens from the specs, one class at a time, with tests in the fixed-step loop.
- Export of each level; the model viewer and agents check the result.
- Parity check: the ported level is compared with the original from the same cameras, along the same walked paths, with the same triggers firing.
- Re-export when the original changes; the game's edits on top of the export stay, because the manifest refers to nodes by name.
Lessons from real ports#
Most of the time in a port goes to things that run without an error and come out wrong. Three pages collect what was found:
- Running the Unreal side of a port: run settings that give complete output, and the traps of the export runs.
- Making a ported level look right: black or dark surfaces, smeared materials, wrong colours, flat light, with causes and fixes; find the cause before changing anything.
- Parity with the original: the same views shot in Unreal and in Ferret, the axis mapping, and reading the differences.
- Porting characters and animation: clips, parts, hair, morph targets, size, Animation Blueprints.
The model viewer#
pnpm viewer starts tools/viewer: a page that shows a level or an asset, plus an HTTP server that relays commands from clients (agents, tests, an MCP server) to the open page and back, serves screenshots of the active camera, and saves edited manifests. pnpm viewer:headless opens the page without a screen, so a CI run or an agent needs none. The viewer grows into the level editor.
Characters. ?characters=<index.json> (or --characters on the viewer server) turns the page into a character viewer for what a game exported: a list of the characters, the clips of the clips file (or the ones in each character's own file) as buttons, "play all", and a report on each character: a file that does not load, a mesh without a skin or a material, a clip with no track on this body or that moves nothing; clip bones the body does not have are noted, not counted as problems. The index is a small JSON the game's build writes next to the files, urls relative to it: { "clips": "cc_clips.glb", "characters": [{ "name": "CC_Female_Preset_01", "url": "low/CC_Female_Preset_01.glb" }] }. A character made of modular parts lists their files in "parts" (one file per part, one skeleton): the viewer shows them together and gives each the clips; url is not loaded then. A character may carry its own "clips" file, which wins over the index's (families with a skeleton of their own: NPCs, animals). Commands for agents: characters.list, characters.show {name} (answers with the report), characters.play {clip}, characters.playall {seconds?}, characters.stop, characters.check {name?} (every character when no name is given: { reports, problems }).