Files

82 lines
3.0 KiB
Python
Raw Permalink Normal View History

# 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")