class_name NodeUtils extends RefCounted ## Static utility methods for node tree traversal. static func find_child_of_type(node: Node, type_name: String) -> Node: if node.get_class() == type_name: return node for child: Node in node.get_children(): var found := find_child_of_type(child, type_name) if found != null: return found return null static func find_first_mesh_with_blendshapes( node: Node, ) -> MeshInstance3D: if node is MeshInstance3D: var mi: MeshInstance3D = node as MeshInstance3D if mi.mesh != null and mi.mesh.get_blend_shape_count() > 0: return mi for child: Node in node.get_children(): var found := find_first_mesh_with_blendshapes(child) if found != null: return found return null static func find_blend_shape_index(mesh: MeshInstance3D, shape_name: String) -> int: if mesh == null or mesh.mesh == null: return -1 for i: int in range(mesh.mesh.get_blend_shape_count()): if mesh.mesh.get_blend_shape_name(i) == shape_name: return i return -1 static func find_bone_case_insensitive(skeleton: Skeleton3D, bone_name: String) -> int: var idx := skeleton.find_bone(bone_name) if idx == -1: idx = skeleton.find_bone(bone_name.to_lower()) if idx == -1: idx = skeleton.find_bone(bone_name.to_upper()) return idx static func read_vrm_morph_names(vrm_path: String) -> PackedStringArray: ## Reads original morph target names from a .vrm (GLB) file's JSON chunk. ## VRM4Godot renames blendshapes to morph_N during import, but the ## original names (e.g. Japanese MMD names) are preserved in ## mesh.primitives[].extras.targetNames inside the GLB JSON. var f := FileAccess.open(vrm_path, FileAccess.READ) if f == null: return PackedStringArray() # GLB header: magic(4) + version(4) + length(4) f.get_32() f.get_32() f.get_32() # First chunk: length(4) + type(4) + JSON data var chunk_length := f.get_32() f.get_32() var json_bytes := f.get_buffer(chunk_length) f.close() var json := JSON.new() if json.parse(json_bytes.get_string_from_utf8()) != OK: return PackedStringArray() var data: Dictionary = json.data var meshes: Array = data.get("meshes", []) for mesh_data: Variant in meshes: var primitives: Array = mesh_data.get("primitives", []) for prim: Variant in primitives: var extras: Dictionary = prim.get("extras", {}) var target_names: Array = extras.get("targetNames", []) if target_names.size() > 0: var result := PackedStringArray() for n: Variant in target_names: result.append(str(n)) return result return PackedStringArray()