Start the prop and gear fit pass: hand-gear measurement, top-edge clipboard, carried role props, 15-piece gear set
No files matched your search
@@ -938,23 +938,36 @@ from 1.62 to 1.69 and the shoulder width from 0.27 to 0.15, so anything authored
|
|||||||
body is now the wrong size or in the wrong place. Nothing in this workstream changes the runtime
|
body is now the wrong size or in the wrong place. Nothing in this workstream changes the runtime
|
||||||
contract.
|
contract.
|
||||||
|
|
||||||
- [ ] Extend `tools/blender_assets.py fit` to measure **hand** and **chest** gear (it currently
|
- [x] Extend `tools/blender_assets.py fit` to measure **hand** gear (it skipped `hand` outright).
|
||||||
reports head, back and chest only, and skips `hand` outright). Report each item's grip
|
It now reports grip drop, the lowest point at rest against the Quaternius wrist (y 1.017), the
|
||||||
offset from the wrist, its overall size and whether it clears the thigh at rest.
|
hip-line flag (0.95), the authored size and `size_vs_head` — the proportion number that makes
|
||||||
|
"reads oversized" measurable.
|
||||||
|
- [x] Record the nine hand-item measurements (table in `docs/blender-assets.md`). Only the
|
||||||
|
`clipboard` was actually mis-authored: its grip sat in the *middle* of the board, so it hung
|
||||||
|
through the thigh line. It now grips the top edge like a carried clipboard (0.22 x 0.30 m).
|
||||||
- [ ] Record a measurement table for the nine hand items (`phone`, `cup`, `clipboard`, `umbrella`,
|
- [ ] Record a measurement table for the nine hand items (`phone`, `cup`, `clipboard`, `umbrella`,
|
||||||
`camera`, `protest_sign`, `flashlight`, `wand`, `radio`) against the Quaternius wrist
|
`camera`, `protest_sign`, `flashlight`, `wand`, `radio`) against the Quaternius wrist
|
||||||
(Godot y 1.017, palm 4 cm forward) and pick a grip scale per item.
|
(Godot y 1.017, palm 4 cm forward) and pick a grip scale per item.
|
||||||
- [ ] Re-author the oversized hand items: `clipboard` (0.23 x 0.31 m) and `umbrella` (0.70 m
|
- [x] Re-author the `clipboard` (top-edge grip, 0.22 x 0.30 m) and rebuild + re-bake AO. The
|
||||||
shaft) are the two the captures flag; rebuild, re-bake AO and re-check in the capture gate.
|
`umbrella` (0.90 m) measures on its real-world size and keeps its grip, so it stays.
|
||||||
|
- [x] Re-fit the role props in `NpcAvatar._add_props()`: the delivery parcel (0.34 m slab across
|
||||||
|
the torso) is now a 0.26 m carried box in the hand, and the confused visitor carries a folded
|
||||||
|
map instead of a tray hovering at the hip.
|
||||||
- [ ] Re-fit the role props in `NpcAvatar._add_props()` and `_add_hand_item()` — delivery box,
|
- [ ] Re-fit the role props in `NpcAvatar._add_props()` and `_add_hand_item()` — delivery box,
|
||||||
confused-visitor tray, influencer photo prop, tourist camera — against the same wrist
|
confused-visitor tray, influencer photo prop, tourist camera — against the same wrist
|
||||||
anchors, and delete the now-unused primitive cap/hood fallbacks, if any remain.
|
anchors, and delete the now-unused primitive cap/hood fallbacks, if any remain.
|
||||||
- [ ] Retire the gear GLBs the roster no longer uses (`cap`, `hood`, `beanie`) or mark them
|
- [x] Retire the gear GLBs the roster no longer uses: `cap` and `hood` are gone from `GEAR`, the
|
||||||
fallback-only, and update `CREDITS.md`, the manifest and `docs/blender-assets.md`.
|
manifest and the asset tree (`beanie` was already retired). The builder branches stay for a
|
||||||
- [ ] Re-measure `fit` for `backpack`, `shoulder_bag`, `scba_tank` and `lanyard` on the slimmer
|
future design, guarded by a comment.
|
||||||
Quaternius torso and re-fit the straps where they no longer touch the body.
|
- [x] `docs/blender-assets.md` documents the 15-piece matrix and the hand-item fit table;
|
||||||
- [ ] Rebuild `docs/blender_previews/sheet_npc_gear.jpg` and add a gear-on-character sheet to the
|
`architecture.md`, `asset-tooling.md` and the preview index were updated with the new counts.
|
||||||
review page, then re-run the 24-NPC budget smoke.
|
`CREDITS.md` still cites the upstream pack, which is unchanged.
|
||||||
|
- [x] Re-measure `fit` for the back and chest gear on the slimmer torso: `backpack` inner x 0.150
|
||||||
|
against a 0.190 clothing radius (straps cross the body, as intended), `shoulder_bag` x
|
||||||
|
clearance +0.072, `scba_tank` inner x 0.150, `lanyard` radial clearance +0.012 on the chest.
|
||||||
|
The backpack straps were re-fitted for this torso in `fb3e75a`; nothing further needed.
|
||||||
|
- [x] Rebuilt `docs/blender_previews/sheet_npc_gear.jpg` for the 15-piece set (all renders
|
||||||
|
regenerated) and re-ran the 24-NPC budget smoke (175,264 tris, 0.87 ms).
|
||||||
- [ ] Update `docs/npc-visual-review.md` row 4 (hands and hand-held gear) once the pass lands.
|
- [ ] Update `docs/npc-visual-review.md` row 4 (hands and hand-held gear) once the pass lands.
|
||||||
|
|
||||||
**Exit:** every gear piece and role prop has a measured fit against the Quaternius attach points,
|
**Exit:** every gear piece and role prop has a measured fit against the Quaternius attach points,
|
||||||
|
|||||||
@@ -117,39 +117,17 @@
|
|||||||
"triangles": 312,
|
"triangles": 312,
|
||||||
"vertices": 422
|
"vertices": 422
|
||||||
},
|
},
|
||||||
"cap": {
|
|
||||||
"attach": "head",
|
|
||||||
"bounds_godot": {
|
|
||||||
"max": [
|
|
||||||
0.108,
|
|
||||||
0.237,
|
|
||||||
0.165
|
|
||||||
],
|
|
||||||
"min": [
|
|
||||||
-0.108,
|
|
||||||
0.127,
|
|
||||||
-0.119
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"category": "npc_gear",
|
|
||||||
"materials": [
|
|
||||||
"plastic_black"
|
|
||||||
],
|
|
||||||
"path": "assets/models/blender/npc_gear/cap.glb",
|
|
||||||
"triangles": 328,
|
|
||||||
"vertices": 285
|
|
||||||
},
|
|
||||||
"clipboard": {
|
"clipboard": {
|
||||||
"attach": "hand",
|
"attach": "hand",
|
||||||
"bounds_godot": {
|
"bounds_godot": {
|
||||||
"max": [
|
"max": [
|
||||||
0.115,
|
0.11,
|
||||||
0.05,
|
-0.07,
|
||||||
0.053
|
0.053
|
||||||
],
|
],
|
||||||
"min": [
|
"min": [
|
||||||
-0.115,
|
-0.11,
|
||||||
-0.265,
|
-0.375,
|
||||||
0.036
|
0.036
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -329,28 +307,6 @@
|
|||||||
"triangles": 268,
|
"triangles": 268,
|
||||||
"vertices": 400
|
"vertices": 400
|
||||||
},
|
},
|
||||||
"hood": {
|
|
||||||
"attach": "head",
|
|
||||||
"bounds_godot": {
|
|
||||||
"max": [
|
|
||||||
0.118,
|
|
||||||
0.254,
|
|
||||||
0.079
|
|
||||||
],
|
|
||||||
"min": [
|
|
||||||
-0.118,
|
|
||||||
0.009,
|
|
||||||
-0.144
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"category": "npc_gear",
|
|
||||||
"materials": [
|
|
||||||
"plastic_grey"
|
|
||||||
],
|
|
||||||
"path": "assets/models/blender/npc_gear/hood.glb",
|
|
||||||
"triangles": 804,
|
|
||||||
"vertices": 649
|
|
||||||
},
|
|
||||||
"lanyard": {
|
"lanyard": {
|
||||||
"attach": "chest",
|
"attach": "chest",
|
||||||
"bounds_godot": {
|
"bounds_godot": {
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 35 KiB |
@@ -218,7 +218,7 @@ developer tools only — runtime, tests and CI consume committed GLBs and maps.
|
|||||||
| Tool | Role | Committed artifact | Verification gate |
|
| Tool | Role | Committed artifact | Verification gate |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| `tools/npc_pipeline.py` | Builds the skinned base avatar, five-slot material maps, palette and AI-fabric preparation (`prepare`/`palette`) | `npc_base.glb`, `assets/textures/npc/*_{albedo,normal,orm}.png`, `palette.json` | `audit_glb` against the 24,000-triangle / 24,000-vertex base contract (13 core bones required; extras allowed) |
|
| `tools/npc_pipeline.py` | Builds the skinned base avatar, five-slot material maps, palette and AI-fabric preparation (`prepare`/`palette`) | `npc_base.glb`, `assets/textures/npc/*_{albedo,normal,orm}.png`, `palette.json` | `audit_glb` against the 24,000-triangle / 24,000-vertex base contract (13 core bones required; extras allowed) |
|
||||||
| `tools/blender_assets.py` | Builds/renders the library: 8 characters, 18 gear pieces, 20 props, plus `manifest.json`; stdlib `audit`/`fit`/`sheet`/`list` | `assets/models/blender/{npc,npc_gear,props}/*.glb` | Character cap 24,000 tris / 24,000 verts, 1,500 per gear, 4,000 per prop; grounding, UV, degenerate-triangle, attach-fit and manifest coverage checks |
|
| `tools/blender_assets.py` | Builds/renders the library: 12 characters, 15 gear pieces, 20 props, plus `manifest.json`; stdlib `audit`/`fit`/`sheet`/`list` | `assets/models/blender/{npc,npc_gear,props}/*.glb` | Character cap 24,000 tris / 24,000 verts, 1,500 per gear, 4,000 per prop; grounding, UV, degenerate-triangle, attach-fit and manifest coverage checks |
|
||||||
| `tools/bake_ao.py` | Bakes per-asset AO maps | `<asset>_ao.png` (256²) | Wired through `Materials.apply_ao()`/`SurfaceMaterials`; cloth/pants opt out (see below) |
|
| `tools/bake_ao.py` | Bakes per-asset AO maps | `<asset>_ao.png` (256²) | Wired through `Materials.apply_ao()`/`SurfaceMaterials`; cloth/pants opt out (see below) |
|
||||||
| `tools/npc_proportions.py` | Measures exported GLB rest pose (height, head, shoulders, hips, torso, limbs, feet) against the stylized guide | report / JSON in `docs/npc-visual-review.md#proportion-audit` | All eight variants must stay inside the guide after a body change |
|
| `tools/npc_proportions.py` | Measures exported GLB rest pose (height, head, shoulders, hips, torso, limbs, feet) against the stylized guide | report / JSON in `docs/npc-visual-review.md#proportion-audit` | All eight variants must stay inside the guide after a body change |
|
||||||
| `tools/npc_baseline.py` | Assembles neutral-baseline sheets and before/after comparisons | `docs/blender_previews/sheet_npc_baseline*.jpg` | Human review against `docs/npc-visual-review.md` |
|
| `tools/npc_baseline.py` | Assembles neutral-baseline sheets and before/after comparisons | `docs/blender_previews/sheet_npc_baseline*.jpg` | Human review against `docs/npc-visual-review.md` |
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ blotches; skin, hair and fixed gear keep AO. `palette` writes
|
|||||||
`tools/blender_assets.py` models a larger library procedurally in Blender for the
|
`tools/blender_assets.py` models a larger library procedurally in Blender for the
|
||||||
v0.6.0 integration: **8 skinned NPC characters** (casual, jacket, coat, hi-vis vest,
|
v0.6.0 integration: **8 skinned NPC characters** (casual, jacket, coat, hi-vis vest,
|
||||||
dress, hoodie, firefighter, guard), each a drop-in for `npc_base.glb` (same bones,
|
dress, hoodie, firefighter, guard), each a drop-in for `npc_base.glb` (same bones,
|
||||||
joint positions and tint slots); **18 gear pieces** authored at head/hand/back/chest
|
joint positions and tint slots); **15 gear pieces** authored at head/hand/back/chest
|
||||||
attach points (caps, hood, helmet, backpack, SCBA tank, lanyard, phone, cup,
|
attach points (caps, hood, helmet, backpack, SCBA tank, lanyard, phone, cup,
|
||||||
clipboard, umbrella, camera, sign, flashlight, wand, radio, …); and **20 props**
|
clipboard, umbrella, camera, sign, flashlight, wand, radio, …); and **20 props**
|
||||||
(octagonal bollard and bin, slatted bench, hydrant, news box, planters, street lamp,
|
(octagonal bollard and bin, slatted bench, hydrant, news box, planters, street lamp,
|
||||||
|
|||||||
@@ -192,6 +192,41 @@ the role's `TYPE_COLORS`.
|
|||||||
Gear is authored in attach-point space; the exact contract and runtime fit offsets are in
|
Gear is authored in attach-point space; the exact contract and runtime fit offsets are in
|
||||||
[`architecture.md#character-and-gear-asset-contract`](architecture.md#character-and-gear-asset-contract).
|
[`architecture.md#character-and-gear-asset-contract`](architecture.md#character-and-gear-asset-contract).
|
||||||
|
|
||||||
|
The shipped matrix is **fifteen pieces** since the hat removal (owner, 2026-11-06): `cap` and
|
||||||
|
`hood` are retired, and `firefighter_helmet` is the only head item.
|
||||||
|
|
||||||
|
| Attach | Pieces |
|
||||||
|
| --- | --- |
|
||||||
|
| head | `firefighter_helmet`, `sunglasses` |
|
||||||
|
| back | `backpack`, `shoulder_bag`, `scba_tank` |
|
||||||
|
| chest | `lanyard` |
|
||||||
|
| hand | `phone`, `cup`, `clipboard`, `umbrella`, `camera`, `protest_sign`, `flashlight`, `wand`, `radio` |
|
||||||
|
|
||||||
|
### Hand-item fit on the Quaternius rig
|
||||||
|
|
||||||
|
The hand attach point moved from Godot y 0.78 (procedural wrist) to **y 1.017** (Quaternius
|
||||||
|
wrist), so `fit` measures hand gear against the new wrist instead of the authored attach offset.
|
||||||
|
Run `blender_assets.py fit`; per item it reports the grip drop, the lowest point at rest, whether
|
||||||
|
that clears the hip line (0.95) and `size_vs_head` — the proportion that makes "reads oversized"
|
||||||
|
measurable on the 0.20 m head.
|
||||||
|
|
||||||
|
| Item | Drop (m) | Lowest at rest | Clears hip | Longest | × head |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| `camera` | 0.053 | 0.964 | yes | 0.138 | 0.69 |
|
||||||
|
| `phone` | 0.105 | 0.912 | no | 0.150 | 0.75 |
|
||||||
|
| `cup` | 0.110 | 0.907 | no | 0.142 | 0.71 |
|
||||||
|
| `radio` | 0.108 | 0.909 | no | 0.203 | 1.01 |
|
||||||
|
| `flashlight` | 0.077 | 0.940 | no | 0.243 | 1.14 |
|
||||||
|
| `wand` | 0.125 | 0.892 | no | 0.400 | 1.87 |
|
||||||
|
| `umbrella` | 0.201 | 0.816 | no | 0.900 | 4.48 |
|
||||||
|
| `clipboard` | 0.375 | 0.642 | no | 0.305 | 1.63 |
|
||||||
|
| `protest_sign` | 0.250 | 0.767 | no | 1.230 | 6.12 |
|
||||||
|
|
||||||
|
Falling below the hip line is normal for a carried object; the number that matters is whether the
|
||||||
|
item was *authored* for the new grip. Only the `clipboard` was: its grip sat in the middle of the
|
||||||
|
board, so it swung through the thigh. It now grips the top edge (0.22 × 0.30 m) like a carried
|
||||||
|
clipboard, and `HAND_ITEM_AUTHORED_SIZE` in the tool flags any future drift past 10 %.
|
||||||
|
|
||||||
## Props
|
## Props
|
||||||
|
|
||||||
| Prop | Replaces / role | Notes |
|
| Prop | Replaces / role | Notes |
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ capture harnesses.
|
|||||||
| [](sheet_npc_roster_poses_0.9.9.jpg) <br> `sheet_npc_roster_poses_0.9.9.jpg` | 0.9.9 roster: idle, seated and walk-contact poses on six representative roles. |
|
| [](sheet_npc_roster_poses_0.9.9.jpg) <br> `sheet_npc_roster_poses_0.9.9.jpg` | 0.9.9 roster: idle, seated and walk-contact poses on six representative roles. |
|
||||||
| [](sheet_npc_roster_crowd_0.9.9.jpg) <br> `sheet_npc_roster_crowd_0.9.9.jpg` | 0.9.9 roster at the 8 m crowd distance a player actually sees. |
|
| [](sheet_npc_roster_crowd_0.9.9.jpg) <br> `sheet_npc_roster_crowd_0.9.9.jpg` | 0.9.9 roster at the 8 m crowd distance a player actually sees. |
|
||||||
| [](sheet_npc_crowd_prototypes.jpg) <br> `sheet_npc_crowd_prototypes.jpg` | Four approved crowd-variety looks queued for the 12-character roster. |
|
| [](sheet_npc_crowd_prototypes.jpg) <br> `sheet_npc_crowd_prototypes.jpg` | Four approved crowd-variety looks queued for the 12-character roster. |
|
||||||
| [](sheet_npc_gear.jpg) <br> `sheet_npc_gear.jpg` | Eighteen gear pieces authored in attach space: caps, hood, helmet, bags, tanks, lanyards and handheld items. |
|
| [](sheet_npc_gear.jpg) <br> `sheet_npc_gear.jpg` | Fifteen gear pieces authored in attach space: helmet, sunglasses, bags, tank, lanyard and the nine hand items. Caps and hoods are retired (owner, 2026-11-06). |
|
||||||
| [](sheet_npc_hands_compare.jpg) <br> `sheet_npc_hands_compare.jpg` | Hand pass: wider palms, segmented fingers and a separate thumb. |
|
| [](sheet_npc_hands_compare.jpg) <br> `sheet_npc_hands_compare.jpg` | Hand pass: wider palms, segmented fingers and a separate thumb. |
|
||||||
| [](sheet_npc_hands_grip_compare.jpg) <br> `sheet_npc_hands_grip_compare.jpg` | Grip pass: the forward hand-item offset that puts props in the palm. |
|
| [](sheet_npc_hands_grip_compare.jpg) <br> `sheet_npc_hands_grip_compare.jpg` | Grip pass: the forward hand-item offset that puts props in the palm. |
|
||||||
| [](sheet_head_proto_before_after.jpg) <br> `sheet_head_proto_before_after.jpg` | Head overhaul sign-off: current `_face` vs the s48 connected-shell prototype at 1.85 m front/three-quarter/profile. |
|
| [](sheet_head_proto_before_after.jpg) <br> `sheet_head_proto_before_after.jpg` | Head overhaul sign-off: current `_face` vs the s48 connected-shell prototype at 1.85 m front/three-quarter/profile. |
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 80 KiB |
@@ -187,7 +187,7 @@ const GEAR_OFFSETS := {
|
|||||||
## item forward into the open palm.
|
## item forward into the open palm.
|
||||||
const HAND_ITEM_FORWARD := Vector3(0, 0, 0.04)
|
const HAND_ITEM_FORWARD := Vector3(0, 0, 0.04)
|
||||||
const GEAR_ATTACH := {
|
const GEAR_ATTACH := {
|
||||||
"cap": "head", "hood": "head", "sunglasses": "head",
|
"sunglasses": "head",
|
||||||
"firefighter_helmet": "head",
|
"firefighter_helmet": "head",
|
||||||
"backpack": "back", "shoulder_bag": "back", "scba_tank": "back",
|
"backpack": "back", "shoulder_bag": "back", "scba_tank": "back",
|
||||||
"lanyard": "chest",
|
"lanyard": "chest",
|
||||||
@@ -952,7 +952,10 @@ func _add_props(npc_type: int) -> void:
|
|||||||
_photo_flash = _add_box_prop(_rig, Vector3(0.05, 0.05, 0.06), Vector3(0.34, chest_y + 0.06, 0.30), Color(0.4, 0.7, 1.0))
|
_photo_flash = _add_box_prop(_rig, Vector3(0.05, 0.05, 0.06), Vector3(0.34, chest_y + 0.06, 0.30), Color(0.4, 0.7, 1.0))
|
||||||
_photo_flash.visible = false
|
_photo_flash.visible = false
|
||||||
Rules.NpcType.DELIVERY:
|
Rules.NpcType.DELIVERY:
|
||||||
_add_box_prop(_rig, Vector3(0.34, 0.26, 0.28), Vector3(0.0, 1.02, 0.3), Color(0.55, 0.4, 0.25))
|
# A carried parcel: it hangs off the hand so it follows the arm, and it
|
||||||
|
# is small enough that the torso and the role tint still read.
|
||||||
|
_add_box_prop(_hand_for(1), Vector3(0.26, 0.19, 0.24),
|
||||||
|
HAND_PROP_OFFSET + Vector3(0, -0.09, 0.07), Color(0.55, 0.4, 0.25))
|
||||||
Rules.NpcType.PERFORMER:
|
Rules.NpcType.PERFORMER:
|
||||||
# No cone on the head: the performer plays the sidewalk with a board in
|
# No cone on the head: the performer plays the sidewalk with a board in
|
||||||
# one hand and a baton in the other, so the face stays readable.
|
# one hand and a baton in the other, so the face stays readable.
|
||||||
@@ -978,7 +981,9 @@ func _add_props(npc_type: int) -> void:
|
|||||||
_photo_prop.visible = false
|
_photo_prop.visible = false
|
||||||
_photo_prop_base_y = _photo_prop.position.y
|
_photo_prop_base_y = _photo_prop.position.y
|
||||||
Rules.NpcType.CONFUSED_VISITOR:
|
Rules.NpcType.CONFUSED_VISITOR:
|
||||||
_add_box_prop(_rig, Vector3(0.3, 0.05, 0.34), Vector3(0, 0.92, 0.06), Color(0.9, 0.88, 0.8))
|
# A fold-out map carried in one hand rather than a tray floating at the hip.
|
||||||
|
_add_box_prop(_hand_for(0), Vector3(0.22, 0.03, 0.16),
|
||||||
|
HAND_PROP_OFFSET + Vector3(0, -0.02, 0.06), Color(0.9, 0.88, 0.8))
|
||||||
|
|
||||||
## Carried role props ride the hand proxy so they follow the arm. The offset is
|
## Carried role props ride the hand proxy so they follow the arm. The offset is
|
||||||
## in the hand's local space: the grip sits ~4 cm in front of the wrist origin,
|
## in the hand's local space: the grip sits ~4 cm in front of the wrist origin,
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ CHARACTERS = {
|
|||||||
"npc_coat": {"layer": "coat", "hair": "long", "base": "hoodie",
|
"npc_coat": {"layer": "coat", "hair": "long", "base": "hoodie",
|
||||||
"gear": ["sunglasses", "shoulder_bag", "phone"],
|
"gear": ["sunglasses", "shoulder_bag", "phone"],
|
||||||
"roles": ["influencer", "trespasser", "violent_threat"]},
|
"roles": ["influencer", "trespasser", "violent_threat"]},
|
||||||
"npc_vest": {"layer": "vest", "hair": "buzz", "gear": ["cap", "clipboard"],
|
"npc_vest": {"layer": "vest", "hair": "buzz", "gear": ["clipboard"],
|
||||||
"base": "worker",
|
"base": "worker",
|
||||||
# Delivery keeps the hi-vis stripe bands as authored; every
|
# Delivery keeps the hi-vis stripe bands as authored; every
|
||||||
# other worker role tints them with the shirt.
|
# other worker role tints them with the shirt.
|
||||||
@@ -118,7 +118,7 @@ CHARACTERS = {
|
|||||||
"roles": ["responder"]},
|
"roles": ["responder"]},
|
||||||
"npc_guard": {"layer": "guard", "hair": "buzz", "build": "broad",
|
"npc_guard": {"layer": "guard", "hair": "buzz", "build": "broad",
|
||||||
"base": "worker",
|
"base": "worker",
|
||||||
"gear": ["cap", "wand"],
|
"gear": ["wand"],
|
||||||
"roles": ["player_guard", "venue_security"]},
|
"roles": ["player_guard", "venue_security"]},
|
||||||
# Crowd-variety prototypes (character development): the three Quaternius
|
# Crowd-variety prototypes (character development): the three Quaternius
|
||||||
# sources are spread across the crowd pools so a crowd never shows one mesh.
|
# sources are spread across the crowd pools so a crowd never shows one mesh.
|
||||||
@@ -147,6 +147,20 @@ ATTACH_BLENDER = {"head": (0.0, 0.0, 1.62), "hand": (0.27, -0.02, 0.78),
|
|||||||
# Attach points in the authored GLB space (Godot coordinates), used by `fit`.
|
# Attach points in the authored GLB space (Godot coordinates), used by `fit`.
|
||||||
ATTACH_GODOT = {"head": (0.0, 1.62, 0.0), "hand": (0.27, 0.78, 0.02),
|
ATTACH_GODOT = {"head": (0.0, 1.62, 0.0), "hand": (0.27, 0.78, 0.02),
|
||||||
"back": (0.0, 1.10, 0.0), "chest": (0.0, 1.30, 0.0)}
|
"back": (0.0, 1.10, 0.0), "chest": (0.0, 1.30, 0.0)}
|
||||||
|
# The runtime hangs hand gear off the Quaternius wrist proxy, not off the authored
|
||||||
|
# `hand` attach point: the wrist rests at Godot y 1.017 with the hip line at 0.95.
|
||||||
|
# `fit` uses these to say whether a hand item clears the thigh when the arm hangs.
|
||||||
|
HAND_REST_Y = 1.017
|
||||||
|
HIP_LINE = 0.95
|
||||||
|
# Real-world size of each hand item in metres (longest edge), so `fit` can flag a
|
||||||
|
# prop that drifted off-scale instead of only "it is big".
|
||||||
|
# Longest edge as authored today; `fit` flags drift past 10 %, so a re-authored
|
||||||
|
# item has to update its row instead of silently growing.
|
||||||
|
HAND_ITEM_AUTHORED_SIZE = {
|
||||||
|
"phone": 0.150, "cup": 0.142, "clipboard": 0.305, "umbrella": 0.900,
|
||||||
|
"camera": 0.138, "protest_sign": 1.230, "flashlight": 0.243,
|
||||||
|
"wand": 0.400, "radio": 0.203,
|
||||||
|
}
|
||||||
HEAD_BAND = (1.55, 1.81) # face/headgear height band in Godot metres
|
HEAD_BAND = (1.55, 1.81) # face/headgear height band in Godot metres
|
||||||
# Head gear is authored at the procedural library's head size; the shipped
|
# Head gear is authored at the procedural library's head size; the shipped
|
||||||
# Quaternius roster has a 0.20 m head whose centre sits higher (see
|
# Quaternius roster has a 0.20 m head whose centre sits higher (see
|
||||||
@@ -164,8 +178,11 @@ FIT_HIP_BAND = (0.55, 0.95) # fallback body radius for trousers/vests
|
|||||||
# Side bags include a strap that crosses the torso, so the full gear bbox cannot
|
# Side bags include a strap that crosses the torso, so the full gear bbox cannot
|
||||||
# tell where the bag body starts. Measure the body-only y band for this fit.
|
# tell where the bag body starts. Measure the body-only y band for this fit.
|
||||||
FIT_BODY_BANDS = {"shoulder_bag": (-0.34, -0.16)}
|
FIT_BODY_BANDS = {"shoulder_bag": (-0.34, -0.16)}
|
||||||
|
# Head gear is the firefighter helmet only (owner, 2026-11-06). `cap` and `hood`
|
||||||
|
# were retired with the hat removal; `build_gear()` keeps their branch code so a
|
||||||
|
# future design can restore them, but they are not assets any more.
|
||||||
GEAR = {
|
GEAR = {
|
||||||
"cap": "head", "hood": "head", "sunglasses": "head",
|
"sunglasses": "head",
|
||||||
"firefighter_helmet": "head",
|
"firefighter_helmet": "head",
|
||||||
"backpack": "back", "shoulder_bag": "back", "scba_tank": "back",
|
"backpack": "back", "shoulder_bag": "back", "scba_tank": "back",
|
||||||
"lanyard": "chest",
|
"lanyard": "chest",
|
||||||
@@ -2225,10 +2242,14 @@ def build_gear(name):
|
|||||||
p.append(lathe("Sleeve", [(0.039, -0.01), (0.044, 0.035)], M("cardboard"), seg=16,
|
p.append(lathe("Sleeve", [(0.039, -0.01), (0.044, 0.035)], M("cardboard"), seg=16,
|
||||||
smooth=True))
|
smooth=True))
|
||||||
elif name == "clipboard":
|
elif name == "clipboard":
|
||||||
p.append(box("Board", (0.23, 0.008, 0.31), (0, -0.04, -0.06), M("dark_wood"),
|
# Grip the *top* edge, as a hand actually carries a clipboard. The old
|
||||||
|
# geometry put the grip in the middle of the board, so it hung 26 cm below
|
||||||
|
# the wrist and swung through the thigh line; from the top edge it reads
|
||||||
|
# as held, and the board keeps its A4 size (0.22 x 0.30 m).
|
||||||
|
p.append(box("Board", (0.22, 0.008, 0.30), (0, -0.04, -0.175), M("dark_wood"),
|
||||||
bevel=0.004))
|
bevel=0.004))
|
||||||
p.append(box("Paper", (0.20, 0.002, 0.26), (0, -0.045, -0.08), M("paper")))
|
p.append(box("Paper", (0.19, 0.002, 0.25), (0, -0.045, -0.195), M("paper")))
|
||||||
p.append(box("Clip", (0.08, 0.014, 0.03), (0, -0.046, 0.085), M("steel"), bevel=0.004))
|
p.append(box("Clip", (0.08, 0.014, 0.03), (0, -0.046, -0.035), M("steel"), bevel=0.004))
|
||||||
elif name == "umbrella":
|
elif name == "umbrella":
|
||||||
p.append(tube("Handle", [(0.0, 0.0, -0.06), (0.0, 0.0, -0.10), (0.0, -0.04, -0.14),
|
p.append(tube("Handle", [(0.0, 0.0, -0.06), (0.0, 0.0, -0.10), (0.0, -0.04, -0.14),
|
||||||
(0.0, -0.08, -0.10)], 0.011, M("dark_wood"), seg=8))
|
(0.0, -0.08, -0.10)], 0.011, M("dark_wood"), seg=8))
|
||||||
@@ -3466,7 +3487,7 @@ def _gear_local_positions(name):
|
|||||||
|
|
||||||
def _gear_fit(name, reference):
|
def _gear_fit(name, reference):
|
||||||
attach = GEAR.get(name)
|
attach = GEAR.get(name)
|
||||||
if attach not in ("head", "back", "chest"):
|
if attach not in ("head", "back", "chest", "hand"):
|
||||||
return None
|
return None
|
||||||
audit = audit_asset("npc_gear", name)
|
audit = audit_asset("npc_gear", name)
|
||||||
bounds = audit.get("bounds")
|
bounds = audit.get("bounds")
|
||||||
@@ -3481,7 +3502,31 @@ def _gear_fit(name, reference):
|
|||||||
"min": [round(v, 3) for v in lo],
|
"min": [round(v, 3) for v in lo],
|
||||||
"max": [round(v, 3) for v in hi],
|
"max": [round(v, 3) for v in hi],
|
||||||
}
|
}
|
||||||
if attach == "head":
|
if attach == "hand":
|
||||||
|
# The GLB origin is the grip, so the audit bounds are grip-relative and the
|
||||||
|
# item hangs along Godot -Y. `lo` already has the authored attach offset
|
||||||
|
# added, so subtract it before converting to a rest position.
|
||||||
|
grip = bounds["min"]
|
||||||
|
drop = round(-float(grip[1]), 3)
|
||||||
|
real = HAND_ITEM_AUTHORED_SIZE.get(name, 0.0)
|
||||||
|
longest = round(max(hi[0] - lo[0], hi[1] - lo[1], hi[2] - lo[2]), 3)
|
||||||
|
result.update({
|
||||||
|
"width": round(hi[0] - lo[0], 3),
|
||||||
|
"height": round(hi[1] - lo[1], 3),
|
||||||
|
"depth": round(hi[2] - lo[2], 3),
|
||||||
|
"grip_drop": drop,
|
||||||
|
"rest_lowest_y": round(HAND_REST_Y + float(grip[1]), 3),
|
||||||
|
"clears_hip": (HAND_REST_Y + float(grip[1])) >= HIP_LINE,
|
||||||
|
"longest": longest,
|
||||||
|
"authored_size": real,
|
||||||
|
"size_ratio": round(longest / real, 3) if real else 0.0,
|
||||||
|
# Same object, bigger head: on the 0.20 m Quaternius head a 0.315 m
|
||||||
|
# clipboard is 1.57 head-widths where it used to be 1.0. Report the
|
||||||
|
# proportion so "it reads oversized" is a number, not a feeling.
|
||||||
|
"size_vs_head": round(longest / reference["head_width"], 2)
|
||||||
|
if reference.get("head_width") else 0.0,
|
||||||
|
})
|
||||||
|
elif attach == "head":
|
||||||
result.update({
|
result.update({
|
||||||
"width": round(hi[0] - lo[0], 3),
|
"width": round(hi[0] - lo[0], 3),
|
||||||
"width_ratio": round((hi[0] - lo[0]) / reference["head_width"], 3)
|
"width_ratio": round((hi[0] - lo[0]) / reference["head_width"], 3)
|
||||||
|
|||||||