import bpy, bmesh, json, sys, os, math # --- parse args: blender -b -P generate_book.py -- --spec spec.json --out out_dir argv = sys.argv argv = argv[argv.index("--")+1:] if "--" in argv else [] args = { argv[i].lstrip("-"): argv[i+1] for i in range(0, len(argv), 2) } spec_path = args.get("spec"); out_dir = args.get("out", os.getcwd()) with open(spec_path, "r") as f: spec = json.load(f) name = spec.get("name", "Book") sx, sy, sz = (spec["size_m"]["x"], spec["size_m"]["y"], spec["size_m"]["z"]) bevel = float(spec.get("bevel_m", 0.002)) corner_caps = bool(spec.get("corner_caps", True)) strap = spec.get("strap", {"enabled": False}) # --- clean scene bpy.ops.wm.read_factory_settings(use_empty=True) # --- build low poly: start from cube (2m cube at size=1.0) mesh = bpy.data.meshes.new("book_lp") obj = bpy.data.objects.new(name, mesh) bpy.context.scene.collection.objects.link(obj) bm = bmesh.new() bmesh.ops.create_cube(bm, size=1.0) bm.to_mesh(mesh); bm.free() # scale to meters (cube at size=1 spans 2m, so use half the desired dims) obj.scale = (sx/2, sy/2, sz/2) bpy.context.view_layer.objects.active = obj bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) # bevel soft edges bev = obj.modifiers.new("Bevel","BEVEL") bev.width = bevel bev.segments = 2 bev.limit_method = 'ANGLE' bev.angle_limit = math.radians(60) # simple spine loopcut (visual separation — minimal) bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.loopcut_slide(MESH_OT_loopcut={"number_cuts":1}, TRANSFORM_OT_edge_slide={"value":0.0}) bpy.ops.object.mode_set(mode='OBJECT') # optional strap if strap.get("enabled", False): w = float(strap.get("width_m", 0.025)) bpy.ops.mesh.primitive_cube_add(size=1) s = bpy.context.active_object; s.name = "strap" s.scale = (sx*0.55, w/2, sz/2*1.05) s.location = (0, 0, 0) bool_mod = obj.modifiers.new("StrapUnion","BOOLEAN") bool_mod.operation = 'UNION'; bool_mod.object = s bpy.context.view_layer.objects.active = obj bpy.ops.object.modifier_apply(modifier=bool_mod.name) bpy.data.objects.remove(s, do_unlink=True) # corner caps (simple unions) if corner_caps: cap_size = min(sx, sy, sz) * 0.12 for sxm in (-1, 1): for sym in (-1, 1): bpy.ops.mesh.primitive_cube_add(size=cap_size) c = bpy.context.active_object c.location = (sxm*(sx*0.5 - cap_size*0.35), sym*(sy*0.5 - cap_size*0.35), 0) bool_mod = obj.modifiers.new("CapUnion","BOOLEAN") bool_mod.operation = 'UNION'; bool_mod.object = c bpy.context.view_layer.objects.active = obj bpy.ops.object.modifier_apply(modifier=bool_mod.name) bpy.data.objects.remove(c, do_unlink=True) # apply bevel last bpy.context.view_layer.objects.active = obj bpy.ops.object.modifier_apply(modifier=bev.name) # shading + autosmooth obj.data.use_auto_smooth = True bpy.ops.object.shade_smooth() # UV unwrap (keep simple for now) bpy.ops.object.mode_set(mode='EDIT') bpy.ops.uv.smart_project(angle_limit=66, island_margin=0.02) bpy.ops.object.mode_set(mode='OBJECT') # simple PBR material mat = bpy.data.materials.new("MAT_Book") mat.use_nodes = True nt = mat.node_tree bsdf = nt.nodes.get("Principled BSDF") base = nt.nodes.new("ShaderNodeRGB") leather = str(spec.get("style", {}).get("leather", "dark_brown")) base.outputs[0].default_value = (0.12, 0.06, 0.03, 1.0) if "brown" in leather else (0.2, 0.2, 0.2, 1.0) nt.links.new(base.outputs["Color"], bsdf.inputs["Base Color"]) bsdf.inputs["Roughness"].default_value = 0.6 bsdf.inputs["Metallic"].default_value = 0.0 if len(obj.data.materials) == 0: obj.data.materials.append(mat) else: obj.data.materials[0] = mat # export glTF os.makedirs(out_dir, exist_ok=True) export_path = os.path.join(out_dir, f"{name}.glb") bpy.ops.export_scene.gltf(filepath=export_path, use_selection=False, export_format='GLB') print("Exported:", export_path)