import json import math import os import sys import bpy def rounded(values): return [round(float(value), 6) for value in values] def socket_value(node, name, fallback=None): if node is None or name not in node.inputs: return fallback value = node.inputs[name].default_value return round(float(value), 6) if not hasattr(value, "__len__") else rounded(value) def main(glb_path, output_path): bpy.ops.wm.read_factory_settings(use_empty=True) result = bpy.ops.import_scene.gltf(filepath=os.path.abspath(glb_path)) if "FINISHED" not in result: raise RuntimeError(f"Blender glTF importer failed: {result}") scene = bpy.context.scene scene.render.fps = 24 mesh_object = bpy.data.objects.get("RoundTripMesh") if mesh_object is None or mesh_object.type != "MESH": raise RuntimeError("RoundTripMesh was not imported") mesh = mesh_object.data mesh.calc_loop_triangles() shape_keys = {} if mesh.shape_keys is not None: basis = mesh.shape_keys.key_blocks[0] for key in mesh.shape_keys.key_blocks[1:]: shape_keys[key.name] = [ round((key.data[index].co - basis.data[index].co).length, 6) for index in range(len(key.data)) ] material = mesh.materials[0] if mesh.materials else None principled = None texture = None if material and material.node_tree: principled = next((node for node in material.node_tree.nodes if node.type == "BSDF_PRINCIPLED"), None) texture = next( (node for candidate in bpy.data.materials if candidate.node_tree for node in candidate.node_tree.nodes if node.type == "TEX_IMAGE"), None, ) def animated_location(frame): scene.frame_set(frame) return rounded(mesh_object.matrix_world.translation) armature = next((obj for obj in bpy.data.objects if obj.type == "ARMATURE"), None) skinned = bpy.data.objects.get("SkinnedMesh") weight_sums = [] if skinned is not None and skinned.type == "MESH": for vertex in skinned.data.vertices: weight_sums.append(round(sum(group.weight for group in vertex.groups), 6)) image = texture.image if texture is not None else None report = { "mesh": { "vertexCount": len(mesh.vertices), "triangleCount": len(mesh.loop_triangles), "positions": [rounded(vertex.co) for vertex in mesh.vertices], "frame1WorldTranslation": animated_location(1), "frame25WorldTranslation": animated_location(25), "shapeKeys": shape_keys, }, "material": { "name": material.name if material else None, "baseColor": rounded(material.diffuse_color) if material else [], "metallic": round(float(material.metallic), 6) if material else None, "roughness": round(float(material.roughness), 6) if material else None, "blendMethod": material.surface_render_method if material else None, "texture": { "present": texture is not None, "interpolation": texture.interpolation if texture else None, "extension": texture.extension if texture else None, "imageWidth": image.size[0] if image else 0, "imageHeight": image.size[1] if image else 0, "packed": bool(image and image.packed_file), "colorSpace": image.colorspace_settings.name if image else None, }, "principled": { "baseColor": rounded(principled.inputs["Base Color"].default_value) if principled else [], "alpha": round(float(principled.inputs["Alpha"].default_value), 6) if principled else None, "metallic": round(float(principled.inputs["Metallic"].default_value), 6) if principled else None, "roughness": round(float(principled.inputs["Roughness"].default_value), 6) if principled else None, "ior": socket_value(principled, "IOR"), "specularIORLevel": socket_value(principled, "Specular IOR Level"), "transmissionWeight": socket_value(principled, "Transmission Weight"), "coatWeight": socket_value(principled, "Coat Weight"), "coatRoughness": socket_value(principled, "Coat Roughness"), "emissionStrength": socket_value(principled, "Emission Strength"), }, "nodes": [ { "type": node.type, "name": node.name, "inputs": { socket.name: rounded(socket.default_value) if hasattr(socket.default_value, "__len__") else round(float(socket.default_value), 6) for socket in node.inputs if hasattr(socket, "default_value") }, } for node in material.node_tree.nodes ] if material and material.node_tree else [], }, "animation": { "actionCount": len(bpy.data.actions), "frameRange": rounded([scene.frame_start, scene.frame_end]), }, "skin": { "armatureCount": sum(1 for obj in bpy.data.objects if obj.type == "ARMATURE"), "bones": sorted(bone.name for bone in armature.data.bones) if armature else [], "boneParents": { bone.name: bone.parent.name if bone.parent else None for bone in armature.data.bones } if armature else {}, "modifierCount": len(skinned.modifiers) if skinned else 0, "vertexGroupCount": len(skinned.vertex_groups) if skinned else 0, "weightSums": weight_sums, }, } with open(output_path, "w", encoding="utf-8") as handle: json.dump(report, handle, indent=2, sort_keys=True) handle.write("\n") print( "blender-glb-import-ok " f"vertices={report['mesh']['vertexCount']} bones={len(report['skin']['bones'])} " f"actions={report['animation']['actionCount']}") if __name__ == "__main__": arguments = sys.argv[sys.argv.index("--") + 1:] if len(arguments) != 2: raise SystemExit("usage: blender -b --python blender-check-glb-roundtrip.py -- input.glb output.json") main(arguments[0], arguments[1])