feat(characters): ship lena_leafbikini_base_v01 as female Ariki default
Lena with the leaf bikini sculpted into the mesh — a separate character descending from her own Tripo original, not a lena_base_v02. Full-res (1,029,360 v) at 1.777 m, now Ariki_Female_QuatSkin.glb + LOD1. First nearest-surface graft in the rig lane: AccuRig had to be fed a 20:1 decimated bait, so the index-exact graft the two earlier ships used was impossible. The bait recovers AccuRig's 9.346 mm offset in closed form, which makes it a required input forever — copied into the ship folder, and rig-work's "disposable" rule amended to say so. Mesh moved 0 mm, 0 unweighted verts. lena_base_v01 goes superseded, not rolled-back: she lost the default slot but Ariki_Female_QuatSkin_Nude.glb is untouched, so per rule 9 her folder stays put. Also lands the leaf-cut lane (work/lena_leafbikini): hue-keyed seam detection that removes the leaf shell and leaves both holes open by request, registered as a lane milestone with its sha256. Corrects the rig-work trap note — baits do carry embedded textures; it is AccuRig that strips them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
# lena_leafbikini lane, stage 03: look at her. Textured AND clay, full and close.
|
||||
#
|
||||
# blender --background --factory-startup --python 03_render_leaves.py -- <mesh.glb|.blend> <outdir> [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)
|
||||
Reference in New Issue
Block a user