332 lines
8.7 KiB
GDScript
332 lines
8.7 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",
|
|
]
|
|
const EXPECTED_HEAD_MESHES: PackedStringArray = [
|
|
"head_round",
|
|
"head_pointy",
|
|
"head_anteater",
|
|
"head_axolotl",
|
|
"head_bat",
|
|
"head_bird",
|
|
"head_boar",
|
|
"head_butterfly",
|
|
"head_donkey",
|
|
"head_elephant",
|
|
"head_goat",
|
|
"head_hamster",
|
|
"head_horse",
|
|
"head_moth",
|
|
"head_opossum",
|
|
"head_owl",
|
|
"head_panda",
|
|
"head_pig",
|
|
"head_shark",
|
|
"head_sheep",
|
|
"head_snail",
|
|
]
|
|
const EXPECTED_ARM_MESHES: PackedStringArray = [
|
|
"arms_mammal", "arms_fish", "arms_avian",
|
|
]
|
|
const EXPECTED_EAR_MESHES: PackedStringArray = [
|
|
"ears_cat",
|
|
"ears_long",
|
|
"ears_fox",
|
|
"ears_bunny",
|
|
"ears_antlers_round",
|
|
"ears_bear",
|
|
"ears_anteater",
|
|
"ears_bat",
|
|
"ears_boar",
|
|
"ears_butterfly",
|
|
"ears_donkey",
|
|
"ears_axolotl",
|
|
"ears_elephant",
|
|
"ears_goat",
|
|
"ears_hamster",
|
|
"ears_horse",
|
|
"ears_opossum",
|
|
"ears_moth",
|
|
"ears_pig",
|
|
"ears_owl",
|
|
"ears_panda",
|
|
]
|
|
const EXPECTED_TAIL_MESHES: PackedStringArray = [
|
|
"tails_gator",
|
|
"tails_fox",
|
|
"tails_bunny",
|
|
"tails_cat",
|
|
"tails_bear",
|
|
"tails_pointy",
|
|
"tails_anteater",
|
|
"tails_boar",
|
|
"tails_butterfly",
|
|
"tail_donkey",
|
|
"tails_axolotl",
|
|
"tails_horse",
|
|
"tails_opossum",
|
|
"tails_moth",
|
|
"tails_pig",
|
|
"tails_shark",
|
|
"tails_bird",
|
|
]
|
|
const EXPECTED_SPECIAL_MESHES: PackedStringArray = [
|
|
"tusks_boar",
|
|
"tusks_elephant",
|
|
"wings_butterfly",
|
|
"wings_bat",
|
|
"wings_moth",
|
|
"horns_goat",
|
|
"horns_sheep",
|
|
"back_shell",
|
|
"head_fin",
|
|
"beak_owl",
|
|
"beak_bird",
|
|
]
|
|
const FACE_FEATURES: PackedStringArray = ["eyes", "nose", "mouth"]
|
|
const EXCLUDED_AUTHORING_MESHES: PackedStringArray = [
|
|
"frog_eyes_1",
|
|
"frog_eyes_2",
|
|
"frog_head_1",
|
|
"frog_head_2",
|
|
"rod_socket_preview-noimp",
|
|
]
|
|
const EXPECTED_BONE_PARENTS: Dictionary = {
|
|
"root": "",
|
|
"hips": "root",
|
|
"spine": "hips",
|
|
"clavicle.L": "spine",
|
|
"upper_arm.L": "clavicle.L",
|
|
"forearm.L": "upper_arm.L",
|
|
"hand.L": "forearm.L",
|
|
"clavicle.R": "spine",
|
|
"upper_arm.R": "clavicle.R",
|
|
"forearm.R": "upper_arm.R",
|
|
"hand.R": "forearm.R",
|
|
"rod_socket": "hand.R",
|
|
"rod_socket.001": "rod_socket",
|
|
"neck": "spine",
|
|
"head": "neck",
|
|
"thigh.L": "hips",
|
|
"shin.L": "thigh.L",
|
|
"thigh.R": "hips",
|
|
"shin.R": "thigh.R",
|
|
}
|
|
|
|
|
|
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)
|
|
assert(character.get_path_to(skeleton) == ^"CharacterRig/Skeleton3D")
|
|
assert(skeleton.get_bone_count() == EXPECTED_BONE_PARENTS.size())
|
|
for bone_name: String in EXPECTED_BONE_PARENTS:
|
|
var bone_index := skeleton.find_bone(bone_name)
|
|
assert(bone_index >= 0, "Missing bone: %s" % bone_name)
|
|
var parent_index := skeleton.get_bone_parent(bone_index)
|
|
var actual_parent := (
|
|
str(skeleton.get_bone_name(parent_index))
|
|
if parent_index >= 0
|
|
else ""
|
|
)
|
|
assert(
|
|
actual_parent == str(EXPECTED_BONE_PARENTS[bone_name]),
|
|
"Unexpected parent for bone %s: %s" % [bone_name, actual_parent],
|
|
)
|
|
|
|
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 expected_mesh_names := _expected_runtime_mesh_names()
|
|
assert(expected_mesh_names.size() == 137)
|
|
var face_decal_count := 0
|
|
for head_name: String in EXPECTED_HEAD_MESHES:
|
|
for feature_name: String in FACE_FEATURES:
|
|
assert(expected_mesh_names.has(
|
|
"%s_%s" % [head_name, feature_name]
|
|
))
|
|
face_decal_count += 1
|
|
assert(face_decal_count == 63)
|
|
|
|
var runtime_meshes: Array[MeshInstance3D] = []
|
|
_collect_mesh_instances(character, runtime_meshes)
|
|
assert(runtime_meshes.size() == expected_mesh_names.size())
|
|
var actual_mesh_names: Dictionary = {}
|
|
var total_blended_vertices := 0
|
|
for mesh_instance: MeshInstance3D in runtime_meshes:
|
|
assert(not actual_mesh_names.has(mesh_instance.name))
|
|
actual_mesh_names[mesh_instance.name] = true
|
|
assert(
|
|
expected_mesh_names.has(mesh_instance.name),
|
|
"Unexpected runtime character mesh: %s" % mesh_instance.name,
|
|
)
|
|
total_blended_vertices += _validate_mesh_weights(mesh_instance)
|
|
assert(actual_mesh_names.size() == expected_mesh_names.size())
|
|
for mesh_name: String in expected_mesh_names:
|
|
assert(actual_mesh_names.has(mesh_name), "Missing mesh: %s" % mesh_name)
|
|
for mesh_name: String in EXCLUDED_AUTHORING_MESHES:
|
|
assert(not actual_mesh_names.has(mesh_name))
|
|
|
|
var blended_arm_vertices := 0
|
|
for mesh_name: String in EXPECTED_ARM_MESHES:
|
|
var arms := character.find_child(
|
|
mesh_name,
|
|
true,
|
|
false,
|
|
) as MeshInstance3D
|
|
assert(arms != null)
|
|
var blended_vertices := _validate_mesh_weights(arms)
|
|
assert(
|
|
blended_vertices > 0,
|
|
"%s has no blended joint weights." % mesh_name,
|
|
)
|
|
blended_arm_vertices += blended_vertices
|
|
assert(total_blended_vertices >= blended_arm_vertices)
|
|
|
|
print(
|
|
(
|
|
"Character rig validation: PASS "
|
|
+ "(meshes=%d, face decals=%d, bones=%d, animations=%d, "
|
|
+ "blended arm vertices=%d)"
|
|
)
|
|
% [
|
|
runtime_meshes.size(),
|
|
face_decal_count,
|
|
skeleton.get_bone_count(),
|
|
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: int = int(
|
|
float(weights.size()) / float(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 <= 4)
|
|
if influence_count > 1:
|
|
blended_vertices += 1
|
|
return blended_vertices
|
|
|
|
|
|
func _expected_runtime_mesh_names() -> Dictionary:
|
|
var result: Dictionary = {"body_main": true}
|
|
for mesh_name: String in EXPECTED_ARM_MESHES:
|
|
result[mesh_name] = true
|
|
for head_name: String in EXPECTED_HEAD_MESHES:
|
|
result[head_name] = true
|
|
for feature_name: String in FACE_FEATURES:
|
|
result["%s_%s" % [head_name, feature_name]] = true
|
|
for mesh_name: String in EXPECTED_EAR_MESHES:
|
|
result[mesh_name] = true
|
|
for mesh_name: String in EXPECTED_TAIL_MESHES:
|
|
result[mesh_name] = true
|
|
for mesh_name: String in EXPECTED_SPECIAL_MESHES:
|
|
result[mesh_name] = true
|
|
return result
|
|
|
|
|
|
func _collect_mesh_instances(
|
|
parent: Node,
|
|
result: Array[MeshInstance3D],
|
|
) -> void:
|
|
if parent is MeshInstance3D:
|
|
result.append(parent as MeshInstance3D)
|
|
for child: Node in parent.get_children():
|
|
_collect_mesh_instances(child, result)
|
|
|
|
|
|
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
|