"""Generate the bounded Blender 5.2 desktop GLB fixture group for M12-06A. The generator deliberately keeps each feature in its own file. Later import and round-trip tasks can therefore fail on one capability without hiding it behind a large all-in-one scene. """ import hashlib import json import os import struct import sys from pathlib import Path import bpy FIXTURES = ( ("mesh", "M12 Mesh Fixture", "mesh.glb"), ("pbr", "M12 PBR Fixture", "pbr.glb"), ("uv", "M12 UV Fixture", "uv.glb"), ("skin", "M12 Skin Fixture", "skin.glb"), ("animation", "M12 Animation Fixture", "animation.glb"), ) def sha256_file(path): digest = hashlib.sha256() with open(path, "rb") as handle: for chunk in iter(lambda: handle.read(1024 * 1024), b""): digest.update(chunk) return digest.hexdigest() def reset(): bpy.ops.wm.read_factory_settings(use_empty=True) scene = bpy.context.scene scene.frame_start = 1 scene.frame_end = 25 scene.render.fps = 24 scene.unit_settings.system = "METRIC" scene.unit_settings.scale_length = 1.0 return scene def mesh_object(name, vertices, faces): mesh = bpy.data.meshes.new(name + " Mesh") mesh.from_pydata(vertices, [], faces) mesh.update() obj = bpy.data.objects.new(name, mesh) bpy.context.scene.collection.objects.link(obj) return obj def select_only(objects): bpy.ops.object.select_all(action="DESELECT") for obj in objects: obj.select_set(True) bpy.context.view_layer.objects.active = objects[0] def export_selected(path): result = bpy.ops.export_scene.gltf( filepath=str(path), export_format="GLB", use_selection=True, export_apply=False, export_animations=True, export_animation_mode="ACTIONS", export_frame_range=True, export_frame_step=1, export_force_sampling=True, export_skins=True, export_all_influences=True, export_morph=True, export_morph_animation=True, export_attributes=True, export_texcoords=True, export_normals=True, export_tangents=False, export_materials="EXPORT", export_image_format="AUTO", export_cameras=False, export_lights=False, export_draco_mesh_compression_enable=False, export_meshopt_compression_enable=False, export_try_sparse_sk=False, export_try_omit_sparse_sk=False, export_current_frame=False, export_yup=True, ) if "FINISHED" not in result: raise RuntimeError("Blender GLB export did not finish: %s" % (result,)) def add_pbr_material(name, color, metallic, roughness): material = bpy.data.materials.new(name) material.use_nodes = True material.diffuse_color = (*color, 1.0) principled = material.node_tree.nodes.get("Principled BSDF") principled.inputs["Base Color"].default_value = (*color, 1.0) principled.inputs["Metallic"].default_value = metallic principled.inputs["Roughness"].default_value = roughness if principled.inputs.get("IOR"): principled.inputs["IOR"].default_value = 1.45 return material def add_uv_layer(obj): layer = obj.data.uv_layers.new(name="UVMap") values = ((0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)) for loop, value in zip(layer.data, values): loop.uv = value def add_corner_colors(obj): colors = obj.data.color_attributes.new(name="M12Color", type="FLOAT_COLOR", domain="CORNER") values = ( (1.0, 0.0, 0.0, 1.0), (0.0, 1.0, 0.0, 1.0), (0.0, 0.0, 1.0, 1.0), (1.0, 1.0, 0.0, 1.0), ) for item, value in zip(colors.data, values): item.color = value obj.data.color_attributes.active_color_index = 0 def use_corner_colors(material): vertex_color = material.node_tree.nodes.new("ShaderNodeVertexColor") vertex_color.layer_name = "M12Color" principled = material.node_tree.nodes.get("Principled BSDF") material.node_tree.links.new(vertex_color.outputs["Color"], principled.inputs["Base Color"]) def create_mesh_fixture(): reset() obj = mesh_object( "M12 Mesh Fixture", [(-1.0, -1.0, 0.0), (1.0, -1.0, 0.0), (1.0, 1.0, 0.0), (-1.0, 1.0, 0.0)], [(0, 1, 2, 3)], ) add_corner_colors(obj) material = add_pbr_material("M12 Mesh Material", (0.22, 0.48, 0.83), 0.15, 0.55) use_corner_colors(material) obj.data.materials.append(material) select_only([obj]) def create_pbr_fixture(): reset() obj = mesh_object( "M12 PBR Fixture", [(-1.0, -1.0, 0.0), (1.0, -1.0, 0.0), (1.0, 1.0, 0.0), (-1.0, 1.0, 0.0)], [(0, 1, 2, 3)], ) material = add_pbr_material("M12 PBR Material", (0.31, 0.57, 0.91), 0.72, 0.28) principled = material.node_tree.nodes.get("Principled BSDF") principled.inputs["Emission Color"].default_value = (0.02, 0.04, 0.08, 1.0) if principled.inputs.get("Emission Strength"): principled.inputs["Emission Strength"].default_value = 1.5 obj.data.materials.append(material) select_only([obj]) def create_uv_fixture(): reset() obj = mesh_object( "M12 UV Fixture", [(-1.0, -1.0, 0.0), (1.0, -1.0, 0.0), (1.0, 1.0, 0.0), (-1.0, 1.0, 0.0)], [(0, 1, 2, 3)], ) add_uv_layer(obj) material = add_pbr_material("M12 UV Material", (1.0, 1.0, 1.0), 0.0, 0.45) image = bpy.data.images.new("M12 UV Texture", width=2, height=2, alpha=True) image.colorspace_settings.name = "sRGB" image.pixels = ( 1.0, 0.1, 0.1, 1.0, 0.1, 1.0, 0.1, 1.0, 0.1, 0.1, 1.0, 1.0, 1.0, 1.0, 0.1, 1.0, ) image.pack() texture = material.node_tree.nodes.new("ShaderNodeTexImage") texture.name = "M12 UV Image Texture" texture.image = image texture.interpolation = "Closest" texture.extension = "REPEAT" principled = material.node_tree.nodes.get("Principled BSDF") material.node_tree.links.new(texture.outputs["Color"], principled.inputs["Base Color"]) obj.data.materials.append(material) select_only([obj]) def create_armature(name): armature_data = bpy.data.armatures.new(name + " Data") armature = bpy.data.objects.new(name, armature_data) bpy.context.scene.collection.objects.link(armature) bpy.context.view_layer.objects.active = armature armature.select_set(True) bpy.ops.object.mode_set(mode="EDIT") root = armature_data.edit_bones.new("Root") root.head = (0.0, 0.0, 0.0) root.tail = (0.0, 0.0, 1.0) tip = armature_data.edit_bones.new("Tip") tip.head = (0.0, 0.0, 1.0) tip.tail = (0.0, 0.0, 2.0) tip.parent = root bpy.ops.object.mode_set(mode="OBJECT") return armature def create_skin_fixture(): reset() armature = create_armature("M12 Skin Armature") obj = mesh_object( "M12 Skin Fixture", [(-1.0, -0.5, 0.0), (1.0, -0.5, 0.0), (1.0, 0.5, 0.0), (-1.0, 0.5, 0.0)], [(0, 1, 2, 3)], ) root = obj.vertex_groups.new(name="Root") tip = obj.vertex_groups.new(name="Tip") root.add([0, 3], 0.75, "REPLACE") tip.add([0, 3], 0.25, "REPLACE") root.add([1, 2], 0.2, "REPLACE") tip.add([1, 2], 0.8, "REPLACE") modifier = obj.modifiers.new(name="M12 Armature Deform", type="ARMATURE") modifier.object = armature obj.parent = armature material = add_pbr_material("M12 Skin Material", (0.76, 0.24, 0.18), 0.05, 0.5) obj.data.materials.append(material) select_only([armature, obj]) def create_animation_fixture(): reset() obj = mesh_object( "M12 Animation Fixture", [(-0.75, -0.75, 0.0), (0.75, -0.75, 0.0), (0.0, 0.75, 0.0)], [(0, 1, 2)], ) material = add_pbr_material("M12 Animation Material", (0.16, 0.72, 0.38), 0.1, 0.4) obj.data.materials.append(material) obj.location = (-1.0, 0.0, 0.0) obj.rotation_mode = "XYZ" obj.keyframe_insert(data_path="location", frame=1) obj.keyframe_insert(data_path="rotation_euler", frame=1) obj.location = (0.0, 0.5, 0.25) obj.rotation_euler[2] = 0.75 obj.keyframe_insert(data_path="location", frame=13) obj.keyframe_insert(data_path="rotation_euler", frame=13) obj.location = (1.0, 0.0, 0.0) obj.rotation_euler[2] = 1.5 obj.keyframe_insert(data_path="location", frame=25) obj.keyframe_insert(data_path="rotation_euler", frame=25) if obj.animation_data and obj.animation_data.action: obj.animation_data.action.name = "M12 Animation Action" select_only([obj]) def read_glb(path): payload = path.read_bytes() if len(payload) < 20 or payload[:4] != b"glTF": raise RuntimeError("invalid GLB header: %s" % path) version, total_length = struct.unpack_from("