Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .claude/skills/uo-fit-lab/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@ live in the local sidecar (`SPRITEMOTION_SIDECAR`, default `../SpriteMotion-Side
`python tools/fit-lab/run.py export --pack <pack>` and `python tools/fit-lab/run.py serve --pack <pack>`.
2. In the lab, change one slot at a time. Run **Measure slot** before and after: keep a change only when the slot
total of poke pixels drops without the contact sheet looking worse. Check several actions and directions.
3. Adjustments autosave; confirm **Saved to disk** (or click **Save adjustments**). Undo/redo and the History list
recover earlier edits; **Last three saved versions** restores disk backups as undoable edits. Larger previews can
3. Adjustments autosave; confirm **Saved to disk** (or click **Save**). Undo/redo and the History list
recover earlier edits; **Saved versions** (History tab) restores disk backups as undoable edits. Larger previews can
animate and cycle directions independently of the main viewport; pause them to compare the exact same pose.
Regenerate the mapping (`make_mapping.py` merges `lab-adjustments.json`), then rebuild a
preview job and compare its contact sheet. Report numbers, not impressions.
4. Check the **Blender render** pane beside the 3D view: it shows the real renderer output frame-locked to the lab
(Frame or Sprite sheet). **Render animation** (or **Auto re-render**) renders the current animation in the
background; keep a fit only when the render reads *Matches the saved fit* and looks right.
7 changes: 5 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ Open work and where to start: `docs/handoff.md`.

## Layout

The audited CC0 starter models in `examples/cc0-starter` may be committed with their original
license and provenance hashes. This exception does not cover game-derived or commercial assets.

- `common/` is the shared Python package (imports as `spritemotion`); `schemas/` holds every JSON data contract.
Extend a schema (and `docs/`) before emitting a new field.
- `games/<game>/`: adapter, profiles, annotations, equipment slots (`equipment/layers.json`), outfit lab.
Expand All @@ -38,8 +41,8 @@ headless Blender run, not only a syntax check.

## Key facts

- Canonical body: UO_Model3D v13 (`workspace/ultima-online/canonical-model/model/UO_Body_0x190.blend`), 108 bones,
rig `UO_Rig`, direction = driver `-d*pi/4` on the rig's Z rotation, UO frame i = scene frame 1 + 3i.
- Canonical body: UO_Model3D v13 (`workspace/ultima-online/canonical-model/model/UO_Body_0x190.blend`), 112 bones
(the 2026-10 update added `weapon1h.R`, `axe2h.L`, `bow.L`, `polearm.L` to v13's 108), rig `UO_Rig`, direction = driver `-d*pi/4` on the rig's Z rotation, UO frame i = scene frame 1 + 3i.
- In item renders the body is a holdout: body poking through clothing cuts holes in the item sprite. Asset-pack
parts can hide body faces under them (`hide_body` in the mapping, tuned in `tools/fit-lab`).
- Asset packs are mapped 1:1 per pack: `docs/asset-packs.md`.
17 changes: 14 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,16 @@ On other platforms, create and activate a Python environment, run
`python -m pip install -e ".[test]"`, then use the same Python setup command and
`python tools/uo-content/studio.py`.

## Try the CC0 starter equipment

[Bundled CC0 examples](examples/cc0-starter/README.md) cover all 25 UO layer routes,
with animated wearables, jewelry source/paperdoll examples and mount guidance.
After the Python, Blender and local model setup above, run
`launchers/editor/workbench.bat cc0-starter` on Windows or
`sh launchers/editor/workbench.sh cc0-starter` on Linux. The workbench prepares the
starter lab export when needed and starts Studio and Fit Lab.
See [all launchers](launchers/editor/README.md).

## Open Fit Lab

Fit Lab needs a prepared asset-pack mapping, item list and Blender export.
Expand All @@ -85,16 +95,17 @@ The instructions above describe the public `main` branch. More recent fitting
and rendering changes may be on development branches or in
[open pull requests](https://github.com/DatMoshu/SpriteMotion/pulls).

Scoped animation/direction corrections and updated masking are being integrated
through [the fitting improvements PR](https://github.com/DatMoshu/SpriteMotion/pull/1).
Fit Lab supports scoped animation/direction corrections, Blender render review,
body holdouts, front/back depth and independent left/right item offsets.
A shared Fit Lab inside [GUO](https://github.com/DatMoshu/GodotUO) has been tested
locally; its distribution and fresh-install onboarding are still in development.
Do not assume a SpriteMotion checkout installs the GUO integration.

## What you download

**Included:** Python and Blender tools, the web editors, game adapters, schemas,
annotation data, agent instructions, and a redistributable procedural sample.
annotation data, agent instructions, a redistributable procedural sample and
audited CC0 starter equipment with licenses and provenance hashes.

**Not included:** UO client files, extracted sprites, the canonical body scene,
commercial asset packs, private mappings, or generated game content. Work files
Expand Down
17 changes: 17 additions & 0 deletions docs/asset-packs.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ repository data; only put one there if its pack's licence allows it.
| `target` | Target-rig bone that receives this bone's vertex weights |
| `end` | Present on **aligned** bones: the source bone at the far end of the chain (`null`: use `fit.missing_end_offset`) |
| `keep_orientation` | Aligned bone that is only moved, not rotated or scaled (spine, head: FBX bone roll is unrelated to anatomy) |
| `target_end` | Optional target bone whose head ends the aligned chain. Use a knuckle for a wrist-to-knuckle source chain, rather than a hand bone that extends to the fingertip. |
| `end_from_parent` | With `end: null`, extrapolate the previous joint segment for a terminal finger. Imported FBX bone tails may point across the anatomy and must not be used as finger tips. |
| `follows` | For a non-aligned bone: the aligned ancestor it moves with |
| `proposed_target` | A better target the rig offers that the fitter does not use yet (twist bones, fingers, the shield bone) |

Expand All @@ -42,6 +44,10 @@ its length, clamped to `fit.scale_clamp`. Every vertex is moved by the weighted
its weights are folded into the target bones. A bone that is not aligned (twist, finger, socket, a part's own cloth or
hair bones) uses its nearest aligned ancestor (`fit.unmapped: nearest_mapped_ancestor`).

To animate fingers, explicitly align each finger segment with its target and next source joint. Give terminal
segments `end: null, end_from_parent: true`. `proposed_target` remains informational; it never silently changes
existing bindings. Paired rigid pieces retain one anchor **per mesh**, both in the lab and Blender.

`dynamic_chains` lists cloth and hair bones that ship inside part files rather than the base skeleton (matched by name
prefix), with the socket they hang from and, where the rig has one, a better target such as the rig's cloak or skirt
chains. `attach_points` lists the pack's sockets and the layers they serve.
Expand All @@ -59,7 +65,9 @@ One entry per part **type** (a helmet type, a left-hand type, and so on):
| `studio_part` | Fit template in the studio (`helm`, `chest`, `arms`, `gloves`, `legs`, `boots`, `robe`, `cloak`, `skirt`, `weapon`, `shield`, `bow`, `quiver`) |
| `bind` | `skinned` (deforms with its bones) or `rigid` (moves with one bone; metal must not stretch) |
| `offset` | Rest-pose offset in metres, applied before binding (for example helmet crown clearance) |
| `surface_clearance` | Optional rest-pose clearance in metres for close-fitting shells. Vertices within 5 cm of the body that penetrate its surface are projected outward before binding. Does not replace joint alignment or pose-time collision checks. |
| `rotate`, `scale` | Rest-pose rotation (degrees, XYZ about the item centre) and uniform scale |
| `depth_scale` | Front-to-back multiplier on Blender Y about the item centre, before rotation. Defaults to 1; changes depth without changing width or height. Saved slot values replace the mapping default; scoped corrections multiply it. |
| `hide_body` | `{enabled, outward, inward}`: CC4-style hiding of body faces under the part (metres along each face normal, rest pose), so the body cannot poke through or hold out holes in the sprite |
| `weighted_bones` | Bones the pack's parts of this type are weighted to (sampled) |

Expand All @@ -77,3 +85,12 @@ Tune `offset`, `rotate`, `scale`, `bind` and `hide_body` per slot in the [fit la
(an empty list means valid).
4. Build a preview job with `source_files`, `palette` (if the pack uses a palette texture) and `pack_mapping` in its
settings, and review the fit on the contact sheet before running full builds.

### Independent left/right item offsets

The Fit tab provides Left/Right X, Y and Z offsets for an individual item, useful for paired gloves and boots.
These add to the shared fit in Blender XYZ metres before animation. Left/right refer to the character,
not the screen; mirrored UO directions mirror the fitted result. Target-rig bone weights select each side,
including meshes containing both limbs. Neutral bones are unchanged. Each side can be reset separately;
autosave, backups and undo/redo include these offsets. Existing saves default to zero.
The adjustment document stores these as `items.<id>.sides.left` and `.right` three-number vectors.
43 changes: 43 additions & 0 deletions docs/body-occlusion.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Body occlusion in final equipment renders

The complete posed body hides equipment behind it without appearing in the exported equipment image.
`tools/uo-content/occlusion.py` supplies the final Blender depth holdout through `fit_runtime.py`.
It does not modify the separately installed upstream renderer.

The old clothing policy excluded the torso and globally exempted every anatomical part worn by any item.
It also discarded left/right suffixes. Paired bracers could consequently show through a torso or the opposite arm.

## Geometry and contact

The renderer now keeps a pristine body proxy with the body's animation modifiers. It is never rendered as color.
The proxy remains complete when the fitting mesh has faces removed by Hide body under clothes. Push-out continues
to use the fitting mesh and its existing collision regions; changing visibility does not change that solver.

For every pixel, compare nearest body depth with nearest item depth. Body closer by more than the normal holdout
margin removes the item pixel. Clothing mode can extend that margin to the saved hide-body inward distance only
when the nearest body and item surfaces belong to the same anatomical region. Left and right remain distinct;
twist groups normalize to their limb while preserving the side. This is a finite penetration tolerance, not an
unlimited exemption. A far-side surface on the same limb still disappears when it exceeds the tolerance.

Body mode has no garment-specific allowance. None bypasses body masking. The upstream original-sprite outline
correction and mounted horse masking remain in place. The final mask follows the original body's silhouette;
the model supplies front/back depth, so inaccurate source geometry can still require fit corrections.

## Recovery and validation

No saved adjustment format changes. New renders use the new policy. Renderer fingerprints include the occlusion
module, so old builds cannot be selectively mixed with new masking; create a fresh build. Old jobs remain available.
The lab's live geometry preview is still approximate and is not changed by this renderer patch.

Regression coverage includes torso blocking, opposite limbs, small own-surface penetration, deep own-surface
occlusion, a rear garment trying to exempt a different front item, and empty depth pixels. Actual Blender acceptance
uses before/after forearm renders and regression renders of other equipment. Local evidence stays in ignored workspace.

Acceptance run on 2026-10-02: the reported forearm item rendered all 35 actions and five stored directions
(175 blocks, 1,050 frames), with a passing independent VD alpha/anchor round trip and no clipped or empty frames.
Chest, leg and back-item checks each covered actions 0/4/9/22/25 in all five stored directions (125 frames each),
with the same file-validation result. Representative composites were visually inspected; this is not an in-game
equip test or exhaustive visual approval of every frame. No cloak item was present in the sampled catalog.
The screenshot's action 0, direction 3, frame index 1 now hides the far bracer behind the torso and keeps the near bracer.
Evidence: `workspace/forearm-occlusion-detail.png`, `workspace/forearm-occlusion-comparison.png`,
`workspace/forearm-full-job.txt`, `workspace/occlusion-regression.json` and `workspace/occlusion-regression.png`.
29 changes: 29 additions & 0 deletions docs/fit-lab-service.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Fit Lab editor connection, version 1

GUO and the standalone browser connect to one loopback Fit Lab process. Editors first GET `/api/service` and
require `schema: spritemotion.fit-lab-service`, `schema_version: 1` and the configured `pack`. A mismatch is an
error, not permission to start another server on that port. Service discovery contains no machine paths.

| Route | Contract |
|---|---|
| GET `/api/service` | `schemas/fit-lab-service.schema.json`; capability and pack negotiation |
| GET `/data/manifest.json` | Available item IDs, slots, actions, GLB paths and camera |
| GET `/api/mapping` | Local pack defaults; never publish this response |
| GET `/api/state` | Adjustment document, opaque revision and saved backups |
| POST `/api/adjustments` | `{adjustments, base_revision}`; 409 preserves a concurrent editor's changes |
| GET `/api/backups/<id>` | A prior document; restoring it is another revision-checked save |
| GET, POST `/api/build` | Build state / `schemas/fit-lab-build.schema.json` request |
| GET `/api/renders?item=<id>` | Completed jobs, newest first, with action coverage |
| GET `/builds/<job>/review/manifest.json` | Final sprite sequences, frame counts, playback FPS and anchor |

Start with `python tools/fit-lab/run.py serve --pack <pack> --port <port> --no-browser`. The configured checkout
and Python executable belong in the consuming editor's local settings. Pack exports must already exist. Closing
an editor view detaches from the process; it must not kill shared builds or start duplicate workers after reload.

Native controls and embedded web controls use these same saved documents. GUI undo is distinct from undoing a
published game asset. Preserve unknown adjustment fields, stable item IDs, and all unrelated scoped corrections.
Do not treat a live 3D preview as a final sprite export. Native consumers must preserve nearest-neighbour pixels,
alpha, anchors, action/direction identities and the explicit mirror mapping in the review artifact.

Version 1 does not provide a GUO staging importer, cross-process worker locking, or a native 3D viewport contract.
These remain integration work; service discovery alone does not mean an editor integration is complete.
Loading
Loading