FERRETGame engine over Babylon.js
Ferret

Asset library

What the Unreal ports have already exported (baked materials and textures, characters with their clips) is kept once, as a library any game takes from by id instead of copying files out of another project. The library is a set of .ferret packages of kind assets and a catalog.json that lists their items. On OS Beroun it lives in /lib/ferret-assets/, next to the engine's /lib/ferret/; on the web it is a folder on a server, read through the asset store like any catalog.

This page is the format.

Items#

Every item has an id (lower case letters, digits, ., _, -) that stays the same across re-exports, and an item file items/<id>.json in its package. A package lists its items in provides.assets; the package check (FerretPackage.validate) reads every item file, so a bad item, a file it names that is not in the package, an item that is not listed and a listed id with no item all show up as problems with a path.

TypeWhat it isMain fields
texturea texture set: base colour, normal (OpenGL, +Y), occlusion-roughness-metallic, emissivemaps, tiling (metres per repeat), seamless, size, aliases
materiala PBR description that maps one to one to Babylon's PBRMaterialcolour and factors, maps (ferret://<package>/<path>), uv, worldAligned, alpha, variants
charactera skinned model per quality tier, a part of one, or an assembled onekind (human, animal, creature, part), tiers, skeleton, clips, graph, movement, height, facing, parts
clipsa skeleton with animations and no meshskeleton, compatible, file, clips (name, duration)

A material refers to its textures with ferret://<package>/<path> and may refer only to packages it depends on (dependencies in ferret.json), so several materials share one texture package. A character names its clip sets by id; a clip set belongs to a skeleton id, and skeletons that share bone names are marked compatible, because clips bind to bones by name.

The catalog#

The library's catalog is an ordinary catalog (packages, tiers, hashes, signature) with an assets section that finds an item without opening a package:

"assets": {
  "bear": { "package": "chr-bear", "type": "character", "name": "Bear", "tags": ["animal"], "preview": "previews/bear.webp", "bytes": { "desktop": 1840000, "low": 420000 } }
}

validateCatalog checks the section: each entry points at a package of the catalog and has a known type and a name.

Tools#

tools/asset-lib/ builds the library from the exports of a port. The tools take export folders as arguments and name no game; ids.json holds the stable ids (and a few hand-set decisions: which materials are tiling or baked, which clip exports to skip, the names of skeletons).

ToolDoes
inventory.mjslists what the exports hold: counts, duplicate images, shared skeletons, characters missing a tier
materials.mjsa level's baked glb -> texture packages and material packages (repeating maps only; ORM packed; world-aligned size kept)
characters.mjssized characters, parts and clip sets -> packages (the clips file stripped of its mesh)
authored.mjsclip sets made in Ferret (ids.json authoredClips: the clip JSON of tools/fight-clips) -> clip set packages: each clip sampled every frame on the rest pose of the body it was made for, notifies and loops in the item
previews.mjsa preview of every item in one studio scene, and the load check
build.mjspacks the folders, writes the catalog and the layout, and checks every file it wrote
make.mjsall of the above for --from <export dir>

The output layout is catalog.json, c/<hh>/<sha256>.ferret (the packages, content addressed) and previews/<id>.webp. A character package holds all three tiers; a client that opens it over HTTP reads only the tier it needs. A texture package has a .mobile / .low sibling only where its maps are larger than that tier's cap.

In a game#

AssetLibrary (in @ferret/levels) is a service that opens the catalog through the asset store, so the cache, the hashes and range reads of the streaming layer apply. It opens the package of an item, and the packages that item depends on, on first use. Without an argument it opens globalThis.FERRET_ASSETS, else /lib/ferret-assets/.

const library = world.addService(new AssetLibrary({ store, mount, assets: runtimeAssets, tier: 'mobile' }));
await library.open();                                          // or open('https://host/lib/catalog.json')
library.list({ type: 'material', text: 'bark' });               // from the catalog: no package is opened
const wall = await library.material('bricks', { variant: 'red' });   // PBRMaterial, textures shared between materials
const bear = await library.character('bear', { position: [2, 0, 3] }); // a placeholder at once, the model swaps in
const { groups } = await library.bindClips(bear);                 // the clip sets of the character on its bones, by bone name

A tier picks the file a character is loaded from (a lighter tier when the item has no heavier one's equal, never a heavier one than asked for when a lighter exists) and, for texture packages, the .mobile / .low sibling. A relative base in the catalog (./) is the folder of the catalog file, so the folder can be moved or served from anywhere. Commands for an agent: lib.open, lib.list, lib.item, lib.material, lib.spawn.

Browsing#

The viewer shows the library with ?library (or ?library=<catalog.json url>): a grid of previews with a type filter and a search; opening an item puts it in the scene (a character with its clips, a material on a sphere with its variants, a texture set on a plane) with its id, sizes and the line of code that loads it. Arrows move in the grid, Enter opens, Escape closes. The commands are library.list, library.show {id, variant?, tier?}, library.play {clip} and library.clear. The docs site gets a gallery page of every item from node tools/docs/build.mjs --library <dir of catalog.json>.

The sample examples/library is a scene made only of library items (materials on a floor and walls, animals with their clips, a person on a locomotion graph); it asks for ids and takes what the catalog has, so it runs on any library (?lib=<catalog.json url>).