Repository navigation
Fit Lab
Fit Lab shows equipment parts on the animated UO body through the UO camera, lets you tune each equipment slot's fit, hides body faces under clothes to stop poke-through, and renders the real Blender output beside the live preview.

▶ Watch the Fit Lab tour (MP4)
New look, facing ring and 2×2 view: the screenshots on this page show the DirectorDeck-styled Fit Lab from PR #19 (pending merge). Until it is merged,
mainhas the previous styling, the old direction ring and the single 3D view; everything else on this page already applies.
Step by step: Guide: Fit a slot in Fit Lab. Source guide: tools/fit-lab/README.md.
Fit Lab works on a prepared pack export: a body GLB, fitted item GLBs and a manifest, created once by Blender.
| Pack | Commands |
|---|---|
| Bundled CC0 starter |
launchers\editor\workbench.bat cc0-starter, or python tools/starter-assets/run.py --prepare-lab then python tools/fit-lab/run.py serve --pack cc0-starter
|
| Your own mapped pack |
python <sidecar>/tools/<pack>/lab_items.py --per-slot 4, then python tools/fit-lab/run.py export --pack <pack>, then launchers\editor\fit-lab.bat <pack>
|
Open http://127.0.0.1:8774. You can set SPRITEMOTION_FIT_PACK instead of passing the pack name. Exports are
reused; --force redoes them. Data lives in workspace/ultima-online/fit-lab/<pack>/. The launcher does not
turn an arbitrary folder of raw models into a pack; see Asset Packs.

| Area | Contents |
|---|---|
| Top | Pack, save status, Undo, Redo, Save, Open Content Studio link |
| Left | Slot dropdown and its items. Tick items to show them in 3D; click one (or its preview card) to select it. Load fitted GLBs from a folder. |
| Centre | Live 3D (drag to orbit, 1 view / 2×2, UO camera resets, Hidden faces), the Blender render pane, the action dropdown, the facing ring, Play and the frame slider. Below: enlarged slot previews with their own Animate, Cycle directions, Size, Base, Poke pixels and Steady head controls. |
| Right | Tabs: Fit, Corrections, Render, Measure, History |

Each number on the ring sits where the character faces on screen under the UO camera: 0 faces the viewer, then each step turns 45° clockwise (1 down-left, 2 left, 3 up-left, 4 away, 5 up-right, 6 right, 7 down-right). The needle and the label beside the ring show the current facing. Click a number, or focus the ring and use the arrow keys to turn one step. Dashed 5–7 are mirrored from 3–1, as in the client.

1 view / 2×2 (remembered per browser) switches the 3D pane between the orbit view and four views: the orbit view plus fixed Front, Side and Back views that turn with the character, so Front always shows the face. Use it to check a garment from every side while you adjust one slider. The animation, hidden-face overlay and item visibility apply to all four.

- Slot fit: Offset X/Y/Z (cm), Rotate X/Y/Z (degrees), Scale, Front/back depth and Binding (skinned deforms with its bones; rigid follows one bone, for metal that must not stretch). This is the base for every animation and direction.
- This item only: an extra offset for the selected item.
- Left/Right X/Y/Z: separate offsets for the character's left and right side of one item (paired gloves and boots). Left/right refer to the character, not the screen; mirrored directions mirror the fitted result.
- Hide body under clothes: on/off plus outward and inward distance. Body faces this close to the item are removed from the fitting body so they cannot poke through or cut holes in the sprite.
Front/back depth changes only the rest-pose front-to-back size (Blender Y), before rotation. Use it for a shallow torso shell instead of raising Scale and widening the shoulders.


A correction is a delta on top of the slot fit, applied to a target (pack, slot, named group or one item) and optionally only for one action (0–34) and/or one stored direction (0–4). Directions 5/6/7 share the corrections of 3/2/1.
- Offsets and rotations add; scale and depth multiply.
- Resolution order: pack, slot, groups, item; within each, all poses, then action, then direction, then action+direction.
- Groups are created from the checked items in the current slot.
- Body masking per correction: inherit, clothing (limbs/head only), body (also torso) or none. Back and quiver slots default to body.
The lab and the Blender build resolve corrections with the same rules (Python and JavaScript implementations are parity-tested). Every rendered pose starts from the untouched base geometry, so corrections never accumulate.

- Build item renders the selected item with a snapshot of the saved fit at Current animation, Preview: 5 animations or All 35 animations coverage.
- Rebuild changed blocks compares with that item's last successful lab build and re-renders only the action/direction blocks your changes affect, into a new validated revision. The original job is never changed.
- The Blender render pane shows the newest finished render for the current animation, locked to the main frame and direction. Status: Matches the saved fit or Fit changed since this render. Auto re-render rebuilds once the saved fit has been still for 3 s.
- Sprite sheet shows every frame of the 5 stored directions; click a cell to jump there. Download sheet and Import package download the results.
![]()

Measure slot counts poke pixels (body pixels showing through the item) over every frame of the checked actions in 5 directions. The first run is the baseline; later runs show the change. Run A/B compares the experimental Steady head preview against the original motion on the first three items of every slot, with a downloadable report and contact sheet.
Poke counts are approximate and run high: they do not model the renderer's 1 cm holdout margin or 6 mm push-out. Compare them between runs; don't read them as absolutes. A lower count is not proof of a better match to UO art.

| Feature | Behaviour |
|---|---|
| Autosave | 800 ms after the last edit; Save saves immediately |
| Undo / redo | Ctrl+Z; Ctrl+Y or Ctrl+Shift+Z (Command on macOS). A slider drag is one step. |
| History | Jump to any of the last 100 steps. Editing after undo discards the redo branch. |
| Crash recovery | Every edit is cached in the browser, scoped to this pack and server |
| Disk backups | Saves atomically replace lab-adjustments.json and keep the previous three in lab-adjustments-backups/. Restore backup is itself an undoable edit. |
| Conflicts | If another tab or tool changed the file, saving pauses: Use disk version or Keep my recovered edits. |
Selection, playback and camera changes are not fit edits.
- Base: the original UO sprite (extracted from the model's embedded original frames), the 3D body, or transparent content only.
- Preview cards default to 2x native size with pixelated scaling (up to 4x). Animate and Cycle directions (every two seconds) work independently of the main view.
- Magenta marks body pixels poking through.
These are live camera renders, not final Blender output. The Blender render is the final word, and an in-game test is the acceptance check.
Load fitted GLBs from a folder accepts a local folder of self-contained GLBs that already carry skin weights with the canonical body's bone names. Select the destination slot first. Files are copied into an ignored import cache; imports are session-only. Limits: 100 GLBs, 50 MB each. Raw FBX and unrigged objects must go through the pack export workflow.
- A pack's mapping generator (in your sidecar) merges
lab-adjustments.jsonslot fits into the mapping's part types (offset,rotate,scale,bind,hide_body). -
tools/uo-content/blender_build.pyapplies the mapped part as the default fit and, whenhide_bodyis enabled, deletes the covered body faces before rendering. - Lab builds, and Studio jobs with
fit_item, also use item overrides and scoped corrections, snapshotted at job creation.
GUO and the standalone browser can share one Fit Lab process. Key routes: GET /api/service, GET /api/state,
POST /api/adjustments ({adjustments, base_revision}; a stale revision returns HTTP 409), GET/POST /api/build,
GET /api/renders?item=<id>, POST /api/assets. Contract:
docs/fit-lab-service.md and
Data Contracts.
SpriteMotion is MIT licensed · Repository · License · Third-party notices
Not affiliated with or endorsed by Electronic Arts or Broadsword. Ultima Online is a trademark of its owners. No game data is distributed with this project.
Start
Guides
- Fit a slot in Fit Lab
- Render an item end to end
- Clean up with masking
- Troubleshoot a failing step
- Take content into GUO
Tools
Reference
Project