Blender / generate_book.py
Percy3822's picture
Update generate_book.py
c2d7db1 verified
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)