Smooth the NPC neck and shoulder transition
Full build / Import + headless tests (push) Successful in 39s
Full build / Full desktop build + optional release (push) Skipped

This commit is contained in:
zwitschi committed 2026-11-02 19:03:38 +01:00
1 parent ab9c67d266
commit 15ab8c686d
29 files changed
+85 -53

No files matched your search

+28 -8
View File
@@ -535,15 +535,18 @@ def _skin_body(spec):
((0, 0, 0.96), 0.150 * w * bulk, 0.105 * d * bulk), # 1 waist
((0, 0.0, 1.18), 0.170 * w * bulk, 0.115 * d * bulk), # 2 chest
((0, 0.0, 1.355), 0.172 * w * bulk, 0.108 * d * bulk), # 3 upper chest
((0, 0, 1.44), 0.058, 0.058), # 4 neck base
((0, -0.005, 1.53), 0.052, 0.052), # 5 neck top
# A slightly fuller neck base and a lower shoulder root give the
# head/neck/shoulder line a trapezius slope instead of a thin post on
# a hanger. The head and collar still sit above this band.
((0, 0, 1.43), 0.074, 0.072), # 4 neck base
((0, -0.005, 1.53), 0.060, 0.060), # 5 neck top
]
edges = [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)]
for s in (-1.0, 1.0):
base = len(nodes)
nodes += [
((0.20 * s * w, 0, 1.39), 0.075 * t * sleeve, 0.07 * t * sleeve), # shoulder
((0.27 * s * w, 0, 1.24), 0.058 * t * sleeve, 0.058 * t * sleeve), # upper arm
((0.20 * s * w, 0, 1.37), 0.080 * t * sleeve, 0.074 * t * sleeve), # shoulder
((0.27 * s * w, 0, 1.23), 0.058 * t * sleeve, 0.058 * t * sleeve), # upper arm
((0.275 * s * w, 0.005, 1.06), 0.046 * t * sleeve, 0.048 * t * sleeve), # elbow
((0.275 * s * w, -0.01, 0.80), 0.034 * t, 0.030 * t), # wrist
]
@@ -929,6 +932,23 @@ def build_character(name):
rest.append(cl)
mesh = join(rest, "npc_base")
# The fuller neck/shoulder skin graph can leave a few collinear triangles
# after the decimate step. Remove them before UV/export; Godot warns about
# zero-area faces during LOD generation and `audit` rejects them.
bm = bmesh.new()
bm.from_mesh(mesh.data)
bmesh.ops.dissolve_degenerate(bm, dist=2e-4, edges=bm.edges)
# Skin-modifier branch cleanup can still leave a handful of zero-area
# faces; Godot's LOD generator treats them as malformed. Triangulate first
# because a degenerate quad can become a zero-area triangle on export, then
# drop the tiny faces explicitly.
bmesh.ops.triangulate(bm, faces=bm.faces[:], quad_method="BEAUTY",
ngon_method="BEAUTY")
bad = [face for face in bm.faces if face.calc_area() < 1e-10]
if bad:
bmesh.ops.delete(bm, geom=bad, context="FACES_ONLY")
bm.to_mesh(mesh.data)
bm.free()
# Every contract slot must exist even if a variant does not paint with it.
present = {m.name for m in mesh.data.materials if m}
for slot in SLOTS:
@@ -1019,10 +1039,10 @@ def build_gear(name):
# torso front (Godot z≈0.21). The old chest/waist points sat at
# z≈0.13 inside the body, so only thin tube fragments read as two
# rigid black bars from the front.
p.append(tube("Strap", [(0.09 * s, 0.13, 0.27), (0.10 * s, -0.02, 0.29),
(0.105 * s, -0.06, 0.25),
(0.11 * s, -0.19, 0.18), (0.12 * s, -0.19, -0.08)],
0.018, M("plastic_black"), seg=8, width_scale=1.35,
p.append(tube("Strap", [(0.09 * s, 0.13, 0.27), (0.11 * s, -0.02, 0.29),
(0.12 * s, -0.06, 0.25),
(0.14 * s, -0.19, 0.18), (0.145 * s, -0.19, -0.08)],
0.016, M("denim_bag"), seg=8, width_scale=1.35,
thickness_scale=0.42))
elif name == "shoulder_bag":
# Keep the bag's inner face outside the widest skirt flare (dress = 0.26):