extends SceneTree ## Print every mesh surface of every shipping skin: node name, surface index, ## material name, whether it carries a texture, its albedo and cull mode, and ## which bones dominate it. ## ## This exists because the surface table in the rig sidecar is written by Blender ## and read by Godot, and the two do not have to agree on what anything is ## called. Rather than assume the glTF round trip preserves names and ordering, ## this measures what Godot ends up holding, so the sidecar can be keyed on ## something that actually survives. ## ## godot --headless --path . -s res://debug/dump_surfaces.gd func _init() -> void: var registry := "res://assets/characters/skins/skins.json" var data = JSON.parse_string(FileAccess.get_file_as_string(registry)) for entry in data["skins"]: var path: String = entry.get("model", "") if path == "" or not ResourceLoader.exists(path): continue print("\n=== %s (%s)" % [entry["id"], path]) var scene: Node = load(path).instantiate() var skel: Skeleton3D = _first_skeleton(scene) for mi in scene.find_children("*", "MeshInstance3D", true, false): if mi.mesh == null: continue var owner_names := _dominant_bones(mi, skel) for s in mi.mesh.get_surface_count(): var m: BaseMaterial3D = mi.mesh.surface_get_material(s) as BaseMaterial3D var mat_name := "" if m == null else m.resource_name var tex := m != null and m.albedo_texture != null var col := Color.WHITE if m == null else m.albedo_color var cull := -1 if m == null else int(m.cull_mode) print(" %-34s s%d mat=%-28s tex=%s albedo=(%.2f,%.2f,%.2f) cull=%d bones=%s" % [mi.name, s, mat_name, "Y" if tex else "n", col.r, col.g, col.b, cull, owner_names]) scene.free() quit() func _first_skeleton(node: Node) -> Skeleton3D: if node is Skeleton3D: return node for c in node.get_children(): var found := _first_skeleton(c) if found: return found return null ## The five bones holding the most dominant-weight vertices on this mesh. func _dominant_bones(mi: MeshInstance3D, skel: Skeleton3D) -> String: if mi.skin == null or skel == null or mi.mesh == null: return "" var bone_of := {} for b in mi.skin.get_bind_count(): var n := mi.skin.get_bind_name(b) bone_of[b] = skel.find_bone(n) if n != "" else mi.skin.get_bind_bone(b) var tally := {} var arrays: Array = mi.mesh.surface_get_arrays(0) var bones: PackedInt32Array = arrays[Mesh.ARRAY_BONES] var weights: PackedFloat32Array = arrays[Mesh.ARRAY_WEIGHTS] if bones.is_empty(): return "" var verts: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX] var per := bones.size() / maxi(1, verts.size()) for v in verts.size(): var best := -1 var best_w := 0.0 for k in per: var w := weights[v * per + k] if w > best_w: best_w = w best = bones[v * per + k] if best >= 0 and best_w > 0.25: var bi: int = bone_of.get(best, -1) if bi >= 0: var nm := skel.get_bone_name(bi) tally[nm] = tally.get(nm, 0) + 1 var names: Array = tally.keys() names.sort_custom(func(a, b): return tally[a] > tally[b]) var out: PackedStringArray = [] for i in mini(5, names.size()): out.append("%s:%d" % [names[i], tally[names[i]]]) return ", ".join(out)