component-studio / scripts /remove_internal_character_shells.py
mantrakp's picture
Integrate isolated Astra refinement, learned rigging and native Kimodo animation
381de33 verified
Raw History Blame Contribute Delete
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,
)
)