Raise the NPC character budget to 24k and refine the prototype neck

This commit is contained in:
zwitschi committed 2026-11-05 19:37:19 +01:00
1 parent f0d4b81239
commit 79224c5acf
19 files changed
+128 -56

No files matched your search

+11 -11
View File
@@ -53,14 +53,14 @@ MANIFEST = OUT_DIR / "manifest.json"
SLOTS = ["skin", "cloth", "pants", "accessory", "hair"]
BONES = ["Head", "HipL", "HipR", "KneeL", "KneeR", "ShoulderL", "ShoulderR",
"ElbowL", "ElbowR", "HandL", "HandR"]
# Character cap raised to 16000/16000 on 2026-11-04 for the head/face/hand
# overhaul and possible MakeHuman parts. The required 13 core bones stay, but
# optional extra bones (fingers, face, toes) are allowed; they must not move the
# core rest positions. The glTF vertex count is after UV-seam splits; each
# avatar is still one skinned mesh, and `smoke_npc_budget.gd` must confirm the
# 24-NPC frame time before accepting the increase.
CHAR_MAX_TRIS = 16000
CHAR_MAX_VERTS = 16000
# Character cap raised to 24000/24000 on 2026-11-05 for the MPFB body, the
# accepted MakeHuman head and a seamless neck/shoulder junction. The required
# 13 core bones stay, but optional extra bones (fingers, face, toes) are
# allowed; they must not move the core rest positions. The glTF vertex count is
# after UV-seam splits; each avatar is still one skinned mesh, and the 24-NPC
# smoke test must confirm frame time before shipping.
CHAR_MAX_TRIS = 24000
CHAR_MAX_VERTS = 24000
GEAR_MAX_TRIS = 1500
PROP_MAX_TRIS = 4000
@@ -2486,7 +2486,7 @@ def inspect_glb(path, category=None) -> dict:
return result
def adapt_glb(path, out_path, target_tris=16000):
def adapt_glb(path, out_path, target_tris=24000):
"""Import an external GLB and adapt it toward the library contract.
Applies node transforms, grounds the feet (lowest vertex at y = 0), welds
@@ -2892,8 +2892,8 @@ def main(argv=None):
ada.add_argument("--file", required=True,
help="external GLB to adapt (absolute path; runs in Blender)")
ada.add_argument("--out", required=True, help="where to write the adapted GLB")
ada.add_argument("--target-tris", type=int, default=16000,
help="decimate target (default 16000)")
ada.add_argument("--target-tris", type=int, default=24000,
help="decimate target (default 24000)")
tex = sub.add_parser("texels", help="per-slot texel density of the character GLBs")
tex.add_argument("--only", default="")
tex.add_argument("--json", action="store_true")
+3 -3
View File
@@ -23,7 +23,7 @@ Contract (frozen in ADR-0013 and docs/blender-assets.md):
(plus Hips/Spine/Neck as parents)
* material slots: skin, cloth, pants, accessory, hair
* neutral albedo + normal + packed ORM maps, albedo tinted at runtime
* <= 16000 triangles / 16000 vertices, UVs in [0,1], feet at y ~= 0
* <= 24000 triangles / 24000 vertices, UVs in [0,1], feet at y ~= 0
* the 13 core bones are required; extra optional bones are allowed
"""
@@ -45,8 +45,8 @@ BONES = [
"Head", "HipL", "HipR", "KneeL", "KneeR",
"ShoulderL", "ShoulderR", "ElbowL", "ElbowR", "HandL", "HandR",
]
MAX_TRIANGLES = 16000
MAX_VERTICES = 16000
MAX_TRIANGLES = 24000
MAX_VERTICES = 24000
TEX_SIZE = 256
AI_DIR = TEX_DIR / "ai"
# Base material slots that get an AI fabric albedo (`prepare` layers them over
+6 -6
View File
@@ -9,10 +9,10 @@ class CatalogueTests(unittest.TestCase):
def test_contract_matches_npc_pipeline(self):
self.assertEqual(ba.SLOTS, npc_pipeline.SLOTS)
self.assertEqual(sorted(ba.BONES), sorted(npc_pipeline.BONES))
# The character cap was raised to 16000/16000 for the head/face/hand
# The character cap was raised to 24000/24000 for the MPFB body and the head
# overhaul; the 13 core bones stay required and extra bones are optional.
self.assertLessEqual(ba.CHAR_MAX_TRIS, 16000)
self.assertLessEqual(ba.CHAR_MAX_VERTS, 16000)
self.assertLessEqual(ba.CHAR_MAX_TRIS, 24000)
self.assertLessEqual(ba.CHAR_MAX_VERTS, 24000)
def test_gear_uses_known_attach_points(self):
for name, attach in ba.GEAR.items():
@@ -197,7 +197,7 @@ class InspectionTests(unittest.TestCase):
def test_character_contract_failures_are_listed(self):
stats = self.stats(mesh_nodes=["Armature"], skinned=False, unskinned=["Armature"],
materials=["cloth"], bones=["Head"], meshes_without_uv=1,
triangles=17000, split_vertices=17000,
triangles=25000, split_vertices=25000,
world_bounds={"min": [-0.3, -0.4, -0.2], "max": [0.3, 1.2, 0.2]})
issues = "\n".join(ba.analyse_inspection(stats)["issues"])
self.assertIn("no mesh node named npc_base", issues)
@@ -205,8 +205,8 @@ class InspectionTests(unittest.TestCase):
self.assertIn("missing material slot skin", issues)
self.assertIn("mesh not skinned", issues)
self.assertIn("mesh(es) without UVs", issues)
self.assertIn(f"triangles 17000 > {ba.CHAR_MAX_TRIS}", issues)
self.assertIn(f"glTF vertices 17000 > {ba.CHAR_MAX_VERTS}", issues)
self.assertIn(f"triangles 25000 > {ba.CHAR_MAX_TRIS}", issues)
self.assertIn(f"glTF vertices 25000 > {ba.CHAR_MAX_VERTS}", issues)
self.assertIn("feet not on the ground", issues)
self.assertIn("unexpected height", issues)
+2 -2
View File
@@ -13,8 +13,8 @@ class NpcContractTests(unittest.TestCase):
"ShoulderL", "ShoulderR", "ElbowL", "ElbowR",
"HandL", "HandR"]:
self.assertIn(bone, npc_pipeline.BONES)
self.assertLessEqual(npc_pipeline.MAX_TRIANGLES, 16000)
self.assertLessEqual(npc_pipeline.MAX_VERTICES, 16000)
self.assertLessEqual(npc_pipeline.MAX_TRIANGLES, 24000)
self.assertLessEqual(npc_pipeline.MAX_VERTICES, 24000)
def test_textures_exist_for_every_slot(self):
for slot in npc_pipeline.SLOTS: