145 lines
6.2 KiB
Python
145 lines
6.2 KiB
Python
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])
|