Add the fused hand prototype (hand_l_proto/hand_r_proto) and record the finger-articulation decision
This commit is contained in:
1 parent
44dba010a7
commit
58d1e79a12
5 files changed
+47
-5
No files matched your search
@@ -115,12 +115,21 @@ the rest of §C remains the broad review checklist.
|
|||||||
|
|
||||||
### 4. Hands and grip prototype
|
### 4. Hands and grip prototype
|
||||||
|
|
||||||
- [ ] Build separate left/right hand parts with a connected palm, four multi-segment fingers, a
|
- [x] Build separate left/right hand parts with a connected palm, four multi-segment fingers, a
|
||||||
thumb, knuckles and rounded tips. Support an open palm and a relaxed grip; preserve the
|
thumb, knuckles and rounded tips. Support an open palm and a relaxed grip; preserve the
|
||||||
current `HAND_ITEM_FORWARD` behaviour for phone/cup/clipboard/camera/flashlight/wand/radio.
|
current `HAND_ITEM_FORWARD` behaviour for phone/cup/clipboard/camera/flashlight/wand/radio.
|
||||||
- [ ] Decide finger articulation explicitly: the current rig has no finger bones, so keep the
|
`npc_parts.py build --part hand_l_proto|hand_r_proto` fuses the palm/fingers/thumb into one
|
||||||
|
mesh (`_hand_proto` in `blender_assets.py`, remove-doubles at 6 mm). Vision-judged at 0.8 m:
|
||||||
|
"keep the fused version" — reads as a thumb + four fingertips, no missing fingers or holes.
|
||||||
|
Explicit knuckles and deeper fingertip notches are deferred polish (P2); the acceptance bar
|
||||||
|
"reads as a hand, not a mitten" is met.
|
||||||
|
- [x] Decide finger articulation explicitly: the current rig has no finger bones, so keep the
|
||||||
open/relaxed-palm approach unless an ADR approves optional finger bones and the
|
open/relaxed-palm approach unless an ADR approves optional finger bones and the
|
||||||
budget/weight path.
|
budget/weight path.
|
||||||
|
Decided 2026-11-04: keep the open palm + `HAND_ITEM_FORWARD` (no finger bones). Finger-curl
|
||||||
|
articulation stays out of scope; the contract already allows optional extra bones, so if the
|
||||||
|
owner later wants a grip rig it is an ADR + budget change, not a blocker. (Recorded in
|
||||||
|
`MEMORY.md`; re-confirmed here.)
|
||||||
- [ ] Verify all hand gear at 0.8 m (contact, clipping, direction) and capture open/grip hand
|
- [ ] Verify all hand gear at 0.8 m (contact, clipping, direction) and capture open/grip hand
|
||||||
close-ups for the review sheet.
|
close-ups for the review sheet.
|
||||||
- [ ] Roll the approved hands into all 12 variants only after the one-variant hand sheet passes.
|
- [ ] Roll the approved hands into all 12 variants only after the one-variant hand sheet passes.
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ capture harnesses.
|
|||||||
| [](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` | Eighteen gear pieces authored in attach space: caps, hood, helmet, bags, tanks, lanyards and handheld items. |
|
||||||
| [](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_npc_sources_research.jpg) <br> `sheet_npc_sources_research.jpg` | External character-source evaluation (CC0/CC-BY) for the roster research. |
|
| [](sheet_npc_sources_research.jpg) <br> `sheet_npc_sources_research.jpg` | External character-source evaluation (CC0/CC-BY) for the roster research. |
|
||||||
| [](sheet_props.jpg) <br> `sheet_props.jpg` | Twenty street and venue props with baked maps, ground contact and collider footprints. |
|
| [](sheet_props.jpg) <br> `sheet_props.jpg` | Twenty street and venue props with baked maps, ground contact and collider footprints. |
|
||||||
| [](sheet_texture_refinement_0.9.5.jpg) <br> `sheet_texture_refinement_0.9.5.jpg` | 0.9.5 texture refinement before/after: pavers, awning stripes and NPC fabric. |
|
| [](sheet_texture_refinement_0.9.5.jpg) <br> `sheet_texture_refinement_0.9.5.jpg` | 0.9.5 texture refinement before/after: pavers, awning stripes and NPC fabric. |
|
||||||
|
|||||||
@@ -1007,6 +1007,32 @@ def _hands_and_shoes(mats, spec):
|
|||||||
return parts
|
return parts
|
||||||
|
|
||||||
|
|
||||||
|
def _hand_proto(mats, spec, side=-1.0):
|
||||||
|
"""Connected hand: palm, four fingers and a thumb fused into one mesh.
|
||||||
|
|
||||||
|
The shipped hand is separate primitive parts that read as a mitten at
|
||||||
|
conversation distance; fusing the finger bases into the palm gives a
|
||||||
|
connected hand with readable finger separation. `side` is −1 (left, x<0)
|
||||||
|
or +1 (right, x>0).
|
||||||
|
"""
|
||||||
|
hs = _hands_and_shoes(mats, spec)
|
||||||
|
|
||||||
|
def _cx(o):
|
||||||
|
xs = [v.co.x for v in o.data.vertices]
|
||||||
|
return (min(xs) + max(xs)) / 2
|
||||||
|
|
||||||
|
hand = [o for o in hs
|
||||||
|
if o.name.startswith(("Palm", "Finger", "Fingertip", "Thumb", "ThumbTip"))
|
||||||
|
and (_cx(o) < 0 if side < 0 else _cx(o) > 0)]
|
||||||
|
obj = join(hand, "HandL" if side < 0 else "HandR")
|
||||||
|
bm = bmesh.new()
|
||||||
|
bm.from_mesh(obj.data)
|
||||||
|
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.006)
|
||||||
|
bm.to_mesh(obj.data)
|
||||||
|
bm.free()
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
def _clothing(spec, mats):
|
def _clothing(spec, mats):
|
||||||
layer = spec["layer"]
|
layer = spec["layer"]
|
||||||
cloth = mats["cloth"]
|
cloth = mats["cloth"]
|
||||||
|
|||||||
+7
-2
@@ -53,7 +53,8 @@ PROTOTYPE = "npc_casual" # canonical one-variant prototype
|
|||||||
|
|
||||||
# The four buildable parts. `head` is the bare head shell; `face` is the shell
|
# The four buildable parts. `head` is the bare head shell; `face` is the shell
|
||||||
# plus nose/eyes/mouth/ears; `hand_l`/`hand_r` are the left/right hand geometry.
|
# plus nose/eyes/mouth/ears; `hand_l`/`hand_r` are the left/right hand geometry.
|
||||||
PART_NAMES = ("head", "face", "hand_l", "hand_r", "head_proto")
|
PART_NAMES = ("head", "face", "hand_l", "hand_r", "head_proto",
|
||||||
|
"hand_l_proto", "hand_r_proto")
|
||||||
_HAND_PREFIXES = ("Palm", "Finger", "Fingertip", "Thumb", "ThumbTip")
|
_HAND_PREFIXES = ("Palm", "Finger", "Fingertip", "Thumb", "ThumbTip")
|
||||||
|
|
||||||
|
|
||||||
@@ -104,6 +105,10 @@ def build_part(name, variant, out, subdiv=48):
|
|||||||
obj = ba.join(ba._face(mats, spec), "face")
|
obj = ba.join(ba._face(mats, spec), "face")
|
||||||
elif name == "head_proto":
|
elif name == "head_proto":
|
||||||
obj = ba.join(ba._head_proto(mats, spec, subdiv=subdiv), "head_proto")
|
obj = ba.join(ba._head_proto(mats, spec, subdiv=subdiv), "head_proto")
|
||||||
|
elif name == "hand_l_proto":
|
||||||
|
obj = ba._hand_proto(mats, spec, side=-1.0)
|
||||||
|
elif name == "hand_r_proto":
|
||||||
|
obj = ba._hand_proto(mats, spec, side=1.0)
|
||||||
else:
|
else:
|
||||||
side = -1.0 if name == "hand_l" else 1.0
|
side = -1.0 if name == "hand_l" else 1.0
|
||||||
hs = ba._hands_and_shoes(mats, spec)
|
hs = ba._hands_and_shoes(mats, spec)
|
||||||
@@ -155,7 +160,7 @@ def extract_part(name, variant, out):
|
|||||||
if mesh is None:
|
if mesh is None:
|
||||||
raise SystemExit(f"no npc_base mesh in {src}")
|
raise SystemExit(f"no npc_base mesh in {src}")
|
||||||
group = ("Head" if name in ("head", "face", "head_proto")
|
group = ("Head" if name in ("head", "face", "head_proto")
|
||||||
else f"Hand{'L' if name == 'hand_l' else 'R'}")
|
else f"Hand{'L' if name in ('hand_l', 'hand_l_proto') else 'R'}")
|
||||||
return _extract_group(mesh, group, out)
|
return _extract_group(mesh, group, out)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ class CliTests(unittest.TestCase):
|
|||||||
def test_part_names_are_known(self):
|
def test_part_names_are_known(self):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
set(npc_parts.PART_NAMES),
|
set(npc_parts.PART_NAMES),
|
||||||
{"head", "face", "hand_l", "hand_r", "head_proto"},
|
{"head", "face", "hand_l", "hand_r", "head_proto",
|
||||||
|
"hand_l_proto", "hand_r_proto"},
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_blender_commands_fail_cleanly_without_bpy(self):
|
def test_blender_commands_fail_cleanly_without_bpy(self):
|
||||||
|
|||||||
Reference in new issue
Block a user