3823a28d1c
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
172 lines
6.4 KiB
Python
172 lines
6.4 KiB
Python
"""Decima (simplifica) una malla 3D GLB/OBJ/PLY conservando su apariencia.
|
|
|
|
Post-proceso de las mallas densas que produce el pipeline Hunyuan3D de ComfyUI
|
|
(60 MB / ~1.67 M caras). Suelda los vertices duplicados ANTES del collapse —
|
|
las mallas de VoxelToMeshBasic son "cube-soup" (4 vertices nuevos por cara, sin
|
|
compartir aristas) y el quadric edge collapse no las decima si no se sueldan
|
|
primero. Luego aplica meshing_decimation_quadric_edge_collapse (pymeshlab), el
|
|
mismo filtro que el FaceReducer del wrapper Hy3D usa internamente.
|
|
|
|
Conserva la apariencia segun el tipo del original:
|
|
- Vertex colors (ColorVisuals, lo habitual en el shape stage de Hunyuan3D):
|
|
pymeshlab los interpola a traves del collapse y se vuelven a adjuntar.
|
|
- Textura (TextureVisuals con UV + baseColorTexture): reproyecta las UV por
|
|
vertice mas cercano (scipy cKDTree, sin rtree) y readjunta la imagen.
|
|
- Sin apariencia: simplifica la geometria igual.
|
|
|
|
Impura: lee y escribe archivos en disco. Requiere trimesh + pymeshlab + scipy.
|
|
"""
|
|
import os
|
|
|
|
import numpy as np
|
|
|
|
|
|
def _err(in_faces, in_mb, msg):
|
|
return {
|
|
"ok": False, "in_faces": in_faces, "out_faces": 0,
|
|
"in_mb": in_mb, "out_mb": 0.0, "out_path": "",
|
|
"welded_faces": 0, "decimate_status": "", "appearance": "",
|
|
"error": msg,
|
|
}
|
|
|
|
|
|
def _load_mesh(path):
|
|
import trimesh
|
|
|
|
obj = trimesh.load(path, process=False)
|
|
if isinstance(obj, trimesh.Scene):
|
|
obj = trimesh.util.concatenate(list(obj.geometry.values()))
|
|
return obj
|
|
|
|
|
|
def _detect_appearance(mesh):
|
|
"""Devuelve (kind, attr, image): 'vertex_color'|'texture'|'none'."""
|
|
import trimesh
|
|
|
|
vis = mesh.visual
|
|
if isinstance(vis, trimesh.visual.texture.TextureVisuals) and vis.uv is not None:
|
|
img = getattr(getattr(vis, "material", None), "baseColorTexture", None)
|
|
if img is not None:
|
|
return "texture", np.asarray(vis.uv, dtype=np.float64), img
|
|
if isinstance(vis, trimesh.visual.color.ColorVisuals):
|
|
vc = vis.vertex_colors
|
|
if vc is not None and len(vc) == len(mesh.vertices):
|
|
return "vertex_color", np.asarray(vc, dtype=np.uint8), None
|
|
return "none", None, None
|
|
|
|
|
|
def comfyui_simplify_mesh(
|
|
in_path: str,
|
|
*,
|
|
target_faces: int = 80000,
|
|
weld: bool = True,
|
|
out_path: str | None = None,
|
|
) -> dict:
|
|
"""Decima una malla GLB/OBJ/PLY a target_faces conservando su apariencia.
|
|
|
|
Args:
|
|
in_path: ruta de la malla de entrada (.glb/.obj/.ply/.gltf/.stl/.off).
|
|
target_faces: numero de caras objetivo tras la decimacion. keyword-only.
|
|
weld: si True (default), suelda vertices duplicados antes del collapse.
|
|
Imprescindible para las mallas cube-soup de VoxelToMeshBasic; sin el,
|
|
el collapse no reduce caras. keyword-only.
|
|
out_path: ruta de salida. Si None, escribe "<in>_simplified.glb" junto al
|
|
original. keyword-only.
|
|
|
|
Returns:
|
|
dict {ok, in_faces, out_faces, in_mb, out_mb, out_path, welded_faces,
|
|
decimate_status, appearance, error}. Si falla, ok=False y error explica.
|
|
"""
|
|
try:
|
|
import pymeshlab as ml
|
|
import trimesh
|
|
except ImportError as exc:
|
|
return _err(0, 0.0, f"falta dependencia: {exc}. Instala en el venv del "
|
|
f"registry: cd python && uv add trimesh pymeshlab scipy")
|
|
|
|
if not os.path.exists(in_path):
|
|
return _err(0, 0.0, f"no existe el archivo de entrada: {in_path!r}")
|
|
if out_path is None:
|
|
out_path = os.path.splitext(in_path)[0] + "_simplified.glb"
|
|
|
|
in_mb = round(os.path.getsize(in_path) / 1e6, 3)
|
|
try:
|
|
orig = _load_mesh(in_path)
|
|
except Exception as exc:
|
|
return _err(0, in_mb, f"no se pudo cargar la malla {in_path!r}: {exc}")
|
|
|
|
in_faces = int(len(orig.faces))
|
|
kind, attr, image = _detect_appearance(orig)
|
|
|
|
try:
|
|
ms = ml.MeshSet()
|
|
if kind == "vertex_color":
|
|
ms.add_mesh(ml.Mesh(
|
|
vertex_matrix=orig.vertices, face_matrix=orig.faces,
|
|
v_color_matrix=attr.astype(np.float64) / 255.0,
|
|
))
|
|
else:
|
|
ms.add_mesh(ml.Mesh(vertex_matrix=orig.vertices, face_matrix=orig.faces))
|
|
|
|
if weld:
|
|
ms.apply_filter("meshing_remove_duplicate_vertices")
|
|
ms.apply_filter("meshing_remove_unreferenced_vertices")
|
|
welded_faces = int(ms.current_mesh().face_number())
|
|
|
|
decimate_status = "ok"
|
|
if target_faces < welded_faces:
|
|
ms.apply_filter(
|
|
"meshing_decimation_quadric_edge_collapse",
|
|
targetfacenum=int(target_faces), qualitythr=0.3,
|
|
preserveboundary=True, boundaryweight=3.0,
|
|
preservenormal=True, preservetopology=True, autoclean=True,
|
|
)
|
|
else:
|
|
decimate_status = f"skipped_target_ge_welded({welded_faces})"
|
|
|
|
cm = ms.current_mesh()
|
|
dec = trimesh.Trimesh(
|
|
vertices=cm.vertex_matrix(), faces=cm.face_matrix(), process=False,
|
|
)
|
|
|
|
appearance = kind
|
|
if kind == "vertex_color" and cm.has_vertex_color():
|
|
cols = np.clip(cm.vertex_color_matrix() * 255.0, 0, 255).astype(np.uint8)
|
|
dec.visual = trimesh.visual.color.ColorVisuals(mesh=dec, vertex_colors=cols)
|
|
elif kind == "texture":
|
|
from scipy.spatial import cKDTree
|
|
|
|
_, idx = cKDTree(orig.vertices).query(dec.vertices)
|
|
dec.visual = trimesh.visual.texture.TextureVisuals(uv=attr[idx], image=image)
|
|
|
|
parent = os.path.dirname(out_path)
|
|
if parent:
|
|
os.makedirs(parent, exist_ok=True)
|
|
dec.export(out_path)
|
|
except Exception as exc:
|
|
return _err(in_faces, in_mb, f"fallo al decimar/exportar: {type(exc).__name__}: {exc}")
|
|
|
|
return {
|
|
"ok": True,
|
|
"in_faces": in_faces,
|
|
"out_faces": int(len(dec.faces)),
|
|
"in_mb": in_mb,
|
|
"out_mb": round(os.path.getsize(out_path) / 1e6, 3),
|
|
"out_path": out_path,
|
|
"welded_faces": welded_faces,
|
|
"decimate_status": decimate_status,
|
|
"appearance": appearance,
|
|
"error": "",
|
|
}
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import json
|
|
import sys
|
|
|
|
src = sys.argv[1] if len(sys.argv) > 1 else (
|
|
os.path.expanduser("~/ComfyUI/output/character_3d_00001_.glb"))
|
|
tgt = int(sys.argv[2]) if len(sys.argv) > 2 else 80000
|
|
out = sys.argv[3] if len(sys.argv) > 3 else None
|
|
print(json.dumps(comfyui_simplify_mesh(src, target_faces=tgt, out_path=out), indent=2))
|