Files
workinf_Blender_Wasm/tools/web/generate-glb-desktop-fixtures.py
mes123456 380cbed4ff
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled
Checkpoint web parity through Chromium input tasks
2026-08-19 10:39:03 -04:00

456 lines
16 KiB
Python

"""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("<II", payload, 4)
if version != 2 or total_length != len(payload):
raise RuntimeError("invalid GLB version/length: %s" % path)
offset = 12
chunks = {}
while offset < len(payload):
length, chunk_type = struct.unpack_from("<II", payload, offset)
start = offset + 8
chunks[chunk_type] = payload[start : start + length]
offset = start + length
document = json.loads(chunks[0x4E4F534A].rstrip(b" \x00").decode("utf-8"))
return payload, document
def accessor_summary(document, index):
if index is None:
return None
accessor = document.get("accessors", [])[index]
return {
"componentType": accessor["componentType"],
"count": accessor["count"],
"type": accessor["type"],
"normalized": bool(accessor.get("normalized", False)),
"min": accessor.get("min"),
"max": accessor.get("max"),
}
def semantic_summary(document):
meshes = []
for mesh in document.get("meshes", []):
primitives = []
for primitive in mesh.get("primitives", []):
primitives.append(
{
"attributes": {
name: accessor_summary(document, index)
for name, index in sorted(primitive.get("attributes", {}).items())
},
"indices": accessor_summary(document, primitive.get("indices")),
"material": primitive.get("material"),
"mode": primitive.get("mode", 4),
"targets": [
{name: accessor_summary(document, index) for name, index in sorted(target.items())}
for target in primitive.get("targets", [])
],
}
)
meshes.append({"name": mesh.get("name"), "primitives": primitives})
materials = []
for material in document.get("materials", []):
pbr = material.get("pbrMetallicRoughness", {})
materials.append(
{
"name": material.get("name"),
"alphaMode": material.get("alphaMode", "OPAQUE"),
"doubleSided": bool(material.get("doubleSided", False)),
"pbr": {
"baseColorFactor": pbr.get("baseColorFactor"),
"baseColorTexture": pbr.get("baseColorTexture"),
"metallicFactor": pbr.get("metallicFactor"),
"roughnessFactor": pbr.get("roughnessFactor"),
},
"normalTexture": material.get("normalTexture"),
"emissiveFactor": material.get("emissiveFactor"),
}
)
animations = []
for animation in document.get("animations", []):
samplers = animation.get("samplers", [])
channels = animation.get("channels", [])
animations.append(
{
"name": animation.get("name"),
"samplers": [
{
"interpolation": sampler.get("interpolation", "LINEAR"),
"input": accessor_summary(document, sampler.get("input")),
"output": accessor_summary(document, sampler.get("output")),
}
for sampler in samplers
],
"channels": [
{"sampler": channel["sampler"], "target": channel["target"]} for channel in channels
],
}
)
return {
"asset": document.get("asset"),
"extensionsUsed": sorted(document.get("extensionsUsed", [])),
"extensionsRequired": sorted(document.get("extensionsRequired", [])),
"scene": document.get("scene"),
"nodeNames": [node.get("name") for node in document.get("nodes", [])],
"nodes": [
{
"name": node.get("name"),
"mesh": node.get("mesh"),
"skin": node.get("skin"),
"children": node.get("children", []),
"translation": node.get("translation"),
"rotation": node.get("rotation"),
"scale": node.get("scale"),
}
for node in document.get("nodes", [])
],
"meshes": meshes,
"materials": materials,
"textures": document.get("textures", []),
"images": document.get("images", []),
"samplers": document.get("samplers", []),
"skins": [
{
"name": skin.get("name"),
"joints": skin.get("joints", []),
"inverseBindMatrices": accessor_summary(document, skin.get("inverseBindMatrices")),
"skeleton": skin.get("skeleton"),
}
for skin in document.get("skins", [])
],
"animations": animations,
}
def generate_fixture(fixture_id, output_path):
creators = {
"mesh": create_mesh_fixture,
"pbr": create_pbr_fixture,
"uv": create_uv_fixture,
"skin": create_skin_fixture,
"animation": create_animation_fixture,
}
creators[fixture_id]()
export_selected(output_path)
payload, document = read_glb(output_path)
return {
"id": fixture_id,
"file": output_path.name,
"byteLength": len(payload),
"sha256": hashlib.sha256(payload).hexdigest(),
"semantic": semantic_summary(document),
}
def runtime_identity():
binary = Path(bpy.app.binary_path)
def text(value):
return value.decode("utf-8") if isinstance(value, bytes) else value
return {
"blenderVersion": text(bpy.app.version_string),
"versionTuple": list(bpy.app.version),
"buildDate": text(bpy.app.build_date),
"buildTime": text(bpy.app.build_time),
"buildHash": text(bpy.app.build_hash),
"buildBranch": text(bpy.app.build_branch),
"buildPlatform": text(bpy.app.build_platform),
"buildType": text(bpy.app.build_type),
"binarySha256": sha256_file(binary),
}
def main(output_dir, report_path):
output_dir = Path(output_dir).resolve()
report_path = Path(report_path).resolve()
output_dir.mkdir(parents=True, exist_ok=True)
report_path.parent.mkdir(parents=True, exist_ok=True)
fixtures = []
for fixture_id, _label, filename in FIXTURES:
fixtures.append(generate_fixture(fixture_id, output_dir / filename))
report = {
"schemaVersion": 1,
"task": "M12-06A",
"operation": "DESKTOP_GLB_FIXTURE_GENERATION",
"runtime": runtime_identity(),
"fixtureCount": len(fixtures),
"maxFixtureBytes": 512 * 1024,
"fixtures": fixtures,
"nextTask": "M12-06B",
}
report_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8")
print(
"glb-desktop-fixtures-generated "
"fixtures=%s bytes=%s next=%s"
% (len(fixtures), sum(item["byteLength"] for item in fixtures), report["nextTask"])
)
if __name__ == "__main__":
args = sys.argv[sys.argv.index("--") + 1 :] if "--" in sys.argv else []
if len(args) != 2:
raise SystemExit("usage: blender --background --python generate-glb-desktop-fixtures.py -- OUTPUT_DIR REPORT")
main(args[0], args[1])