Model Format and 3D Composition
FOnline uses .fo3d model descriptions to compose baked 3D meshes, converted source animations, layer-selected equipment, child models, particles, material overrides, and cut volumes into a client-side model instance.
Use this guide for the authoring and runtime model. Use the generated token reference, asset and limit reference, validation rules, and canonical JSON model for the exact current-revision contract.
Scope and authority
The owning sources are:
Source/Tools/ModelMeshBaker.cppandSource/Common/ModelMeshData.*for.fbx/.objmesh import, validation, and the mesh-onlyLFMODMSHpayload;Source/Tools/ModelSourceLoader.*,Source/Tools/ModelAnimationConverter.*, andSource/Common/ModelAnimationData.*for source skeleton/clip extraction, compatibility analysis, Ozz conversion, and the native rig payload;Source/Tools/ModelInfoBaker.cppfor.fo3dparsing, include expansion, dependency and source validation, model-info serialization, runtime-rig creation, and animation metadata;Source/Client/ModelManager.*,ModelHierarchy.*,ModelInformation.*,ModelInstance.*, andModelAnimation.*for strict runtime loading, shared immutable data, per-instance composition/pose state, animation controllers, and drawing;Source/Frontend/Rendering.hplus the generated CMake project interface for compile-time model limits;- the model baker, mesh-data, source-loader, animation-data/converter/runtime, skeleton-compatibility, Ozz, and client-engine tests for executable grammar, wire, conversion, loading, and failure examples.
This page is reusable Engine documentation. An embedding project owns concrete model names, layer meanings, model-layer values, animation enums, art direction, equipment rules, gameplay timing, and visible validation scenes.
The exhaustive machine model is generated from BuildTools/ModelFormatInterface.json. Its generator compares the documented token set directly with ModelDescriptionParser::ParseToken; parser drift makes documentation validation fail.
Pipeline overview
The model pipeline has two ordered bakers and shared source/conversion modules:
ModelMeshBakerruns at order4. It imports.fbxand.objsource files and writes a versioned, mesh-onlyLFMODMSHresource at the same path and extension. Clips and mutable pose data are not stored in this hierarchy payload.ModelInfoBakerruns at order6. It parses concrete.fo3dfiles, validates references and source freshness, loads the selected source skeletons/clips through the per-bakeModelSourceAssetCache, converts a canonical runtime rig, and writes versionedLFMODINFwith a requiredLFOZZRIGpayload at the same.fo3dpath. It also emitsModelAnimationInfo.foinfofor common duration and bounds lookup.
The client never parses authored text or source FBX/OBJ data. ModelManager loads shared mesh hierarchies, ModelInformation strictly loads one immutable model description and runtime rig, and each ModelInstance owns mutable controllers, pose matrices, children, particles, and render composition. Old headerless payloads and partial rig fallbacks are rejected.
Files whose basename starts with TEMPLATE_ are include-only. They affect concrete descriptions and bake timestamps, but are not emitted as independent .fo3d resources or ModelAnimationInfo.foinfo sections.
Source mesh contract
Supported inputs
Current ModelMeshBaker scans only:
.fbxfor skeletal or static meshes, skinning, material diffuse texture names, source skeletons, and animation clips;.objfor static models, attachments, and cut volumes.
Legacy .x and .3ds model paths are not current inputs. A file retaining one of those extensions is not selected by ModelMeshBaker.
Import behavior
The mesh baker and source loader use pinned ufbx through separate owners. The mesh baker emits hierarchy, bind, vertex, index, skin, and material data; the source loader extracts validated skeleton/TRS/clip data for animation conversion. Embedded files are ignored, skinning is evaluated, skin weights are cleaned, and missing normals receive deterministic handling in the mesh path.
Author meshes with these constraints:
- faces must already be triangles;
- a concrete model must contain at least one drawable mesh;
- node names become bone names, and nodes with attached geometry become drawable mesh names;
- use one material per drawable node when deterministic texture ownership matters;
- the first material’s file-backed
DiffuseColortexture becomes texture slot0; - texture filenames are stored without their original directory and later resolved relative to the baked mesh;
- only the first skin deformer is consumed;
- skin-cluster count must fit
FO_MODEL_MAX_BONES; - only
FO_MODEL_BONES_PER_VERTEXinfluences are retained per vertex, then normalized; - a non-skinned mesh receives a deterministic single-bone fallback;
- animation stack names are the clip names used by
.fo3dAnimentries, but those clips are loaded from source and converted into the model-info rig rather than serialized intoLFMODMSH; - direct
.fbxattachments must be rest-only: use a child.fo3dwith explicitAnimmappings when an attached source contains clips; - external animation sources should contain hierarchy and animation only. Drawable geometry is rejected unless the exact selected file has a temporary
AllowAnimationGeometryexception.
The source loader rejects non-finite transforms, duplicate case-insensitive clip names, invalid durations or key times, excessive counts/depth, and malformed skeleton relationships before conversion. Animation sources may contribute compatible canonical joints that have no physical ModelBone; physical meshes and cuts remain in the base hierarchy.
The current default limits are:
| Project option | Runtime constant | Default |
|---|---|---|
FO_MODEL_LAYERS_COUNT |
MODEL_LAYERS_COUNT |
30 |
FO_MODEL_MAX_TEXTURES |
MODEL_MAX_TEXTURES |
8 |
FO_MODEL_MAX_BONES |
MODEL_MAX_BONES |
54 |
FO_MODEL_BONES_PER_VERTEX |
MODEL_BONES_PER_VERTEX |
4 |
These are binary and shader shape contracts. If a project overrides them, its client binaries, baked model resources, Critter.ModelLayers data, effects, and packages must use the same values.
Lexical syntax
A .fo3d file is a sequence of whitespace-separated tokens:
#and;start a comment;- there is no quoted-string or escape syntax;
- paths and names therefore cannot contain whitespace;
- one line may contain multiple directives;
- each directive consumes its required arguments and parsing continues with the next token;
- unknown tokens and missing arguments are bake errors;
- integer arguments accept numbers, explicit booleans, or enum names known to the baking metadata resolver;
- float arguments must parse as finite numbers.
Compact entries are legal:
Layer 1 Value 2 Attach Hat.fbx Link Head RotY 180 Texture 0 Hat.tga
For maintainability, keep structural selectors (Layer, Value, Root, Attach) before the modifiers that apply to them.
Minimal descriptions
A static model can be as small as:
Model Props/Crate.obj
A skeletal model with one animation and one layer-selected attachment:
Model Characters/Human.fbx
RotationBone Spine
Anim CritterStateAnim.Unarmed CritterActionAnim.Idle ModelFile Idle
Anim CritterStateAnim.Unarmed CritterActionAnim.Walk ModelFile Walk
Layer 1
Value 1
Attach Items/Hat.fbx Link Head
Enum names depend on the embedding project’s metadata. Numeric examples in engine tests prove parser behavior but are not a recommended project vocabulary.
Includes and templates
Include parses another file inline:
Include TEMPLATE_Humanoid.fo3d mesh Human.fbx scale 0.9
Arguments after the path are name/value pairs. Before tokenization, every literal %name% in the included text is replaced with its value:
# TEMPLATE_Humanoid.fo3d
Model %mesh%
Scale* %scale%
Important include rules:
- the include path is relative to the file containing
Include; Model,Attach, andCutpaths inside the included text are relative to the included file;- an
Animfile other thanModelFileis resolved later relative to the concrete.fo3doutput; - include arguments consume the rest of their line, so do not place another directive after them;
- replacements are plain text, not token-aware substitutions;
- included content shares parser state with its caller;
- recursive includes are rejected;
- the newest timestamp in the complete include graph controls incremental rebaking.
Prefer self-contained templates that establish their own Root, Layer, Value, and Mesh context. A template that silently depends on caller state is difficult for humans, AI agents, and validators to reason about.
Parser state
The parser tracks:
- the selected
Layer; - the selected
Value; - the current link receiving modifiers;
- the current
Meshselector used byTextureandEffect.
At file start, the current link is the default root. Top-level transforms and material modifiers therefore apply to the base model even without an explicit Root.
Layer or Value:
- updates that selector;
- clears
Mesh; - redirects the current link to a dummy object.
After selecting a layer/value pair, write Root, Attach, or AttachParticles before any transform, material, disable, or cut directive. Modifiers written while the dummy link is current are parsed but discarded.
There is no directive that restores Layer to the initial -1 state. Author all default-root declarations before the first Layer, or put them in an earlier include.
Root also clears Mesh. Attach and AttachParticles create a new link and clear Mesh. Put Mesh after the link selector it should affect.
Link is stored only for a non-default, non-dummy link. On the default root it is ignored. A Link on a layer Root is serialized but has no child to attach; omit it.
Layers and values
Layer selects an index in the fixed model-layer array. The valid range is:
0 <= layer < FO_MODEL_LAYERS_COUNT
Value selects an exact project-defined integer. Zero means inactive and cannot create a layer Root, Attach, or AttachParticles entry.
At runtime, ModelInstance::PlayAnim:
- copies the supplied model-layer array, or reuses the previous array;
- applies exact
AnimLayerValueoverrides for the requested state/action pair; - resets the model to default-root data;
- finds entries whose
LayerandValuematch; - applies root modifiers and material changes;
- creates or retains child models and particles;
- removes no-longer-selected children and particles;
- regenerates combined meshes when composition changed.
A layer value is rendering composition state, not merely cosmetic metadata. Changing it can alter transforms, animation speed, visible geometry, materials, draw effects, cuts, child models, particles, and batching.
The meaning of every layer index and value belongs in the embedding project’s documentation and tests.
Root modifiers
Root selects the base model’s default link when no layer has been selected:
Root
Scale 0.9
RotX 90
With a selected non-zero layer/value pair, it creates a conditional root modifier:
Layer 3
Value 2
Root
DisableMesh Torso
Texture 0 Armor.tga
Conditional root links can:
- add transforms and speed multipliers;
- override textures and effects;
- disable other layers;
- disable meshes;
- add cut volumes.
They do not create a child model.
Model attachments
Attach requires a selected layer and non-zero value:
Layer 1
Value 4
Attach Weapons/Rifle.fo3d Link RightHand
The child path is relative to the declaring file.
Single-bone attachment
With Link <bone>, the complete child is parented to one validated bone:
Attach Hat.fbx Link Head
The link’s rotation, translation, scale, speed, materials, disables, and cuts apply inside the child model instance.
Shared-skeleton attachment
Without Link, runtime pairs same-named child and parent bones:
Attach ArmorTorso.fbx
Use this only for clothing or body-part assets authored against the same skeleton. Runtime creation fails if no common bones exist.
Child descriptor versus direct mesh
Use Attach child.fo3d when the child needs its own:
- base mesh selection;
- nested layers or attachments;
- default material/effect policy;
- cuts;
- animation declarations;
- rendering flags.
Use direct .fbx / .obj attachment for a simple baked hierarchy. A child .fo3d is baked and validated independently; the parent validates that the descriptor exists and that the parent Link bone is valid.
A direct attachment has no description-level scale correction. Its static maximum-axis extent must remain within Baking.ModelAttachmentMinExtent .. Baking.ModelAttachmentMaxExtent; otherwise baking fails with the measured extent and limit. Use a child .fo3d when an explicit scale is part of the composition. Mesh nodes with a negative transform determinant are rejected earlier by ModelMeshBaker: reset/freeze mirrored geometry to a positive transform before export instead of relying on the baker to flip normals and winding.
Particle attachments
AttachParticles is layer-selected:
Layer 8
Value 1
AttachParticles Particles/Jet.spk Link Backpack
MoveY 0.15
RotY 90
The particle path is a global baked-resource path, not relative to the .fo3d file. Reference the generated .spk or .efk resource rather than its .spark or .efkproj authoring source. Always provide a valid Link bone; runtime particle creation requires it.
The link’s MoveX, MoveY, MoveZ, and RotY feed particle placement. The particle instance remains alive while the exact layer/value link stays active and is removed when composition changes.
The attached resource’s XML, registered SPARK objects, renderer fields, effects/textures, runtime cache, and visible validation are owned by Particle Format And Runtime. Model-bone particles use the direct 3D composition path rather than ParticleSprite’s atlas/direct-scene selector.
Transforms and speed
Per-link fields are:
RotX,RotY,RotZin degrees;MoveX,MoveY,MoveZin model coordinates;ScaleX,ScaleY,ScaleZ;Speedas a playback multiplier.
Scale sets all three scale axes.
Every field has assignment, additive, and multiplicative forms:
Scale 0.9
Scale+ 0.1
Scale* 1.5
RotY 90
RotY+ 15
RotY* 0.5
The + and * forms use special zero initialization:
- if the current field is zero, the operand becomes the field value;
- otherwise addition or multiplication is applied normally.
This lets a template use Scale* 0.9 or Speed* 1.2 without requiring an earlier assignment. It also means declaration order is observable.
At runtime, zero means identity/no contribution. Non-zero transforms are multiplied into the current model transform. A negative final Speed is rejected during baking; zero means no speed contribution.
Meshes, textures, and effects
Mesh selects a drawable node by name:
Mesh Torso
Texture 0 Armor.tga
Effect Effects/Armor.fofx
Mesh All clears the selector, so following material directives target every drawable mesh in the current link’s model.
Subset is obsolete. The parser consumes its argument and logs a warning, but does not select anything. Never use it in new content.
Textures
Texture <slot> <name>
The slot must be in [0, FO_MODEL_MAX_TEXTURES).
For a normal texture name:
- the target
Meshmust exist and be drawable; - the texture path is resolved relative to the current target mesh file;
- the texture must exist in baked resources.
An imported material’s diffuse texture is the default for slot 0. All other slots start empty unless assigned.
Inside an attached child, Parent copies the first matching parent’s current texture at the same slot. Parent_<mesh> copies it from the named parent mesh:
Texture 0 Parent_Torso
Do not use Parent on a root description. When a parent has several meshes, prefer the explicit suffix.
Effects
Effect Effects/SkinnedArmor.fofx
Effect paths are global baked-resource paths loaded for model usage. Parent and Parent_<mesh> copy the parent’s current effect using the same attached-child rules as textures.
Meshes can share one combined draw batch only when effect, texture set, and bone capacity are compatible. Material overrides may therefore change batching and should be measured on representative composed models.
Disabling layers and meshes
DisableLayer accepts hyphen-separated layer indices:
DisableLayer 5-6-7
When the link is active, those layer slots are skipped inside the affected model instance.
DisableMesh accepts hyphen-separated drawable node names:
DisableMesh Hair-HelmetBase
DisableMesh All stores a wildcard and disables every mesh in the affected model instance.
Use disables to express mutually exclusive composition, but keep project layer ownership explicit. Cyclic or order-dependent exclusion policy quickly becomes difficult to test.
Cut volumes
Cut removes geometry from selected composed-mesh layers:
Cut CutVolumes/Helmet.obj All HeadVolume - - -
The six arguments are:
- cut-volume
.fbx/.objpath, relative to the declaring file; - hyphen-separated target layers, or
All; - hyphen-separated drawable shape names from the cut file, or
All; - first unskin bone, or
-; - second unskin bone, or
-; - unskin shape,
~shapefor reversed behavior, or-.
All layers expands to every compile-time layer except the currently selected layer. At default-root scope, all layers are included.
All shapes selects every drawable shape except the separately named unskin shape.
The current runtime classifies a cut shape by its baked vertex count:
- exactly
36vertices: axis-aligned box bounds; - any other count: sphere radius derived from the X extent.
This is an Engine format rule, not a general mesh heuristic. Author dedicated simple cut assets and validate the result visually.
Both unskin bones must be provided together. An unskin shape requires both bones. All referenced bones and drawable shapes are checked during baking.
Applying any cut disables normal culling for the composed model. Treat cuts as a correctness feature with a rendering cost; avoid using detailed production meshes as cut volumes.
Rendering controls
Automatic model-sprite layout
DrawSize and ViewSize are removed legacy directives. ModelInfoBaker writes
aggregate, idle-priority view/name, and per-animation bounds to
ModelAnimationInfo.foinfo version 2. At runtime the client projects those bounds for
each direction, extends them with enabled child models and layers, and derives the
offscreen frame, visual anchor, lighting envelope, and interaction/view rectangle.
Every non-particle child link serializes a validated root-space AABB; the
default link and particle links carry no geometry payload. The baker includes
disabled meshes, nested descriptions, link transforms, and the parent’s sampled
animations when calculating that envelope. Runtime framing unions the active
animation bounds with the selected link envelopes and projects only their
corners—there is no per-frame weighted-vertex sweep. Live particles can still
force bounded expansion and rerender when they exceed the baked geometry
envelope. Authors therefore tune source transforms, animation reach,
attachments, and Render.ModelProjFactor, not fixed pixel rectangles inside
.fo3d.
Validate every direction and representative animation in a visible client. Use
Game.DumpAtlases() or the mapper’s Dump atlases command when diagnosing clipping,
unexpected empty space, polygon edges, or crop placement. See
Baking Pipeline and
Frontend and Rendering for the binary and
runtime contracts.
Other flags
DisableShadowdisables model shadow drawing.DisableAnimationInterpolationselects nearest-key sampling when the baked runtime rig is loaded.DisableBackwardAnimselects forward walk/run instead ofWalkBack/RunBackand aligns look direction with movement.RotationBone <bone>enables the movement overlay controller and directional torso/head rotation around a validated body bone.FastTransitionBone <bone>resets transition state for a newly attached child using that link bone.
Animation boundary
The .fo3d animation directives are:
Anim <state> <action> <ModelFile|animation-source> <clip|~clip|Base>
AnimSpeed <state> <action> <positive-factor>
AllowAnimationGeometry <external-animation-file>
AnimLayerValue <state> <action> <layer> <value>
StateAnimEqual <from> <to>
ActionAnimEqual <from> <to>
FastTransitionBone <bone>
RotationBone <bone>
DisableAnimationInterpolation
DisableBackwardAnim
Use Model Animation for:
- first-entry-wins tuple behavior;
ModelFile,Base, and reversed~cliplookup;- source skeleton compatibility, conversion into the required runtime rig, and the temporary external-animation geometry exception;
- one-step aliases;
- effective duration;
- common versus loaded-client lookup;
- animation substitutions and validation.
AnimLayerValue applies to the exact requested pair before model composition. Alias resolution belongs to animation lookup; do not assume an alias also rewrites the key used for layer overrides.
AllowAnimationGeometry is a narrow migration aid, not a normal asset policy. It names one exact external file selected by Anim, resolves from the final concrete .fo3d, and is consumed only by baker validation. It is not serialized. Duplicate paths, duplicate resolved targets, non-selected files, and exceptions left behind after geometry removal are hard errors. Repair the source into a geometry-free animation export while preserving required helper/bone hierarchy, then remove the exception in the same asset migration.
3D skeletal animation is separate from the 2D NextX / NextY contract in Sprite Root Motion.
Runtime loading and caching
ModelManager::CreateModel(name) accepts:
- a baked
.fo3ddescription, which creates full model information and composition behavior; - a baked mesh path, which creates a basic rest-pose hierarchy-backed model without
.fo3ddeclarations or an animation controller.
Model descriptions and mesh hierarchies are cached by resource name. Immutable animation clips, remaps, bindings, and canonical skeleton data belong to ModelInformation; mutable timelines, sampled poses, matrices, linked children, and procedural transforms belong to each ModelInstance. Layer changes reuse active child links by stable baked link id and remove children and particles that no longer match.
Combined mesh generation merges compatible visible meshes until effect, texture, or bone-capacity differences require a new batch. Cuts are applied after parent and child meshes have been combined.
Do not mutate or parse the baked binary .fo3d, .fbx, or .obj payloads from project scripts. Their binary layout is a private baker/runtime contract.
Failure behavior
ModelInfoBaker rejects or reports:
- missing
Model; - missing, unreadable, stale, or malformed baked meshes and their source files;
- a primary mesh with no drawable geometry;
- missing default diffuse textures;
- missing explicit textures, effects, particles, child descriptions, or cut files;
- invalid layer or texture indices;
- zero layer values for
Root/Attach; - missing bones or drawable mesh references;
- malformed include graphs or replacement pairs;
- invalid or non-finite numbers;
- negative link
Speed; - non-positive
AnimSpeed; - unknown animation enums, missing clips, incompatible source skeletons, or invalid runtime-rig conversion;
- direct attached FBX files with clips, external animation files with unexpected drawable geometry, and duplicate/non-selected/stale
AllowAnimationGeometryexceptions; - mirrored mesh nodes and direct FBX/OBJ attachments outside the configured Engine world-unit extent;
- invalid cut layer/shape/unskin combinations;
- unknown tokens.
Runtime loading repeats critical binary and range checks. Runtime exceptions indicate corrupted/stale baked data or a validation gap; do not catch them and substitute an unrelated model as a silent fallback.
Legacy content warning
Do not infer current support from old FOnline project files. In particular:
.xand.3dsare not selected by the current mesh baker;AnimEqualwas replaced by the domain-specificStateAnimEqualandActionAnimEqual;CalculateTangentSpace,RenderFrame, andRenderFramesare not current tokens;Subsetis accepted only as an obsolete warning path and has no selection effect.
Port legacy assets by first translating them to the current source formats and grammar, then validating the result against the current Engine revision. Legacy project content is evidence of historical usage, not a normative format specification.
Authoring practices
For maintainable model descriptions:
- Keep default-root declarations before the first
Layer. - Give every concrete description exactly one intentional final
Model. - Prefix include-only files with
TEMPLATE_. - Make templates establish their own selector context instead of inheriting caller state.
- Use enum names for state/action and project layer constants where metadata exposes them.
- Document every project layer index, allowed value, owner, and conflicting layer.
- Use one drawable node per independently overridden material or effect.
- Use direct mesh attachments for simple props and child
.fo3ddescriptions for reusable composed objects. - Always give particle attachments an explicit
Link. - Use exact case in paths, bone names, mesh names, animation stacks, and effects.
- Keep cut volumes simple and purpose-built.
- Exercise the full layer combination matrix, not only each attachment in isolation.
- Export external animation files without drawable geometry; preserve required helper/bone hierarchy and tracks when repairing older files.
- Treat every
AllowAnimationGeometryline as temporary migration debt with a named source-repair owner, then remove it as soon as the export is clean. - After source-loader, converter, mesh-wire, or animation-source changes, run a full force bake followed by an incremental bake to prove both conversion and dependency timestamps.
For AI-authored content, record the intended parser state before emitting each modifier:
current layer = 3
current value = 2
current link = Attach Armor.fo3d
current mesh = Torso
next directive = Texture 0 Parent_Torso
If that state cannot be stated unambiguously, split the compact line or make the selectors explicit.
Validation workflow
After changing model assets or descriptions:
-
Regenerate and validate the format reference:
python BuildTools\docs_model_format.py --write python BuildTools\docs_model_format.py --check python -m unittest BuildTools.tests.test_docs_model_format -
Run focused Engine model tests:
.\Binaries\Tests-Windows-win64\LF_UnitTests.exe "ModelBaker*" -
Rebake the embedding project:
cmake --build Build\Auto --config RelWithDebInfo --target BakeResources -
Launch a visible client scene that covers:
- automatic framing, view/name anchoring, and atlas crop bounds;
- every authored layer/value;
- single-bone and shared-skeleton attachments;
- particles;
- texture/effect inheritance;
- mesh/layer disables;
- cuts;
- idle, movement, turn, backward, and action animations;
- shadows and animation interpolation.
-
Record project-specific layer semantics, expected screenshots, and regression routes in the embedding project’s documentation.
A clean bake proves grammar, asset closure, enum/range validity, and baked serialization. It does not prove pose quality, scale, clipping, material appearance, animation blending, cut geometry, interaction bounds, or performance.
Change routing
When changing:
.fo3dtokens, parser state, include behavior, path rules, or validation: update this guide,BuildTools/ModelFormatInterface.json, generated model-format outputs, and focused tests;.fbx/.objimport, skinning, material extraction, animation-stack conversion, or model limits: update the asset and limit contract and run native mesh-baker tests;- model layers, attachments, particles, transforms, textures, effects, cuts, batching, or rendering flags: update composition/runtime sections and validate a visible client scene;
- animation tuples, aliases, speed, duration, or script lookups: update Model Animation;
- 2D frame offsets or walk/run sprite phase: update Sprite Root Motion;
- project model catalogs or layer meanings: update only the embedding-project documentation while linking back to this reusable contract.