straywild/tests/character_rig_validation.gd

139 lines
3.9 KiB
GDScript

extends SceneTree
const CHARACTER_SCENE: PackedScene = preload(
"res://art/exported/characters/base/straywild_base_character.glb"
)
const EXPECTED_ANIMATIONS: Array[StringName] = [
&"casting",
&"casting_sit",
&"draw",
&"fighting",
&"fighting_sit",
&"fishing",
&"fishing_sit",
&"idle",
&"idle_show",
&"idle_sit",
&"idle_sit_show",
&"idle_sneak",
&"pocket_idle_idle",
&"pocket_idle_show",
&"pocket_show_idle",
&"pocket_show_show",
&"pocket_sit_idle_idle",
&"pocket_sit_idle_show",
&"pocket_sit_show_idle",
&"pocket_sit_show_show",
&"pocket_walking_idle_idle",
&"pocket_walking_idle_show",
&"pocket_walking_show_idle",
&"pocket_walking_show_show",
&"release",
&"release_sit",
&"retract",
&"retract_sit",
&"running",
&"running_show",
&"sneaking",
&"strike",
&"walking",
&"walking_show",
]
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
var character := CHARACTER_SCENE.instantiate()
root.add_child(character)
var skeleton := _find_first(character, "Skeleton3D") as Skeleton3D
var animation_player := _find_first(
character,
"AnimationPlayer",
) as AnimationPlayer
assert(skeleton != null)
assert(animation_player != null)
var socket_index := skeleton.find_bone("rod_socket")
assert(socket_index >= 0)
var socket_direction_index := skeleton.find_bone("rod_socket.001")
assert(socket_direction_index >= 0)
assert(skeleton.get_bone_parent(socket_direction_index) == socket_index)
var animation_names := animation_player.get_animation_list()
assert(animation_names.size() == EXPECTED_ANIMATIONS.size())
for expected in EXPECTED_ANIMATIONS:
assert(expected in animation_names, "Missing animation: %s" % expected)
var animation := animation_player.get_animation(expected)
if expected in [&"casting", &"draw", &"idle_sneak", &"sneaking", &"strike"]:
var has_socket_track: bool = false
for track_index in animation.get_track_count():
has_socket_track = has_socket_track or String(
animation.track_get_path(track_index)
).contains("rod_socket")
assert(
has_socket_track,
"Animation %s lost its authored rod socket pose." % expected,
)
var arms := character.find_child(
"body_arms",
true,
false,
) as MeshInstance3D
assert(arms != null)
var blended_arm_vertices := _validate_mesh_weights(arms)
assert(blended_arm_vertices > 0, "body_arms has no blended joint weights.")
var body := character.find_child(
"body_main",
true,
false,
) as MeshInstance3D
assert(body != null)
_validate_mesh_weights(body)
print(
"Character rig validation: PASS "
+ "(animations=%d, blended arm vertices=%d)"
% [EXPECTED_ANIMATIONS.size(), blended_arm_vertices]
)
character.queue_free()
quit()
func _validate_mesh_weights(mesh_instance: MeshInstance3D) -> int:
var blended_vertices := 0
for surface_index in mesh_instance.mesh.get_surface_count():
var arrays := mesh_instance.mesh.surface_get_arrays(surface_index)
var weights: PackedFloat32Array = arrays[Mesh.ARRAY_WEIGHTS]
var vertices: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
assert(not weights.is_empty())
assert(not vertices.is_empty())
var components_per_vertex := weights.size() / vertices.size()
assert(components_per_vertex == 4 or components_per_vertex == 8)
for offset in range(0, weights.size(), components_per_vertex):
var total := 0.0
var influence_count := 0
for component in range(components_per_vertex):
var weight := weights[offset + component]
total += weight
if weight > 0.000001:
influence_count += 1
assert(absf(total - 1.0) <= 0.0001)
assert(influence_count <= 2)
if influence_count == 2:
blended_vertices += 1
return blended_vertices
func _find_first(parent: Node, class_name_to_find: String) -> Node:
if parent.is_class(class_name_to_find):
return parent
for child in parent.get_children():
var found := _find_first(child, class_name_to_find)
if found != null:
return found
return null