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, ) )