3ba86b2ea8
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>
82 lines
3.0 KiB
Python
82 lines
3.0 KiB
Python
# Stage 23 (read-only): how many CONNECTED COMPONENTS does the mesh have?
|
|
# An atlas can never have fewer charts than the mesh has shells. If Tripo's mesh is a soup of
|
|
# disconnected shells, the chart soup is a symptom, not the disease — and a new atlas has to
|
|
# start by stitching the mesh.
|
|
#
|
|
# blender --background --python 23_shells.py -- <mesh.glb|blend>
|
|
import bpy, sys, time
|
|
import numpy as np
|
|
|
|
argv = sys.argv[sys.argv.index("--") + 1:]
|
|
SRC = argv[0]
|
|
t0 = time.time()
|
|
|
|
if SRC.lower().endswith(".glb"):
|
|
bpy.ops.wm.read_homefile(use_empty=True)
|
|
bpy.ops.import_scene.gltf(filepath=SRC)
|
|
else:
|
|
bpy.ops.wm.open_mainfile(filepath=SRC)
|
|
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, n_f = len(me.vertices), len(me.polygons)
|
|
print(f"mesh '{ob.name}': {n_v}v {n_f}f")
|
|
|
|
ev = np.empty(len(me.edges) * 2, dtype=np.int32); me.edges.foreach_get("vertices", ev)
|
|
ev = ev.reshape(-1, 2)
|
|
|
|
parent = np.arange(n_v, dtype=np.int64)
|
|
|
|
|
|
def find(x):
|
|
r = x
|
|
while parent[r] != r:
|
|
r = parent[r]
|
|
while parent[x] != r:
|
|
parent[x], x = r, parent[x]
|
|
return r
|
|
|
|
|
|
for a, b in ev:
|
|
ra, rb = find(a), find(b)
|
|
if ra != rb:
|
|
parent[rb] = ra
|
|
roots = np.array([find(i) for i in range(n_v)])
|
|
_, comp, sizes = np.unique(roots, return_inverse=True, return_counts=True)
|
|
print(f"\nTOPOLOGICAL SHELLS (edge-connected): {len(sizes)}")
|
|
o = np.argsort(-sizes)
|
|
print(f" largest {sizes[o[0]]} verts ({100.0*sizes[o[0]]/n_v:.1f}% of the mesh)")
|
|
print(f" shells >1000 verts: {int((sizes>1000).sum())} "
|
|
f">100: {int((sizes>100).sum())} >10: {int((sizes>10).sum())} "
|
|
f"<=10: {int((sizes<=10).sum())}")
|
|
print(f" top 15 shell sizes: {sizes[o[:15]].tolist()}")
|
|
print(f" verts outside the largest shell: {n_v - sizes[o[0]]} "
|
|
f"({100.0*(n_v-sizes[o[0]])/n_v:.2f}%)")
|
|
|
|
# how far apart are the shells really? if a small shell sits flush against the big one,
|
|
# a merge-by-distance would stitch it — report the gap.
|
|
if len(sizes) > 1:
|
|
from mathutils import Vector
|
|
from mathutils.kdtree import KDTree
|
|
co = np.empty(n_v * 3); me.vertices.foreach_get("co", co); co = co.reshape(-1, 3)
|
|
span = co.max(axis=0) - co.min(axis=0)
|
|
UNITM = 1.777 / span.max()
|
|
big = comp == comp[np.argmax(np.bincount(comp))]
|
|
bidx = np.nonzero(big)[0][::7]
|
|
kd = KDTree(len(bidx))
|
|
for j, i in enumerate(bidx):
|
|
kd.insert(Vector(co[i]), j)
|
|
kd.balance()
|
|
gaps = []
|
|
others = np.nonzero(~big)[0]
|
|
for i in others[::max(1, len(others) // 3000)]:
|
|
_, _, dist = kd.find(Vector(co[i]))
|
|
gaps.append(dist * UNITM * 1000.0)
|
|
gaps = np.array(gaps)
|
|
if len(gaps):
|
|
print(f"\n gap from off-shell verts to the main shell (real mm, sampled {len(gaps)}):")
|
|
for p in (50, 75, 90, 99):
|
|
print(f" p{p:<2d} {np.percentile(gaps,p):.3f} mm")
|
|
for t in (0.05, 0.2, 0.5, 1.0, 2.0):
|
|
print(f" within {t:4.2f} mm: {100.0*(gaps<t).mean():5.1f}%")
|
|
print("SHELLS_DONE")
|