diff --git a/MEMORY.md b/MEMORY.md index 95b403a..25137b2 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -132,7 +132,9 @@ canonical implemented-features list in `docs/development-status.md`. regression — three meshes for twelve roles — and the modular part library is the `0.9.10` fix. - **Roster is eleven outfits over twelve roles (2026-11-06):** hoodie, Casual, Worker, Suit, Adventurer, Beach, Casual_2, Farmer, King, Punk (Swat is intaken but unused — it faces Godot −Z and - every yaw attempt broke the skin). Per-family slot maps live in `QUATERNIUS_SOURCES`; a family + every yaw attempt broke the skin: node rotation turns the skin with it, object rotation collapses + it, and data-space rotation after `transform_apply` still loses the head). Per-family slot maps + live in `QUATERNIUS_SOURCES`; a family that cannot paint a slot (Farmer has no hair) gets `patch_slots`, which buries a 4 cm triangle so the material survives the export and the contract holds. - **FBX modular parts need five fixes before they are a base (2026-11-06):** (1) every FBX file diff --git a/TODO.md b/TODO.md index b6d0e39..dc0a664 100644 --- a/TODO.md +++ b/TODO.md @@ -781,10 +781,13 @@ regressing gear fit, budget or readability. Farmer, King, Punk, Suit) with a slot map each: the roster now runs **eleven distinct outfits across twelve roles**. `patch_slots` buries a hidden triangle for slots a family cannot paint (Farmer has no hair shell), so the five-slot contract holds on helmeted and hatted heads. -- [ ] **Swat base faces backwards.** It is authored facing Godot -Z. Yawing the exported base - rotates the skin with the armature node and renders the character headless; yawing the mesh - too collapses it. The fix is to yaw the four FBX parts before the join. `npc_guard` stays on - the worker base until then. +- [ ] **Swat base faces backwards, and three yaw attempts failed.** It is authored facing Godot + −Z. Tried and rejected: (a) rotating the exported armature node — the skin turns with it and + the character renders headless; (b) rotating armature + mesh objects — collapses into a + spike; (c) rotating mesh *data* plus the rest bones after `transform_apply` — still headless. + Next idea: yaw each of the four FBX parts in its own import (before any join), or re-author + the base from the pack's `All together/Humans_Master` scene where facing is known. + `npc_guard` stays on the worker base; `Swat.glb` is intaken but unused. - [ ] `SpaceSuit` is not intaken: it has no skin, eye or hair geometry at all and no role fits an astronaut on Hollywood Boulevard. `Weapons` (pistol, sword) is unused by design — the game has no weapon system. diff --git a/assets/models/blender_src/quaternius/Swat.glb b/assets/models/blender_src/quaternius/Swat.glb index 2d32076..01fff90 100644 Binary files a/assets/models/blender_src/quaternius/Swat.glb and b/assets/models/blender_src/quaternius/Swat.glb differ diff --git a/tools/quaternius_parts.py b/tools/quaternius_parts.py index a623e1b..22caf68 100644 --- a/tools/quaternius_parts.py +++ b/tools/quaternius_parts.py @@ -363,12 +363,19 @@ def make_base(family_dir: Path, out: Path, yaw: float = 0.0): # mesh as well double-applies the skin, and baking the rotation into an already # bound mesh collapses the character. if abs(yaw) > 0.01: - # World-space rotation: the FBX armature carries its own -90 X, so - # `rotate_axis("Z")` would turn about that local axis and lay the character - # flat instead of facing the other way. + # Rotate in *data* space, after the object transforms are baked: turning the + # exported armature node rotates with the skin and renders headless, and + # rotating the object pair on top of a bound skin collapses it. Rotating the + # vertices and the rest bones by the same matrix keeps the bind intact. from mathutils import Matrix # type: ignore - keep.matrix_world = (Matrix.Rotation(math.radians(yaw), 4, "Z") - @ keep.matrix_world) + rotation = Matrix.Rotation(math.radians(yaw), 4, "Z") + body.data.transform(rotation) + bpy.context.view_layer.objects.active = keep + bpy.ops.object.mode_set(mode="EDIT") + for bone in keep.data.edit_bones: + bone.head = rotation @ bone.head + bone.tail = rotation @ bone.tail + bpy.ops.object.mode_set(mode="OBJECT") bpy.context.view_layer.update() out.parent.mkdir(parents=True, exist_ok=True)