feat: clothing lane, character sources, and DCC bridges
Bulk import of the working lanes that were living untracked on the PC. Content: - characters/ Lena/male body lanes, bakes, texture work, run logs - clothing/ garment pipeline, configs, gates, contract docs - garments/ MD-authored garment sources (.zprj/.zpac) - UAL-Lib/ Universal Animation Library 2 source (.blend/.fbx/.glb) - tools/ blender_bridge, iclone_bridge, md_bridge, tailor, glm_agent - docs/, plans/, dev/, .agents/plans/ Repo hygiene: - .gitattributes: LFS now covers .blend, .zprj, .zpac, .obj, .npy and the Reallusion .iAvatar/.ccAvatar/.ccRestore containers. Without this the ~3.8 GB in this commit would land as raw blobs. .png/.jpg are left out on purpose — ~250 are already tracked raw and converting them would rewrite every one without shrinking history. - .gitignore: exclude /accurig/ (~1 GB AccuRig program files, redistributable from Reallusion, nothing authored here) and /dev/null/ (git-lfs hook copies dropped by a `>/dev/null` redirect on Windows). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
# Stage 34: apply the chosen bust size and write the v04 master + GLB.
|
||||
#
|
||||
# blender --background --python 34_apply_bust.py -- <in.blend> <out.blend> <out.glb> <k> [review]
|
||||
#
|
||||
# k comes from 33_bust_variants.py. k=0.50 was chosen: it puts bust projection at +0.0155 H
|
||||
# against the approved concept turnaround's +0.0156 H, the only quantitative anchor available.
|
||||
# Two caveats recorded so the number is not over-trusted later:
|
||||
# * the metric SATURATES below ~0.5 (k=0.35 measures +0.0154, indistinguishable), because as the
|
||||
# forms shrink the "deepest slice" row migrates and stops tracking breast volume;
|
||||
# * the reference reading may be inflated by the T-pose arm crossing the chest in a side view,
|
||||
# which would mean the true reference bust is smaller still — so 0.5 is a floor, not a midpoint.
|
||||
# Front renders cannot tell these variants apart at all (the frontal key light flattens the chest);
|
||||
# judge from the side. Re-running at another k is seconds now that the wall is cached.
|
||||
import bpy, sys, os, math, time
|
||||
import numpy as np
|
||||
from mathutils import Vector
|
||||
|
||||
argv = sys.argv[sys.argv.index("--") + 1:]
|
||||
BLEND, OUT_BLEND, OUT_GLB, K = argv[0], argv[1], argv[2], float(argv[3])
|
||||
REVIEW = argv[4] if len(argv) > 4 else ""
|
||||
t0 = time.time()
|
||||
|
||||
Z0, Z1 = 0.620, 0.790
|
||||
Z_FADE = 0.022
|
||||
X_MAX = 0.135
|
||||
X_FADE = 0.022
|
||||
Y_FRONT = 0.010
|
||||
UNIT_MM = 1815.0
|
||||
|
||||
|
||||
def log(m):
|
||||
print(f"[v04 {time.time()-t0:6.1f}s] {m}", flush=True)
|
||||
|
||||
|
||||
def smoothstep(x):
|
||||
x = np.clip(x, 0.0, 1.0)
|
||||
return x * x * (3.0 - 2.0 * x)
|
||||
|
||||
|
||||
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)
|
||||
orig_mats = [ms.material for ms in ob.material_slots]
|
||||
co = np.empty(n_v * 3)
|
||||
me.vertices.foreach_get("co", co)
|
||||
co = co.reshape(-1, 3)
|
||||
log(f"in: {n_v}v, mats={[m.name if m else None for m in orig_mats]}")
|
||||
|
||||
CACHE = os.path.join(os.path.dirname(os.path.abspath(BLEND)), "_bust_wall_cache.npy")
|
||||
if not os.path.exists(CACHE):
|
||||
raise SystemExit(f"[v04] FATAL: wall cache missing ({CACHE}); run 33_bust_variants.py first")
|
||||
wall = np.load(CACHE)
|
||||
if wall.shape != co.shape:
|
||||
raise SystemExit(f"[v04] FATAL: cache shape {wall.shape} != mesh {co.shape} — "
|
||||
f"the cache belongs to a different mesh")
|
||||
log(f"wall cache loaded ({CACHE})")
|
||||
|
||||
W = (smoothstep((co[:, 2] - (Z0 - Z_FADE)) / Z_FADE)
|
||||
* smoothstep(((Z1 + Z_FADE) - co[:, 2]) / Z_FADE)
|
||||
* smoothstep(((X_MAX + X_FADE) - np.abs(co[:, 0])) / X_FADE)
|
||||
* smoothstep((Y_FRONT - co[:, 1]) / 0.030))
|
||||
P = co + (W * (K - 1.0))[:, None] * (co - wall)
|
||||
d = np.linalg.norm(P - co, axis=1) * UNIT_MM
|
||||
log(f"k={K}: {int((d>0.05).sum())} verts moved >0.05 mm, max {d.max():.1f} mm, "
|
||||
f"median(region) {np.median(d[W>0.5]):.2f} mm")
|
||||
|
||||
me.vertices.foreach_set("co", P.reshape(-1))
|
||||
me.update()
|
||||
vn = np.empty(n_v * 3, dtype=np.float32)
|
||||
me.vertices.foreach_get("normal", vn)
|
||||
if me.has_custom_normals:
|
||||
me.normals_split_custom_set_from_vertices(vn.reshape(-1, 3))
|
||||
|
||||
# ---- verify: bust projection + that nothing outside the chest moved ----
|
||||
def bust_projection(X):
|
||||
z0 = X[:, 2].min()
|
||||
Hh = X[:, 2].max() - z0
|
||||
u = (X[:, 2] - z0) / Hh
|
||||
NB = 220
|
||||
uu = np.linspace(0, 1, NB)
|
||||
fm = np.full(NB, np.nan)
|
||||
dm = np.full(NB, np.nan)
|
||||
for i, uc in enumerate(uu):
|
||||
sel = np.abs(u - uc) < (0.5 / NB) * 1.6
|
||||
if sel.sum() < 8:
|
||||
continue
|
||||
t = X[sel][np.abs(X[sel][:, 0]) < 0.16]
|
||||
if len(t) >= 8:
|
||||
fm[i] = t[:, 1].min() / Hh
|
||||
dm[i] = (t[:, 1].max() - t[:, 1].min()) / Hh
|
||||
b = (uu > 0.66) & (uu < 0.80) & np.isfinite(dm)
|
||||
ib = int(np.nanargmax(np.where(b, dm, -np.inf)))
|
||||
b2 = (uu > 0.60) & (uu < 0.70) & np.isfinite(dm)
|
||||
iu = int(np.nanargmin(np.where(b2, dm, np.inf)))
|
||||
return fm[iu] - fm[ib]
|
||||
|
||||
|
||||
print(f"BUST PROJECTION before {bust_projection(co):+.4f} H -> after "
|
||||
f"{bust_projection(P):+.4f} H (reference +0.0156)")
|
||||
outside = d[W <= 0.02]
|
||||
print(f"OUTSIDE THE REGION: max move {outside.max() if len(outside) else 0:.4f} mm "
|
||||
f"(should be ~0)")
|
||||
h0 = co[:, 2].max() - co[:, 2].min()
|
||||
h1 = P[:, 2].max() - P[:, 2].min()
|
||||
print(f"HEIGHT {h0:.5f} -> {h1:.5f} units ({(h1-h0)*UNIT_MM:+.3f} real mm)")
|
||||
|
||||
# restore materials, save, THEN render (the trap that broke 21_tone.py)
|
||||
for i, ms in enumerate(ob.material_slots):
|
||||
ms.material = orig_mats[i]
|
||||
bpy.ops.wm.save_as_mainfile(filepath=OUT_BLEND)
|
||||
log(f"WROTE {OUT_BLEND}")
|
||||
bpy.ops.export_scene.gltf(filepath=OUT_GLB, export_format='GLB', export_image_format='AUTO',
|
||||
export_yup=True, export_apply=False, export_animations=False,
|
||||
export_skins=False, export_morph=False)
|
||||
log(f"WROTE {OUT_GLB} ({os.path.getsize(OUT_GLB)/1e6:.2f} MB)")
|
||||
|
||||
if REVIEW:
|
||||
os.makedirs(REVIEW, exist_ok=True)
|
||||
scn = bpy.context.scene
|
||||
wd = bpy.data.worlds.new("W")
|
||||
wd.color = (0.20, 0.20, 0.21)
|
||||
scn.world = wd
|
||||
for rot, e in (((55, 0, -35), 2.2), ((120, 0, 150), 1.0), ((85, 0, 35), 0.8)):
|
||||
ld = bpy.data.lights.new("s", 'SUN')
|
||||
ld.energy = e
|
||||
ld.use_shadow = False
|
||||
lo = bpy.data.objects.new("s", ld)
|
||||
lo.rotation_euler = tuple(math.radians(a) for a in rot)
|
||||
bpy.context.collection.objects.link(lo)
|
||||
cam = bpy.data.objects.new("Cam", bpy.data.cameras.new("Cam"))
|
||||
cam.data.lens = 70
|
||||
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 = 700, 1000
|
||||
clay = bpy.data.materials.new("Clay")
|
||||
clay.use_nodes = True
|
||||
cn = clay.node_tree.nodes["Principled BSDF"]
|
||||
cn.inputs["Base Color"].default_value = (0.80, 0.56, 0.42, 1)
|
||||
cn.inputs["Roughness"].default_value = 0.55
|
||||
|
||||
def shoot(tag, yaw, span, ctr=(0.0, 0.0, 0.50), use_clay=True):
|
||||
for i, ms in enumerate(ob.material_slots):
|
||||
ms.material = clay if use_clay else orig_mats[i]
|
||||
y = math.radians(yaw)
|
||||
dist = span * 3.0
|
||||
cam.location = Vector(ctr) + Vector((math.sin(y) * dist, -math.cos(y) * dist, 0.0))
|
||||
cam.rotation_euler = (Vector(ctr) - cam.location).to_track_quat('-Z', 'Y').to_euler()
|
||||
scn.render.filepath = os.path.abspath(os.path.join(REVIEW, f"{tag}.png"))
|
||||
bpy.ops.render.render(write_still=True)
|
||||
log(f"render {tag}")
|
||||
|
||||
shoot("front_clay", 0, 0.55)
|
||||
shoot("side_clay", 90, 0.55)
|
||||
shoot("back_clay", 180, 0.55)
|
||||
shoot("front_tex", 0, 0.55, use_clay=False)
|
||||
shoot("side_tex", 90, 0.55, use_clay=False)
|
||||
|
||||
print("V04_DONE")
|
||||
Reference in New Issue
Block a user