# Stage 4: review renders + numeric profile gates for the hires sculpt. # blender --background --python 04_review.py -- <03_sculpted.blend> import bpy, sys, os, math, time import numpy as np from mathutils import Vector argv = sys.argv[sys.argv.index("--") + 1:] BLEND, OUT = argv[0], argv[1] os.makedirs(OUT, exist_ok=True) t0 = time.time() def log(m): print(f"[review {time.time()-t0:6.1f}s] {m}", flush=True) bpy.ops.wm.open_mainfile(filepath=BLEND) ob = max([o for o in bpy.data.objects if o.type == 'MESH'], key=lambda o: len(o.data.vertices)) me = ob.data n_v = len(me.vertices) co = np.empty(n_v * 3, dtype=np.float64) me.vertices.foreach_get("co", co) co = co.reshape(-1, 3) # ---------- numeric gates ---------- front = co[:, 1] < 0 print("\n=== PROFILE y(z) at sternum x=0 (front) ===") for z0 in np.arange(0.58, 0.78, 0.01): m = front & (np.abs(co[:, 0]) < 0.004) & (np.abs(co[:, 2] - z0) < 0.005) if m.sum(): print(f" z={z0:.2f} y={co[m,1].min():+.4f}") print("=== PROFILE y(z) at apex x=0.034 ===") for z0 in np.arange(0.58, 0.78, 0.01): m = front & (np.abs(co[:, 0] - 0.034) < 0.005) & (np.abs(co[:, 2] - z0) < 0.005) if m.sum(): print(f" z={z0:.2f} y={co[m,1].min():+.4f}") print("=== PROFILE y(x) at z=0.688 ===") for x0 in np.arange(-0.10, 0.101, 0.01): m = front & (np.abs(co[:, 0] - x0) < 0.005) & (np.abs(co[:, 2] - 0.688) < 0.006) if m.sum(): print(f" x={x0:+.2f} y={co[m,1].min():+.4f}") # ---------- renders ---------- scn = bpy.context.scene w = bpy.data.worlds.new("W") w.color = (0.22, 0.22, 0.24) scn.world = w key = bpy.data.objects.new("Key", bpy.data.lights.new("Key", 'SUN')) key.data.energy = 3.0 key.data.use_shadow = False bpy.context.collection.objects.link(key) fill = bpy.data.objects.new("Fill", bpy.data.lights.new("Fill", 'SUN')) fill.data.energy = 1.0 fill.data.use_shadow = False bpy.context.collection.objects.link(fill) cam = bpy.data.objects.new("Cam", bpy.data.cameras.new("Cam")) cam.data.lens = 85 bpy.context.collection.objects.link(cam) scn.camera = cam scn.render.engine = 'BLENDER_EEVEE' if bpy.app.version >= (4, 2) else 'BLENDER_EEVEE_NEXT' scn.render.resolution_x = scn.render.resolution_y = 1000 clay_mat = bpy.data.materials.new("Clay") clay_mat.use_nodes = True clay_mat.node_tree.nodes["Principled BSDF"].inputs["Base Color"].default_value = (0.62, 0.60, 0.58, 1) clay_mat.node_tree.nodes["Principled BSDF"].inputs["Roughness"].default_value = 0.45 orig_mats = [ms.material for ms in ob.material_slots] def shoot(tag, ctr, span, yaw_deg, clay): if clay: for ms in ob.material_slots: ms.material = clay_mat else: for ms, m in zip(ob.material_slots, orig_mats): ms.material = m yaw = math.radians(yaw_deg) dist = span * 3.0 cam.location = Vector(ctr) + Vector((math.sin(yaw) * dist, -math.cos(yaw) * dist, 0.02)) cam.rotation_euler = (Vector(ctr) - cam.location).to_track_quat('-Z', 'Y').to_euler() key.rotation_euler = (math.radians(62), 0, math.radians(35 + yaw_deg)) fill.rotation_euler = (math.radians(75), 0, math.radians(yaw_deg - 110)) scn.render.filepath = os.path.join(OUT, f"{tag}.png") bpy.ops.render.render(write_still=True) log(f"render {tag}") CHEST = (0.0, 0.0, 0.675) HIP = (0.0, 0.0, 0.53) FULL = (0.0, 0.0, 0.50) for yaw in (0, 40, 90): shoot(f"chest_clay_{yaw}", CHEST, 0.22, yaw, True) shoot(f"chest_tex_{yaw}", CHEST, 0.22, yaw, False) shoot("hip_clay_0", HIP, 0.22, 0, True) shoot("full_clay_0", FULL, 0.55, 0, True) shoot("full_clay_40", FULL, 0.55, 40, True) print("REVIEW_DONE")