Raise character limits and document optional Blender MCP
This commit is contained in:
1 parent
50a48d2256
commit
aec1fb7b69
20 files changed
+303
-96
No files matched your search
+15
-13
@@ -53,13 +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"]
|
||||
# The eight library variants may use more geometry than the frozen npc_base
|
||||
# fallback (6000 tris / 4000 verts). Raised to 8000/8000 on 2026-11-02 so the
|
||||
# remaining face, hand-grip and clothing work is not forced to replace detail.
|
||||
# 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.
|
||||
CHAR_MAX_TRIS = 8000
|
||||
CHAR_MAX_VERTS = 8000
|
||||
# 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
|
||||
GEAR_MAX_TRIS = 1500
|
||||
PROP_MAX_TRIS = 4000
|
||||
|
||||
@@ -2135,14 +2136,15 @@ def inspect_glb(path, category=None) -> dict:
|
||||
return result
|
||||
|
||||
|
||||
def adapt_glb(path, out_path, target_tris=8000):
|
||||
def adapt_glb(path, out_path, target_tris=16000):
|
||||
"""Import an external GLB and adapt it toward the library contract.
|
||||
|
||||
Applies node transforms, grounds the feet (lowest vertex at y = 0), welds
|
||||
duplicated seam vertices and decimates to `target_tris`, then exports the
|
||||
result to `out_path` as a GLB. The five tint slots and the 13-bone contract
|
||||
armature are deliberately NOT auto-mapped — that is a manual retopology/rig
|
||||
step. Returns before/after measurements for the intake report.
|
||||
result to `out_path` as a GLB. The five tint slots and the required 13 core
|
||||
bones (optional extra bones are allowed) are deliberately NOT auto-mapped —
|
||||
that is a manual retopology/rig step. Returns before/after measurements for
|
||||
the intake report.
|
||||
"""
|
||||
if bpy is None:
|
||||
return {"ok": False, "error": "adapt must run inside Blender"}
|
||||
@@ -2537,8 +2539,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=8000,
|
||||
help="decimate target (default 8000)")
|
||||
ada.add_argument("--target-tris", type=int, default=16000,
|
||||
help="decimate target (default 16000)")
|
||||
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")
|
||||
|
||||
@@ -23,7 +23,8 @@ 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
|
||||
* <= 6000 triangles / 4000 vertices, UVs in [0,1], feet at y ~= 0
|
||||
* <= 16000 triangles / 16000 vertices, UVs in [0,1], feet at y ~= 0
|
||||
* the 13 core bones are required; extra optional bones are allowed
|
||||
"""
|
||||
|
||||
import argparse
|
||||
@@ -44,8 +45,8 @@ BONES = [
|
||||
"Head", "HipL", "HipR", "KneeL", "KneeR",
|
||||
"ShoulderL", "ShoulderR", "ElbowL", "ElbowR", "HandL", "HandR",
|
||||
]
|
||||
MAX_TRIANGLES = 6000
|
||||
MAX_VERTICES = 4000
|
||||
MAX_TRIANGLES = 16000
|
||||
MAX_VERTICES = 16000
|
||||
TEX_SIZE = 256
|
||||
AI_DIR = TEX_DIR / "ai"
|
||||
# Base material slots that get an AI fabric albedo (`prepare` layers them over
|
||||
|
||||
@@ -9,11 +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 base mesh keeps the frozen 6000/4000 contract; the eight library
|
||||
# variants have their own 8000/8000 detail budget (documented in
|
||||
# blender-assets.md and covered by smoke_npc_budget.gd).
|
||||
self.assertLessEqual(ba.CHAR_MAX_TRIS, 8000)
|
||||
self.assertLessEqual(ba.CHAR_MAX_VERTS, 8000)
|
||||
# The character cap was raised to 16000/16000 for the head/face/hand
|
||||
# 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)
|
||||
|
||||
def test_gear_uses_known_attach_points(self):
|
||||
for name, attach in ba.GEAR.items():
|
||||
@@ -188,10 +187,17 @@ class InspectionTests(unittest.TestCase):
|
||||
self.assertEqual(result["bones_missing"], [])
|
||||
self.assertEqual(result["budget"]["max_triangles"], ba.CHAR_MAX_TRIS)
|
||||
|
||||
def test_extra_optional_bones_are_allowed(self):
|
||||
stats = self.stats(bones=list(ba.BONES) + ["Hips", "Spine", "Neck",
|
||||
"FingerL", "FingerR"])
|
||||
result = ba.analyse_inspection(stats)
|
||||
self.assertTrue(result["ok"], result["issues"])
|
||||
self.assertEqual(result["bones_missing"], [])
|
||||
|
||||
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=9000, split_vertices=9000,
|
||||
triangles=17000, split_vertices=17000,
|
||||
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)
|
||||
@@ -199,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 9000 > {ba.CHAR_MAX_TRIS}", issues)
|
||||
self.assertIn(f"glTF vertices 9000 > {ba.CHAR_MAX_VERTS}", issues)
|
||||
self.assertIn(f"triangles 17000 > {ba.CHAR_MAX_TRIS}", issues)
|
||||
self.assertIn(f"glTF vertices 17000 > {ba.CHAR_MAX_VERTS}", issues)
|
||||
self.assertIn("feet not on the ground", issues)
|
||||
self.assertIn("unexpected height", issues)
|
||||
|
||||
|
||||
@@ -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, 6000)
|
||||
self.assertLessEqual(npc_pipeline.MAX_VERTICES, 4000)
|
||||
self.assertLessEqual(npc_pipeline.MAX_TRIANGLES, 16000)
|
||||
self.assertLessEqual(npc_pipeline.MAX_VERTICES, 16000)
|
||||
|
||||
def test_textures_exist_for_every_slot(self):
|
||||
for slot in npc_pipeline.SLOTS:
|
||||
|
||||
Reference in new issue
Block a user