Feat/outline thickness and tp weapon hold #22

Merged
Dotts merged 43 commits from feat/outline-thickness-and-tp-weapon-hold into main 2026-07-27 23:22:53 -07:00
16 changed files with 2025 additions and 2673 deletions
Showing only changes of commit 27c4117c25 - Show all commits
@@ -52,8 +52,16 @@ That is exactly what makes the bug invisible: **the number you normalised always
comes out right, whether or not it was the right number.** A check on "is the
height 1.75" passes on all seven of these.
**Rule:** derive the up axis from the SKELETON (hips → head), not from a
convention, and rotate the model upright before scaling anything.
**Rule:** derive the up axis from the SKELETON, and rotate the model upright
before scaling anything. `flatten_and_scale()` now does this.
**Measure it from the FEET to the HIPS, not from the hips to the head.** The head
is not a reliable landmark: the spine walk ends on whatever the last non-cosmetic
bone in the chain is, and on a rig with a facial skeleton that can be a bone
sitting BELOW the hips. Momo's did, so the first version of this fix stood her
neatly on her head — correct size, correct proportions, upside down. Feet cannot
be mistaken; they are the bottom of a standing character on every rig, and
`foot.L/R` have resolved on every source met so far.
**And compare it to WORLD up, not to the model's own proportions.** The obvious
test — "is the spine the longest axis of the bounding box?" — catches nothing
@@ -172,7 +180,25 @@ than judging by eye.
---
## 6. Known-unsolved, and honestly so
## 6. Some sources are not salvageable, and the gate should say so
**Hit:** hikari. She failed every way at once — stretched and warped, tiny, far
away, gun backwards. Her rig has been through at least two toolchains: her
cosmetic bones are zero-length terminators, she carried a second armature with
its own clips, and her feet and her spine disagree about which way is up, so the
stand-up correction cannot resolve her either. She is now REJECTED by the gate
and removed from the roster rather than shipped broken.
**Rule:** a source that fails several unrelated checks is not a tuning problem,
it is a bad file. Spend the effort on finding a cleaner source, not on repairing
this one — and make sure the gate blocks it, because the failure mode before this
work was that everything passed and the breakage was only visible in game.
The vetting snippet in `separation.md` catches most of these before download:
several meshes, 50+ joints, cosmetic bones present. Add a look for duplicate
armatures and for bone chains whose bones are all at the same position.
## 7. Known-unsolved, and honestly so
Taila's legs still clip through the front of her skirt in a run, slide and dash
(~95 mm). See `cloth-and-hair.md`. The solver sees the contact and pushes on it
Binary file not shown.
File diff suppressed because it is too large Load Diff
Binary file not shown.
@@ -1,9 +0,0 @@
{
"name": "school anime girl (rigged)",
"uid": "6eaf44ff6ef7461583a5eccc301b88fc",
"author": "hikari",
"author_url": "https://sketchfab.com/hikari0135",
"license": "CC Attribution",
"license_slug": "by",
"source_url": "https://sketchfab.com/3d-models/school-anime-girl-rigged-6eaf44ff6ef7461583a5eccc301b88fc"
}
-731
View File
@@ -1,731 +0,0 @@
{
"roles": {
"hips": "Hips",
"head": "Bone.003_end",
"neck": "Neck",
"spine": [
"Spine",
"Chest",
"Upper Chest",
"Neck",
"Head",
"Bone.003",
"Bone.003_end"
],
"shoulder.R": "Right shoulder",
"shoulder.L": "Left shoulder",
"foot.R": "Right ankle",
"foot.L": "Left ankle",
"hand.R": "Right wrist",
"hand.L": "Left wrist",
"forearm.R": "Right elbow",
"forearm.L": "Left elbow",
"shin.R": "Right knee",
"shin.L": "Left knee",
"toe.R": "Right toe",
"toe.L": "Left toe_end",
"upper_arm.R": "Right arm",
"upper_arm.L": "Left arm",
"thigh.L": "Left leg",
"thigh.R": "Right leg"
},
"chains": [
{
"class": "breast",
"root_parent": "Upper Chest",
"bones": [
"Breast_L",
"J_Sec_L_Bust2",
"J_Sec_L_Bust2_end"
],
"tips": [
[
-0.0,
0.0,
0.0
],
[
2e-05,
5e-05,
-0.0
],
[
2e-05,
5e-05,
-0.0
]
],
"hulls": [
[],
[],
[]
],
"neighbours": [
{},
{},
{}
]
},
{
"class": "breast",
"root_parent": "Upper Chest",
"bones": [
"Breast_R",
"J_Sec_R_Bust2",
"J_Sec_R_Bust2_end"
],
"tips": [
[
0.0,
0.0,
-0.0
],
[
-3e-05,
5e-05,
-1e-05
],
[
-3e-05,
5e-05,
-1e-05
]
],
"hulls": [
[],
[],
[]
],
"neighbours": [
{},
{},
{}
]
},
{
"class": "hair",
"root_parent": "Head",
"bones": [
"J_Sec_Hair1_01",
"J_Sec_Hair2_01",
"J_Sec_Hair2_01_end"
],
"tips": [
[
-0.0,
-0.0,
-0.0
],
[
3e-05,
3e-05,
-4e-05
],
[
3e-05,
3e-05,
-4e-05
]
],
"hulls": [
[
[
-0.0,
-0.0,
-0.0
]
],
[
[
-0.0,
0.0,
0.0
]
],
[]
],
"neighbours": [
{
"J_Sec_Hair2_01": 14.335576471341188
},
{
"J_Sec_Hair1_01": 14.335576471341188
},
{}
]
},
{
"class": "hair",
"root_parent": "Head",
"bones": [
"J_Sec_Hair1_02",
"J_Sec_Hair2_02",
"J_Sec_Hair2_02_end"
],
"tips": [
[
0.0,
0.0,
-0.0
],
[
0.0,
2e-05,
-5e-05
],
[
0.0,
2e-05,
-5e-05
]
],
"hulls": [
[
[
-0.0,
0.0,
-0.0
]
],
[
[
-0.0,
0.0,
-0.0
]
],
[]
],
"neighbours": [
{
"J_Sec_Hair2_02": 13.644314260335037
},
{
"J_Sec_Hair1_02": 13.644314260335037
},
{}
]
},
{
"class": "hair",
"root_parent": "Head",
"bones": [
"J_Sec_Hair1_03",
"J_Sec_Hair2_03",
"J_Sec_Hair2_03_end"
],
"tips": [
[
0.0,
0.0,
-0.0
],
[
1e-05,
-0.0,
-6e-05
],
[
1e-05,
-0.0,
-6e-05
]
],
"hulls": [
[
[
0.0,
0.0,
0.0
]
],
[
[
-0.0,
0.0,
0.0
]
],
[]
],
"neighbours": [
{
"J_Sec_Hair2_03": 29.697010893706615
},
{
"J_Sec_Hair1_03": 29.697010893706615
},
{}
]
},
{
"class": "hair",
"root_parent": "Head",
"bones": [
"J_Sec_Hair1_04",
"J_Sec_Hair2_04",
"J_Sec_Hair2_04_end"
],
"tips": [
[
0.0,
0.0,
0.0
],
[
2e-05,
1e-05,
-6e-05
],
[
2e-05,
1e-05,
-6e-05
]
],
"hulls": [
[
[
-0.0,
-0.0,
0.0
]
],
[
[
0.0,
0.0,
-0.0
]
],
[]
],
"neighbours": [
{
"J_Sec_Hair2_04": 11.592366459380674
},
{
"J_Sec_Hair1_04": 11.592366459380674
},
{}
]
},
{
"class": "hair",
"root_parent": "Head",
"bones": [
"J_Sec_Hair1_05",
"J_Sec_Hair2_05",
"J_Sec_Hair2_05_end"
],
"tips": [
[
0.0,
0.0,
-0.0
],
[
1e-05,
2e-05,
-6e-05
],
[
1e-05,
2e-05,
-6e-05
]
],
"hulls": [
[
[
0.0,
0.0,
-0.0
]
],
[
[
-0.0,
0.0,
-0.0
]
],
[]
],
"neighbours": [
{
"J_Sec_Hair2_05": 11.876289952662745
},
{
"J_Sec_Hair1_05": 11.876289952662745
},
{}
]
},
{
"class": "hair",
"root_parent": "Head",
"bones": [
"J_Sec_Hair1_06",
"J_Sec_Hair2_06",
"J_Sec_Hair3_06",
"J_Sec_Hair4_06",
"J_Sec_Hair5_06",
"J_Sec_Hair5_06_end"
],
"tips": [
[
-0.0,
0.0,
-0.0
],
[
-0.0,
0.0,
0.0
],
[
-0.0,
0.0,
0.0
],
[
-0.0,
0.0,
0.0
],
[
0.0,
3e-05,
-5e-05
],
[
0.0,
3e-05,
-5e-05
]
],
"hulls": [
[
[
0.0,
0.0,
-0.0
]
],
[
[
-0.0,
0.0,
0.0
]
],
[
[
-0.0,
0.0,
-0.0
]
],
[
[
-0.0,
0.0,
0.0
]
],
[
[
-0.0,
0.0,
0.0
]
],
[]
],
"neighbours": [
{
"J_Sec_Hair2_06": 35.917357232526754
},
{
"J_Sec_Hair3_06": 35.52913589468082,
"J_Sec_Hair1_06": 35.917357232526754
},
{
"J_Sec_Hair4_06": 40.302343821289796,
"J_Sec_Hair2_06": 35.52913589468082
},
{
"J_Sec_Hair3_06": 40.302343821289796,
"J_Sec_Hair5_06": 40.950213036078026
},
{
"J_Sec_Hair4_06": 40.950213036078026
},
{}
]
},
{
"class": "hair",
"root_parent": "Head",
"bones": [
"J_Sec_Hair1_07",
"J_Sec_Hair2_07",
"J_Sec_Hair3_07",
"J_Sec_Hair4_07",
"J_Sec_Hair5_07",
"J_Sec_Hair5_07_end"
],
"tips": [
[
0.0,
0.0,
0.0
],
[
-0.0,
0.0,
-0.0
],
[
-0.0,
0.0,
0.0
],
[
-0.0,
0.0,
0.0
],
[
3e-05,
3e-05,
-3e-05
],
[
3e-05,
3e-05,
-3e-05
]
],
"hulls": [
[
[
0.0,
0.0,
-0.0
]
],
[
[
-0.0,
0.0,
-0.0
]
],
[
[
0.0,
0.0,
0.0
]
],
[
[
-0.0,
0.0,
0.0
]
],
[
[
0.0,
0.0,
-0.0
]
],
[]
],
"neighbours": [
{
"J_Sec_Hair2_07": 35.51566362438205
},
{
"J_Sec_Hair3_07": 35.31215403867316,
"J_Sec_Hair1_07": 35.51566362438205
},
{
"J_Sec_Hair4_07": 44.27519591590416,
"J_Sec_Hair2_07": 35.31215403867316
},
{
"J_Sec_Hair3_07": 44.27519591590416,
"J_Sec_Hair5_07": 40.6256217681383
},
{
"J_Sec_Hair4_07": 40.6256217681383
},
{}
]
},
{
"class": "hair",
"root_parent": "Head",
"bones": [
"J_Sec_Hair1_08",
"J_Sec_Hair2_08",
"J_Sec_Hair3_08",
"J_Sec_Hair3_08_end"
],
"tips": [
[
0.0,
0.0,
0.0
],
[
-0.0,
0.0,
0.0
],
[
4e-05,
-4e-05,
-1e-05
],
[
4e-05,
-4e-05,
-1e-05
]
],
"hulls": [
[
[
-0.0,
-0.0,
0.0
]
],
[
[
-0.0,
0.0,
0.0
]
],
[
[
-0.0,
-0.0,
0.0
]
],
[]
],
"neighbours": [
{
"J_Sec_Hair2_08": 2.5078807040660998
},
{
"J_Sec_Hair1_08": 2.5078807040660998,
"J_Sec_Hair3_08": 2.222614413427433
},
{
"J_Sec_Hair2_08": 2.222614413427433
},
{}
]
}
],
"twist": [
{
"bone": "Bone.004",
"parent": "Head",
"child": "Bone.004_end"
},
{
"bone": "Bone.005",
"parent": "Head",
"child": "Bone.005_end"
},
{
"bone": "Bone.006",
"parent": "Head",
"child": "Bone.006_end"
},
{
"bone": "Bone.007",
"parent": "Head",
"child": "Bone.007_end"
},
{
"bone": "Bone.008",
"parent": "Head",
"child": "Bone.008_end"
},
{
"bone": "Bone.009",
"parent": "Head",
"child": "Bone.009_end"
},
{
"bone": "Bone.010",
"parent": "Head",
"child": "Bone.010_end"
},
{
"bone": "Bone.011",
"parent": "Head",
"child": "Bone.011_end"
},
{
"bone": "Bone.012",
"parent": "Head",
"child": "Bone.012_end"
},
{
"bone": "Bone.013",
"parent": "Head",
"child": "Bone.013_end"
},
{
"bone": "Bone.001",
"parent": "Upper Chest",
"child": "Bone.001_end"
},
{
"bone": "Bone.002",
"parent": "Upper Chest",
"child": "Bone.002_end"
}
],
"colliders": [],
"weights_authored": true,
"driven_bones": [
"Bone.003",
"Bone.003_end",
"Chest",
"Head",
"Hips",
"Left ankle",
"Left arm",
"Left elbow",
"Left knee",
"Left leg",
"Left shoulder",
"Left toe_end",
"Left wrist",
"Neck",
"Right ankle",
"Right arm",
"Right elbow",
"Right knee",
"Right leg",
"Right shoulder",
"Right toe",
"Right wrist",
"Spine",
"Upper Chest"
]
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 345 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 337 KiB

Binary file not shown.
File diff suppressed because it is too large Load Diff
-7
View File
@@ -41,13 +41,6 @@
"description": "Momo Ayase, DANDADAN (CC-BY, HiGuys920)",
"model": "res://assets/characters/skins/momo.glb",
"unlocked": true
},
{
"id": "hikari",
"name": "Hikari",
"description": "School anime girl \u2014 VRoid (CC-BY, hikari)",
"model": "res://assets/characters/skins/hikari.glb",
"unlocked": true
}
]
}
+22 -1
View File
@@ -922,7 +922,7 @@ func set_weapon(script_path: String) -> void:
_measure_weapon(w)
)
var hand_idx := _find_bone(["RightHand", "Hand_R", "hand.R"])
var hand_idx := _role_bone("hand.R", ["RightHand", "Hand_R", "hand.R"])
if hand_idx >= 0:
_weapon_attachment = BoneAttachment3D.new()
_weapon_attachment.name = "WeaponAttachment"
@@ -995,6 +995,27 @@ func _measure_weapon(w: Node3D) -> void:
# ── Helpers ───────────────────────────────────────────────────────────────────
## A bone by its resolved ROLE, falling back to name matching.
##
## tools/rig_map.py resolves every rig to roles and writes them to the sidecar so
## that nothing downstream has to guess a bone name — but a hardcoded lookup here
## meant four characters could not hold a gun. Their hands resolve perfectly as
## "Right wrist" and "J_Bip_R_Hand"; none of them matches a spelling this file
## knew, so set_weapon fell through to parenting the weapon to the model root at
## a fixed chest offset, where it is not attached to the character at all.
##
## The fallback stays for a model with no sidecar. It must never be the first
## thing tried.
func _role_bone(role: String, fallbacks: Array) -> int:
var roles: Dictionary = _rig_info.get("roles", {})
var actual := String(roles.get(role, ""))
if actual != "" and skeleton:
var b := skeleton.find_bone(actual)
if b >= 0:
return b
return _find_bone(fallbacks)
func _find_bone(name_parts: Array) -> int:
if not skeleton:
return -1
+52 -2
View File
@@ -335,7 +335,7 @@ def rebuild_hierarchy(arm, roles):
# ----------------------------------------------------------------- normalize
def flatten_and_scale(arm, meshes, target_height):
def flatten_and_scale(arm, meshes, target_height, roles=None):
"""Bake the import hierarchy away and set the character's real-world size.
Sketchfab wraps everything in scaled/rotated empties. Left in place they
@@ -363,6 +363,56 @@ def flatten_and_scale(arm, meshes, target_height):
apply_all()
# STAND THE CHARACTER UP before measuring anything.
#
# The scale below normalises the bounding box along Z because Z is up in
# Blender. For a model that arrives lying along another axis that measures
# the character's THICKNESS — about 0.25 m — so it gets scaled by ~7 and left
# on its back. One assumption, both symptoms, and invisible afterwards
# because the exporter maps Blender Z to glTF Y: "is the height 1.75" comes
# out true on a character who is 7.5 m tall lying down.
#
# Which way is up is not a convention to assume, it is a property of the
# skeleton: the head is above the hips. Snapped to the nearest axis rather
# than aligned exactly, so a character with a slight lean in their rest pose
# is stood up, not straightened.
# Measured from the FEET to the HIPS, not from the hips to the head.
#
# The head is not a reliable landmark. The spine walk ends on whatever the
# last non-cosmetic bone in the chain is, and on a rig with a facial skeleton
# that can be a bone sitting BELOW the hips — which points this vector
# downwards and stands the character neatly on her head. Momo did exactly
# that. Feet cannot be mistaken: they are the bottom of a standing character
# on every rig, and foot.L/R resolve on every source met so far.
if roles is not None and roles.hips:
hips_b = arm.data.bones.get(roles.hips)
feet = [arm.data.bones.get(roles.limb.get(("foot", s), ""))
for s in ("L", "R")]
feet = [f for f in feet if f]
ref = None
if feet:
ref = sum((f.head_local for f in feet), Vector()) / len(feet)
elif roles.head and arm.data.bones.get(roles.head):
# No feet — fall back to the head, and accept the risk above.
ref = hips_b.head_local - (
arm.data.bones[roles.head].head_local - hips_b.head_local)
if hips_b and ref is not None:
d = hips_b.head_local - ref
k = max(range(3), key=lambda i: abs(d[i]))
src = Vector((0.0, 0.0, 0.0))
src[k] = 1.0 if d[k] > 0 else -1.0
up = Vector((0.0, 0.0, 1.0))
if src.dot(up) < 0.999:
axis = src.cross(up)
if axis.length < 1e-6:
axis = Vector((1.0, 0.0, 0.0)) # upside down
R = Matrix.Rotation(src.angle(up), 4, axis.normalized())
for obj in [arm] + meshes:
obj.matrix_world = R @ obj.matrix_world
apply_all()
print(f"Stood the character up: feet->hips ran along "
f"{'XYZ'[k]}{'+' if d[k] > 0 else '-'}, rotated to Z+")
lo = Vector((1e9, 1e9, 1e9))
hi = -lo.copy()
for m in meshes:
@@ -1047,7 +1097,7 @@ def main():
rebuild_hierarchy(arm, roles)
subdivide_cloth_panels(arm, meshes, roles)
unbind_cloth_from_legs(arm, meshes, roles)
flatten_and_scale(arm, meshes, TARGET_HEIGHT)
flatten_and_scale(arm, meshes, TARGET_HEIGHT, roles)
fix_unlit_materials(meshes)
roles = RigRoles(arm) # rest positions moved; re-read