# lena_leafbikini lane, stage 03: look at her. Textured AND clay, full and close. # # blender --background --factory-startup --python 03_render_leaves.py -- [tag] # # The clay pass is the whole point (same reasoning as tools/_render_body_closeup.py --clay): # a painted leaf and a sculpted leaf are indistinguishable in a textured render, and they need # completely different removal methods. Whatever survives with all materials stripped is # geometry; whatever vanishes was paint. # # Framing is in FRACTIONS OF BODY HEIGHT, not metres: the Tripo original is 0.979 units tall, # not 1.777, so tools/_render_body_closeup.py's metre bands (torso = 1.00..1.52) would frame # empty space above her head. import bpy, sys, os, math from mathutils import Vector argv = sys.argv[sys.argv.index("--") + 1:] SRC, OUT = os.path.abspath(argv[0]), os.path.abspath(argv[1]) TAG = argv[2] if len(argv) > 2 else "leaf" os.makedirs(OUT, exist_ok=True) # name -> (z_lo, z_hi as fraction of body height, yaw degrees) VIEWS = [ ("full_front", 0.00, 1.00, 0), ("full_back", 0.00, 1.00, 180), ("chest_front", 0.60, 0.82, 0), ("chest_34", 0.60, 0.82, 40), ("hip_front", 0.38, 0.60, 0), ("hip_34", 0.38, 0.60, 40), ] RES = 1000 def load(): bpy.ops.wm.read_factory_settings(use_empty=True) if SRC.lower().endswith(".blend"): bpy.ops.wm.open_mainfile(filepath=SRC) else: bpy.ops.import_scene.gltf(filepath=SRC) meshes = [o for o in bpy.data.objects if o.type == 'MESH'] body = max(meshes, key=lambda o: len(o.data.vertices)) for o in meshes: o.hide_render = o is not body return body def render(body, clay): if clay: body.data.materials.clear() m = bpy.data.materials.new("Clay") m.use_nodes = True b = m.node_tree.nodes["Principled BSDF"] b.inputs["Base Color"].default_value = (0.62, 0.60, 0.58, 1.0) b.inputs["Roughness"].default_value = 0.42 body.data.materials.append(m) pts = [body.matrix_world @ v.co for v in body.data.vertices] zmin = min(p.z for p in pts); zmax = max(p.z for p in pts) H = zmax - zmin w = bpy.data.worlds.new("W"); w.color = (0.20, 0.20, 0.22) bpy.context.scene.world = w scn = bpy.context.scene scn.render.engine = 'BLENDER_EEVEE' if bpy.app.version >= (4, 2) else 'BLENDER_EEVEE_NEXT' scn.render.resolution_x = scn.render.resolution_y = RES scn.render.film_transparent = False cam = bpy.data.objects.new("Cam", bpy.data.cameras.new("Cam")) cam.data.lens = 85 bpy.context.collection.objects.link(cam) scn.camera = cam key = bpy.data.objects.new("Key", bpy.data.lights.new("Key", 'SUN')) key.data.energy = 3.0 bpy.context.collection.objects.link(key) fill = bpy.data.objects.new("Fill", bpy.data.lights.new("Fill", 'SUN')) fill.data.energy = 1.2 bpy.context.collection.objects.link(fill) for name, f_lo, f_hi, yawdeg in VIEWS: z_lo, z_hi = zmin + f_lo * H, zmin + f_hi * H band = [p for p in pts if z_lo <= p.z <= z_hi] or pts xs = [p.x for p in band] ctr = Vector((0.0, sum(p.y for p in band) / len(band), (z_lo + z_hi) / 2)) span = max(max(xs) - min(xs), z_hi - z_lo) yaw = math.radians(yawdeg) # closeups: ignore the T-pose arm span, frame the torso width only if f_hi - f_lo < 0.5: span = min(span, (z_hi - z_lo) * 1.25) dist = span * 2.9 cam.location = ctr + Vector((math.sin(yaw) * dist, -math.cos(yaw) * dist, 0.0)) cam.rotation_euler = (ctr - cam.location).to_track_quat('-Z', 'Y').to_euler() key.rotation_euler = (math.radians(60), 0, math.radians(35 + yawdeg)) fill.rotation_euler = (math.radians(75), 0, math.radians(yawdeg - 110)) path = os.path.join(OUT, f"{TAG}_{name}_{'clay' if clay else 'tex'}.png") scn.render.filepath = path bpy.ops.render.render(write_still=True) print("RENDER_OK", path, flush=True) b = load() render(b, clay=False) b = load() render(b, clay=True)