Spaces:
Paused
Paused
Download scripts/remove_internal_character_shells.py from mantrakp/component-studio: direct link, hf CLI and curl.
- Browser
- Download file 3.37 kB
-
https://huggingface.co/spaces/mantrakp/component-studio/resolve/main/scripts/remove_internal_character_shells.py
- Command line
-
hf download hf://spaces/mantrakp/component-studio/scripts/remove_internal_character_shells.py
-
curl -L -o remove_internal_character_shells.py https://huggingface.co/spaces/mantrakp/component-studio/resolve/main/scripts/remove_internal_character_shells.py
3.37 kB
| import bpy | |
| import numpy as np | |
| from mathutils import Vector | |
| from mathutils.bvhtree import BVHTree | |
| import argparse | |
| import sys | |
| import json | |
| import bmesh | |
| from pathlib import Path | |
| p = argparse.ArgumentParser() | |
| p.add_argument("--input", required=True) | |
| p.add_argument("--output", required=True) | |
| a = p.parse_args(sys.argv[sys.argv.index("--") + 1 :]) | |
| out = Path(a.output) | |
| out.mkdir(parents=True, exist_ok=True) | |
| bpy.ops.object.select_all(action="SELECT") | |
| bpy.ops.object.delete(use_global=False) | |
| bpy.ops.import_scene.gltf(filepath=str(Path(a.input).resolve())) | |
| for obj in bpy.context.scene.objects: | |
| if obj.type == "MESH": | |
| bpy.context.view_layer.objects.active = obj | |
| obj.select_set(True) | |
| bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) | |
| obj.select_set(False) | |
| objs = [o for o in bpy.context.scene.objects if o.type == "MESH"] | |
| v = np.array([tuple(v.co) for o in objs for v in o.data.vertices]) | |
| u, inv = np.unique(np.round(v / 1e-5).astype(int), axis=0, return_inverse=True) | |
| u = u * 1e-5 | |
| adj = [set() for _ in u] | |
| off = 0 | |
| faces = [] | |
| for o in objs: | |
| for p in o.data.polygons: | |
| faces.append(inv[off + np.array(p.vertices)]) | |
| for e in o.data.edges: | |
| a, b = inv[off + np.array(e.vertices)] | |
| adj[a].add(b) | |
| adj[b].add(a) | |
| off += len(o.data.vertices) | |
| r = set(range(len(u))) | |
| comps = [] | |
| while r: | |
| a = r.pop() | |
| c = [a] | |
| q = [a] | |
| while q: | |
| for b in adj[q.pop()]: | |
| if b in r: | |
| r.remove(b) | |
| q.append(b) | |
| c.append(b) | |
| comps.append(c) | |
| comps.sort(key=len, reverse=True) | |
| main = set(comps[0]) | |
| tree = BVHTree.FromPolygons(u, [f for f in faces if f[0] in main], all_triangles=True) | |
| removed = set() | |
| report = [] | |
| for c in comps[1:]: | |
| votes = [] | |
| for vertex in u[c]: | |
| hitvotes = [] | |
| for d in [(1, 0.231, 0.451), (-0.21, 1, 0.323), (0.371, 0.143, 1)]: | |
| origin = Vector(vertex) | |
| direction = Vector(d).normalized() | |
| count = 0 | |
| for i in range(50): | |
| hit, normal, index, dist = tree.ray_cast(origin, direction) | |
| if hit is None: | |
| break | |
| count += 1 | |
| origin = hit + direction * 1e-6 | |
| hitvotes.append(count % 2) | |
| votes.append(sum(hitvotes) >= 2) | |
| inside = float(np.mean(votes)) | |
| if inside == 1.0: | |
| removed.update(c) | |
| report.append({"vertices": len(c), "inside_fraction": inside, "removed": inside == 1.0}) | |
| print("REMOVING", len(removed), "internal vertices", flush=True) | |
| off = 0 | |
| for obj in objs: | |
| ids = {i for i in range(len(obj.data.vertices)) if inv[off + i] in removed} | |
| off += len(obj.data.vertices) | |
| bm = bmesh.new() | |
| bm.from_mesh(obj.data) | |
| bm.verts.ensure_lookup_table() | |
| bmesh.ops.delete(bm, geom=[bm.verts[i] for i in ids], context="VERTS") | |
| bm.to_mesh(obj.data) | |
| bm.free() | |
| bpy.ops.object.select_all(action="SELECT") | |
| bpy.ops.export_scene.gltf(filepath=str(out / "character-static.glb"), export_format="GLB", use_selection=True) | |
| (out / "internal-shell-cleanup.json").write_text( | |
| json.dumps( | |
| { | |
| "removed_vertices": len(removed), | |
| "components": report, | |
| "criterion": "Every vertex inside the largest outer shell by majority parity of three ray directions", | |
| }, | |
| indent=2, | |
| ) | |
| ) | |