Checkpoint web parity through Chromium input tasks
This commit is contained in:
455
tools/web/generate-glb-desktop-fixtures.py
Normal file
455
tools/web/generate-glb-desktop-fixtures.py
Normal file
@@ -0,0 +1,455 @@
|
||||
"""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])
|
||||
Reference in New Issue
Block a user