Load and animate the Blender-authored NPC base mesh
Wire the skinned model from ADR-0013 into NpcAvatar: - _build_skinned_body() instantiates the imported npc_base.glb, binds the 14 armature bones, and keeps the procedural _build_body() as the offline fallback (use_skinned_model can force it in tests) - outfit_for()/layer_for() tint the five material slots through the new SurfaceMaterials.npc_slot(); accessories and hand items attach under Rig/Head - the existing limb pivots (Hip/Knee/Shoulder/Elbow/Hand + Head marker) keep the old animation math; _sync_skeleton() copies their rotations to bones. Bone pose rotation is absolute local orientation, so it is composed with the rest basis (otherwise the whole body snaps into a broken pose) - skinned rigs scale uniformly (non-uniform parent scale shears the skin) and keep Rig/Head for the focus cone, nametag, and reaction tag - commit the editable npc_base.blend plus a .gdignore: Godot's editor otherwise tries to import the blend and hangs during --import - npc_pipeline audit tests (contract, textures, clean GLB, committed source) Verification: headless 212/212, Python tooling 60/60, NPC budget 16 avatars = 144 meshes at ~6.9 ms, movement/focus/props smokes pass, compare 11 good / 0 bad. TODO Phases 0-3 checked; environment lift and release remain.
This commit is contained in:
1 parent
3989d7b2e5
commit
a231f7588a
20 files changed
+316
-37
No files matched your search
@@ -0,0 +1,44 @@
|
||||
import os
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
import npc_pipeline
|
||||
|
||||
|
||||
class NpcContractTests(unittest.TestCase):
|
||||
def test_contract_constants(self):
|
||||
self.assertEqual(npc_pipeline.SLOTS,
|
||||
["skin", "cloth", "pants", "accessory", "hair"])
|
||||
for bone in ["Head", "HipL", "HipR", "KneeL", "KneeR",
|
||||
"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)
|
||||
|
||||
def test_textures_exist_for_every_slot(self):
|
||||
for slot in npc_pipeline.SLOTS:
|
||||
for suffix in ("albedo", "normal", "orm"):
|
||||
path = npc_pipeline.TEX_DIR / f"{slot}_{suffix}.png"
|
||||
self.assertTrue(path.is_file(), path)
|
||||
self.assertGreater(path.stat().st_size, 100, path)
|
||||
|
||||
@unittest.skipUnless(npc_pipeline.GLB_PATH.is_file(), "npc_base.glb not built")
|
||||
def test_committed_model_audits_clean(self):
|
||||
result = npc_pipeline.audit_glb()
|
||||
self.assertTrue(result["ok"], result["issues"])
|
||||
self.assertLessEqual(result["triangles"], npc_pipeline.MAX_TRIANGLES)
|
||||
self.assertLessEqual(result["vertices"], npc_pipeline.MAX_VERTICES)
|
||||
for bone in npc_pipeline.BONES:
|
||||
self.assertIn(bone, result["bones"])
|
||||
|
||||
@unittest.skipUnless(npc_pipeline.GLB_PATH.is_file(), "npc_base.glb not built")
|
||||
def test_blend_source_is_committed(self):
|
||||
self.assertTrue(npc_pipeline.BLEND_PATH.is_file())
|
||||
# Godot must not try to import the .blend (it hangs without Blender).
|
||||
gdignore = npc_pipeline.BLEND_PATH.parent / ".gdignore"
|
||||
self.assertTrue(gdignore.is_file(), "assets/models/npc/.gdignore is required")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user