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
@@ -125,6 +125,55 @@ func test_walk_to_idle_animation_transition() -> bool:
|
||||
avatar.free()
|
||||
return a and b and c
|
||||
|
||||
func test_skinned_model_loads_and_keeps_the_head_contract() -> bool:
|
||||
var avatar := _avatar(Rules.NpcType.TOURIST, 33)
|
||||
var a := TestUtils.check(avatar.loaded_model and not avatar.fallback_avatar,
|
||||
"committed npc_base.glb is used")
|
||||
var b := TestUtils.check(avatar.get_node_or_null("Rig/Head") != null,
|
||||
"Rig/Head survives the skinned path")
|
||||
var c := TestUtils.check(avatar._skeleton != null and avatar._bone_indices.has("Head")
|
||||
and avatar._bone_indices.has("HipL") and avatar._bone_indices.has("HandR"),
|
||||
"armature bones are bound")
|
||||
avatar.free()
|
||||
return a and b and c
|
||||
|
||||
func test_skinned_outfit_tints_the_material_slots() -> bool:
|
||||
var avatar := _avatar(Rules.NpcType.TOURIST, 44)
|
||||
var mi := avatar.find_child("npc_base", true, false) as MeshInstance3D
|
||||
var outfit := NpcAvatar.outfit_for(Rules.NpcType.TOURIST, 44)
|
||||
var found := 0
|
||||
var tinted := 0
|
||||
for index in mi.mesh.get_surface_count():
|
||||
var material := mi.get_surface_override_material(index) as StandardMaterial3D
|
||||
if material == null:
|
||||
continue
|
||||
found += 1
|
||||
if material.albedo_color != Color.WHITE:
|
||||
tinted += 1
|
||||
var a := TestUtils.check(found == 5, "five material slots stay separate")
|
||||
var b := TestUtils.check(tinted == found, "every slot carries the outfit tint")
|
||||
var c := TestUtils.check(outfit["shirt"] != Color.WHITE, "seed gives a non-white outfit")
|
||||
avatar.free()
|
||||
return a and b and c
|
||||
|
||||
func test_forced_fallback_still_builds_the_procedural_rig() -> bool:
|
||||
var profile := NpcProfile.new()
|
||||
profile.id = "fallback_test"
|
||||
profile.npc_type = Rules.NpcType.TOURIST
|
||||
profile.display_name = "Fallback"
|
||||
var avatar := NpcAvatar.new()
|
||||
avatar.use_skinned_model = false
|
||||
avatar.setup(profile, "enter", 5, Vector3(0, 0.5, 6),
|
||||
Vector3(0, 0.5, 1.5), Vector3(0, 0.5, 1.2), "fallback_test")
|
||||
var a := TestUtils.check(not avatar.loaded_model and avatar.fallback_avatar,
|
||||
"fallback is recorded when the model is disabled")
|
||||
var b := TestUtils.check(avatar.get_node_or_null("Rig/Head") != null,
|
||||
"fallback still exposes Rig/Head")
|
||||
var c := TestUtils.check(avatar.find_child("HipL", true, false) != null,
|
||||
"fallback limb pivots exist")
|
||||
avatar.free()
|
||||
return a and b and c
|
||||
|
||||
func test_rig_is_articulated() -> bool:
|
||||
var avatar := _avatar(Rules.NpcType.TOURIST, 77)
|
||||
var a := TestUtils.check(avatar.find_child("HipL", true, false) != null
|
||||
|
||||
Reference in new issue
Block a user