240 lines
7.7 KiB
Python
240 lines
7.7 KiB
Python
"""Generate the pinned Blender 5.2 single-Mesh OBJ fixture for M12-07A."""
|
|
|
|
import hashlib
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import bpy
|
|
|
|
|
|
OBJ_NAME = "M12 OBJ Single Mesh"
|
|
MATERIAL_NAMES = ("M12 OBJ Red", "M12 OBJ Blue")
|
|
|
|
|
|
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 text(value):
|
|
return value.decode("utf-8") if isinstance(value, bytes) else value
|
|
|
|
|
|
def runtime_identity():
|
|
binary = Path(bpy.app.binary_path)
|
|
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 create_fixture():
|
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
|
mesh = bpy.data.meshes.new(OBJ_NAME + " Mesh")
|
|
mesh.from_pydata(
|
|
[(-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), (0, 2, 3)],
|
|
)
|
|
mesh.update()
|
|
obj = bpy.data.objects.new(OBJ_NAME, mesh)
|
|
bpy.context.scene.collection.objects.link(obj)
|
|
|
|
uv_layer = mesh.uv_layers.new(name="UVMap")
|
|
uv_by_vertex = ((0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0))
|
|
for loop in mesh.loops:
|
|
uv_layer.data[loop.index].uv = uv_by_vertex[loop.vertex_index]
|
|
|
|
colors = ((0.8, 0.1, 0.05, 1.0), (0.05, 0.2, 0.85, 1.0))
|
|
for name, color in zip(MATERIAL_NAMES, colors):
|
|
material = bpy.data.materials.new(name)
|
|
material.diffuse_color = color
|
|
material.metallic = 0.0
|
|
material.roughness = 0.5
|
|
mesh.materials.append(material)
|
|
mesh.polygons[0].material_index = 0
|
|
mesh.polygons[1].material_index = 1
|
|
for polygon in mesh.polygons:
|
|
polygon.use_smooth = False
|
|
|
|
obj.select_set(True)
|
|
bpy.context.view_layer.objects.active = obj
|
|
return obj
|
|
|
|
|
|
def export_obj(output_path):
|
|
result = bpy.ops.wm.obj_export(
|
|
filepath=str(output_path),
|
|
export_selected_objects=True,
|
|
apply_modifiers=False,
|
|
apply_transform=False,
|
|
export_eval_mode="DAG_EVAL_VIEWPORT",
|
|
export_uv=True,
|
|
export_normals=True,
|
|
export_colors=False,
|
|
export_materials=True,
|
|
export_pbr_extensions=False,
|
|
export_material_groups=True,
|
|
export_object_groups=False,
|
|
export_vertex_groups=False,
|
|
export_smooth_groups=False,
|
|
export_triangulated_mesh=False,
|
|
export_curves_as_nurbs=False,
|
|
global_scale=1.0,
|
|
forward_axis="NEGATIVE_Z",
|
|
up_axis="Y",
|
|
path_mode="RELATIVE",
|
|
)
|
|
if "FINISHED" not in result:
|
|
raise RuntimeError("Blender OBJ export did not finish: %s" % (result,))
|
|
|
|
|
|
def number(value):
|
|
parsed = float(value)
|
|
return 0.0 if parsed == 0.0 else parsed
|
|
|
|
|
|
def parse_obj(path):
|
|
positions = []
|
|
texcoords = []
|
|
normals = []
|
|
faces = []
|
|
material_libraries = []
|
|
objects = []
|
|
current_material = None
|
|
current_groups = []
|
|
for raw_line in path.read_text(encoding="utf-8").splitlines():
|
|
line = raw_line.strip()
|
|
if not line or line.startswith("#"):
|
|
continue
|
|
parts = line.split()
|
|
kind = parts[0]
|
|
if kind == "v":
|
|
positions.append([number(value) for value in parts[1:4]])
|
|
elif kind == "vt":
|
|
texcoords.append([number(value) for value in parts[1:3]])
|
|
elif kind == "vn":
|
|
normals.append([number(value) for value in parts[1:4]])
|
|
elif kind == "mtllib":
|
|
material_libraries.extend(parts[1:])
|
|
elif kind == "o":
|
|
objects.append(" ".join(parts[1:]))
|
|
elif kind == "g":
|
|
current_groups = parts[1:]
|
|
elif kind == "usemtl":
|
|
current_material = " ".join(parts[1:])
|
|
elif kind == "f":
|
|
vertices = []
|
|
for token in parts[1:]:
|
|
indices = token.split("/")
|
|
vertices.append({
|
|
"position": int(indices[0]),
|
|
"texcoord": int(indices[1]) if len(indices) > 1 and indices[1] else None,
|
|
"normal": int(indices[2]) if len(indices) > 2 and indices[2] else None,
|
|
})
|
|
faces.append({"vertices": vertices, "material": current_material, "groups": list(current_groups)})
|
|
return {
|
|
"materialLibraries": material_libraries,
|
|
"objects": objects,
|
|
"positions": positions,
|
|
"texcoords": texcoords,
|
|
"normals": normals,
|
|
"faces": faces,
|
|
}
|
|
|
|
|
|
def parse_mtl(path):
|
|
materials = []
|
|
current = None
|
|
for raw_line in path.read_text(encoding="utf-8").splitlines():
|
|
line = raw_line.strip()
|
|
if not line or line.startswith("#"):
|
|
continue
|
|
parts = line.split()
|
|
if parts[0] == "newmtl":
|
|
current = {"name": " ".join(parts[1:]), "properties": {}}
|
|
materials.append(current)
|
|
elif current is not None:
|
|
values = parts[1:]
|
|
current["properties"][parts[0]] = [number(value) for value in values] if all_value_numbers(values) else " ".join(values)
|
|
return materials
|
|
|
|
|
|
def all_value_numbers(values):
|
|
if not values:
|
|
return False
|
|
try:
|
|
for value in values:
|
|
float(value)
|
|
return True
|
|
except ValueError:
|
|
return False
|
|
|
|
|
|
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)
|
|
obj_path = output_dir / "single-mesh.obj"
|
|
create_fixture()
|
|
export_obj(obj_path)
|
|
mtl_path = obj_path.with_suffix(".mtl")
|
|
if not mtl_path.exists():
|
|
raise RuntimeError("Blender OBJ export did not write the MTL sidecar")
|
|
semantic = parse_obj(obj_path)
|
|
semantic["materials"] = parse_mtl(mtl_path)
|
|
report = {
|
|
"schemaVersion": 1,
|
|
"task": "M12-07A",
|
|
"operation": "DESKTOP_OBJ_SINGLE_MESH_FIXTURE",
|
|
"runtime": runtime_identity(),
|
|
"sourceAnchor": "blender-5.2.0/source/blender/io/wavefront_obj",
|
|
"operator": "wm.obj_export",
|
|
"settings": {
|
|
"forwardAxis": "NEGATIVE_Z",
|
|
"upAxis": "Y",
|
|
"globalScale": 1.0,
|
|
"exportUV": True,
|
|
"exportNormals": True,
|
|
"exportMaterials": True,
|
|
"exportMaterialGroups": True,
|
|
},
|
|
"files": [
|
|
{"name": obj_path.name, "byteLength": obj_path.stat().st_size, "sha256": sha256_file(obj_path)},
|
|
{"name": mtl_path.name, "byteLength": mtl_path.stat().st_size, "sha256": sha256_file(mtl_path)},
|
|
],
|
|
"semantic": semantic,
|
|
"nextTask": "M12-07B",
|
|
}
|
|
report_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
print(
|
|
"obj-single-mesh-fixture-generated positions=%s texcoords=%s normals=%s faces=%s materials=%s next=%s"
|
|
% (
|
|
len(semantic["positions"]),
|
|
len(semantic["texcoords"]),
|
|
len(semantic["normals"]),
|
|
len(semantic["faces"]),
|
|
len(semantic["materials"]),
|
|
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-obj-single-mesh-fixture.py -- OUTPUT_DIR REPORT")
|
|
main(args[0], args[1])
|