Create generate_book.py
Browse files- generate_book.py +108 -0
generate_book.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import bpy, bmesh, json, sys, os, math
|
| 2 |
+
|
| 3 |
+
# -------- parse args: blender -b -P generate_book.py -- --spec spec.json --out out_dir
|
| 4 |
+
argv = sys.argv
|
| 5 |
+
argv = argv[argv.index("--")+1:] if "--" in argv else []
|
| 6 |
+
args = { argv[i].lstrip("-"): argv[i+1] for i in range(0, len(argv), 2) }
|
| 7 |
+
spec_path = args.get("spec"); out_dir = args.get("out", os.getcwd())
|
| 8 |
+
|
| 9 |
+
with open(spec_path, "r") as f:
|
| 10 |
+
spec = json.load(f)
|
| 11 |
+
|
| 12 |
+
name = spec.get("name","Book")
|
| 13 |
+
sx, sy, sz = (spec["size_m"]["x"], spec["size_m"]["y"], spec["size_m"]["z"])
|
| 14 |
+
bevel = spec.get("bevel_m", 0.002)
|
| 15 |
+
corner_caps = spec.get("corner_caps", True)
|
| 16 |
+
strap = spec.get("strap", {"enabled": False})
|
| 17 |
+
|
| 18 |
+
# -------- clean scene
|
| 19 |
+
bpy.ops.wm.read_factory_settings(use_empty=True)
|
| 20 |
+
|
| 21 |
+
# -------- build low poly book as a cube
|
| 22 |
+
mesh = bpy.data.meshes.new("book_lp")
|
| 23 |
+
obj = bpy.data.objects.new(name, mesh)
|
| 24 |
+
bpy.context.scene.collection.objects.link(obj)
|
| 25 |
+
bm = bmesh.new()
|
| 26 |
+
bmesh.ops.create_cube(bm, size=1.0) # 2m cube
|
| 27 |
+
bm.to_mesh(mesh); bm.free()
|
| 28 |
+
|
| 29 |
+
# scale to meters (cube size=2 → scale by half of desired dims)
|
| 30 |
+
obj.scale = (sx/2, sy/2, sz/2)
|
| 31 |
+
bpy.context.view_layer.objects.active = obj
|
| 32 |
+
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
|
| 33 |
+
|
| 34 |
+
# bevel modifier for soft edges
|
| 35 |
+
bev = obj.modifiers.new("Bevel","BEVEL")
|
| 36 |
+
bev.width = bevel
|
| 37 |
+
bev.segments = 2
|
| 38 |
+
bev.limit_method = 'ANGLE'
|
| 39 |
+
bev.angle_limit = math.radians(60)
|
| 40 |
+
|
| 41 |
+
# simple spine loopcut (visual separation only; minimalistic)
|
| 42 |
+
bpy.ops.object.mode_set(mode='EDIT')
|
| 43 |
+
bpy.ops.mesh.loopcut_slide(MESH_OT_loopcut={"number_cuts":1}, TRANSFORM_OT_edge_slide={"value":0.0})
|
| 44 |
+
bpy.ops.object.mode_set(mode='OBJECT')
|
| 45 |
+
|
| 46 |
+
# optional strap
|
| 47 |
+
if strap.get("enabled", False):
|
| 48 |
+
w = strap.get("width_m", 0.025)
|
| 49 |
+
bpy.ops.mesh.primitive_cube_add(size=1)
|
| 50 |
+
s = bpy.context.active_object; s.name = "strap"
|
| 51 |
+
s.scale = (sx*0.55, w/2, sz/2*1.05)
|
| 52 |
+
s.location = (0, 0, 0)
|
| 53 |
+
bool_mod = obj.modifiers.new("StrapUnion","BOOLEAN")
|
| 54 |
+
bool_mod.operation = 'UNION'; bool_mod.object = s
|
| 55 |
+
bpy.context.view_layer.objects.active = obj
|
| 56 |
+
bpy.ops.object.modifier_apply(modifier=bool_mod.name)
|
| 57 |
+
bpy.data.objects.remove(s, do_unlink=True)
|
| 58 |
+
|
| 59 |
+
# corner caps (simple unions)
|
| 60 |
+
if corner_caps:
|
| 61 |
+
cap_size = min(sx, sy, sz) * 0.12
|
| 62 |
+
for sxm in (-1, 1):
|
| 63 |
+
for sym in (-1, 1):
|
| 64 |
+
bpy.ops.mesh.primitive_cube_add(size=cap_size)
|
| 65 |
+
c = bpy.context.active_object
|
| 66 |
+
c.location = (sxm*(sx*0.5 - cap_size*0.35),
|
| 67 |
+
sym*(sy*0.5 - cap_size*0.35),
|
| 68 |
+
0)
|
| 69 |
+
bool_mod = obj.modifiers.new("CapUnion","BOOLEAN")
|
| 70 |
+
bool_mod.operation = 'UNION'; bool_mod.object = c
|
| 71 |
+
bpy.context.view_layer.objects.active = obj
|
| 72 |
+
bpy.ops.object.modifier_apply(modifier=bool_mod.name)
|
| 73 |
+
bpy.data.objects.remove(c, do_unlink=True)
|
| 74 |
+
|
| 75 |
+
# apply bevel last
|
| 76 |
+
bpy.context.view_layer.objects.active = obj
|
| 77 |
+
bpy.ops.object.modifier_apply(modifier=bev.name)
|
| 78 |
+
|
| 79 |
+
# shading + autosmooth
|
| 80 |
+
obj.data.use_auto_smooth = True
|
| 81 |
+
bpy.ops.object.shade_smooth()
|
| 82 |
+
|
| 83 |
+
# basic UV unwrap (smart project to keep it simple for now)
|
| 84 |
+
bpy.ops.object.mode_set(mode='EDIT')
|
| 85 |
+
bpy.ops.uv.smart_project(angle_limit=66, island_margin=0.02)
|
| 86 |
+
bpy.ops.object.mode_set(mode='OBJECT')
|
| 87 |
+
|
| 88 |
+
# simple PBR material (leather)
|
| 89 |
+
mat = bpy.data.materials.new("MAT_Book")
|
| 90 |
+
mat.use_nodes = True
|
| 91 |
+
nt = mat.node_tree
|
| 92 |
+
bsdf = nt.nodes.get("Principled BSDF")
|
| 93 |
+
base = nt.nodes.new("ShaderNodeRGB")
|
| 94 |
+
leather = spec.get("style", {}).get("leather", "dark_brown")
|
| 95 |
+
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)
|
| 96 |
+
nt.links.new(base.outputs["Color"], bsdf.inputs["Base Color"])
|
| 97 |
+
bsdf.inputs["Roughness"].default_value = 0.6
|
| 98 |
+
bsdf.inputs["Metallic"].default_value = 0.0
|
| 99 |
+
if len(obj.data.materials) == 0:
|
| 100 |
+
obj.data.materials.append(mat)
|
| 101 |
+
else:
|
| 102 |
+
obj.data.materials[0] = mat
|
| 103 |
+
|
| 104 |
+
# export glTF
|
| 105 |
+
os.makedirs(out_dir, exist_ok=True)
|
| 106 |
+
export_path = os.path.join(out_dir, f"{name}.glb")
|
| 107 |
+
bpy.ops.export_scene.gltf(filepath=export_path, use_selection=False, export_format='GLB')
|
| 108 |
+
print("Exported:", export_path)
|