feat(ml): auto-commit con 7 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
---
|
||||
name: comfyui_make_watertight
|
||||
kind: function
|
||||
lang: py
|
||||
domain: ml
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def comfyui_make_watertight(in_path: str, *, method: str = \"voxel\", pitch: float | None = None, out_path: str | None = None) -> dict"
|
||||
description: "Hace estanca (watertight) una malla 3D GLB/OBJ/PLY de ComfyUI/Hunyuan3D. method='voxel' (default) voxeliza el solido, rellena el interior y reconstruye con marching cubes (trimesh) -> is_watertight=True garantizado, a costa de mas caras y de descartar la apariencia; necesita scikit-image. method='repair' hace limpieza ligera (trimesh.repair fill_holes + fix_normals + fix_winding) conservando el detalle, pero no garantiza estanqueidad en mallas muy rotas. La via de raiz es generar con el nodo VoxelToMesh algorithm='surface net' (report 0088). Impura: lee y escribe en disco."
|
||||
tags: [comfyui, 3d, mesh, hunyuan3d, watertight, voxel, remesh, trimesh, ml]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: error_go_core
|
||||
imports: []
|
||||
params:
|
||||
- name: in_path
|
||||
desc: "Ruta de la malla de entrada (.glb/.obj/.ply/.gltf/.stl/.off). Scene GLB con varias geometrias se concatena en una sola malla."
|
||||
- name: method
|
||||
desc: "'voxel' (default, garantiza is_watertight=True via voxeliza+fill+marching cubes) o 'repair' (fill_holes + fix_normals, conserva detalle pero no siempre estanca). keyword-only."
|
||||
- name: pitch
|
||||
desc: "Solo para method='voxel'. Tamano de voxel absoluto. Si None, se calcula como diagonal_bbox / 200. Mas pequeno = mas caras y mas detalle, mas lento. keyword-only."
|
||||
- name: out_path
|
||||
desc: "Ruta de salida. Si None, escribe '<in>_watertight.glb' junto al original (NO sobrescribe). keyword-only."
|
||||
output: "dict {ok, was_watertight, is_watertight, out_path, method, pitch, out_faces, error}. was/is_watertight = estanqueidad antes/despues medida con trimesh.is_watertight. out_faces = caras del resultado (el voxel-remesh suele aumentarlas). Si falla, ok=False y error explica (dependencia ausente, method invalido, archivo no existe, carga o remesh)."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "python/functions/ml/comfyui_make_watertight.py"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
|
||||
from ml.comfyui_make_watertight import comfyui_make_watertight
|
||||
|
||||
# Cierra una malla decimada no estanca (80k caras) por voxel-remesh.
|
||||
res = comfyui_make_watertight(
|
||||
"/tmp/character_simplified.glb",
|
||||
method="voxel",
|
||||
out_path="/tmp/character_watertight.glb",
|
||||
)
|
||||
# res == {"ok": True, "was_watertight": False, "is_watertight": True,
|
||||
# "method": "voxel", "pitch": 0.01596, "out_faces": 250672,
|
||||
# "out_path": "/tmp/character_watertight.glb", "error": ""}
|
||||
```
|
||||
|
||||
Lanzar con el python del venv del registry (import de arriba o heredoc). `./fn run`
|
||||
directo no aplica: la firma usa `*` (keyword-only), no soportado por el runner de `fn run`.
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando una malla de ComfyUI/Hunyuan3D sale NO estanca (`is_watertight=False`, tipico
|
||||
del nodo DEPRECATED `VoxelToMeshBasic`) y la necesitas cerrada para imprimir en 3D,
|
||||
calcular volumen, boolean ops o simulacion. Usa `method="voxel"` cuando exiges
|
||||
estanqueidad garantizada (la silueta se conserva, el detalle fino se suaviza).
|
||||
Usa `method="repair"` cuando la malla solo tiene huecos pequenos y quieres conservar
|
||||
caras/detalle. Para malla ligera Y estanca, decima antes con `comfyui_simplify_mesh`
|
||||
y luego pasa el resultado por aqui con `method="voxel"`.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Impura: lee `in_path` y escribe `out_path`. Nunca sobrescribe el original salvo
|
||||
que apuntes `out_path` a el.
|
||||
- `method="voxel"` DESCARTA la apariencia (UV / vertex colors): el marching cubes
|
||||
genera geometria nueva sin atributos. Si quieres color, aplicalo despues del
|
||||
remesh, o usa la via de raiz (`VoxelToMesh surface net`) que preserva el flujo de
|
||||
texturizado.
|
||||
- `method="voxel"` necesita `scikit-image` en el venv (marching cubes). Sin el
|
||||
devuelve ok=False con la instruccion `uv add scikit-image`.
|
||||
- `method="repair"` NO garantiza `is_watertight=True`: en mallas muy rotas
|
||||
(cube-soup con muchos huecos grandes) devuelve `is_watertight=False`. Es
|
||||
esperado; para garantia usa `method="voxel"`.
|
||||
- El voxel-remesh aumenta el numero de caras (densidad del marching cubes). Si
|
||||
necesitas ligero Y estanco, vuelve a decimar el resultado con
|
||||
`comfyui_simplify_mesh` (el report 0088 logra 80k caras + estanco re-cerrando).
|
||||
- `euler_number` puede quedar negativo aunque sea estanco: indica genus alto real
|
||||
(tuneles de la geometria), no un fallo. Estanco != genus 0.
|
||||
- `pitch` muy pequeno sobre mallas grandes es lento (corre en CPU, no usa VRAM).
|
||||
@@ -0,0 +1,133 @@
|
||||
"""Hace estanca (watertight) una malla 3D GLB/OBJ/PLY.
|
||||
|
||||
Post-proceso de las mallas de ComfyUI/Hunyuan3D producidas con el nodo
|
||||
VoxelToMeshBasic (DEPRECATED), que genera mallas NO estancas (is_watertight=False):
|
||||
crea 4 vertices nuevos por cara expuesta sin soldarlos, dejando huecos y bordes
|
||||
non-manifold. Dos metodos:
|
||||
|
||||
- method="voxel" (default, garantiza is_watertight=True): voxeliza el solido,
|
||||
rellena el interior y reconstruye la superficie con marching cubes
|
||||
(trimesh voxelized(pitch).fill().marching_cubes). Produce una malla cerrada por
|
||||
construccion. Coste: mas caras (densidad del marching cubes) y descarta la
|
||||
apariencia (UV/vertex colors). Necesita scikit-image (marching cubes).
|
||||
- method="repair": limpieza ligera con trimesh.repair (fix_winding + fill_holes +
|
||||
fix_normals + merge_vertices). Conserva el detalle y las caras, pero NO garantiza
|
||||
estanqueidad en mallas muy rotas (solo cierra huecos pequenos).
|
||||
|
||||
La via de RAIZ (no este post-proceso) es generar con el nodo VoxelToMesh
|
||||
algorithm='surface net', que da malla manifold cerrada sin reparar (ver report 0088).
|
||||
|
||||
Impura: lee y escribe archivos en disco. Requiere trimesh (+ scikit-image para voxel).
|
||||
"""
|
||||
import os
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
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 comfyui_make_watertight(
|
||||
in_path: str,
|
||||
*,
|
||||
method: str = "voxel",
|
||||
pitch: float | None = None,
|
||||
out_path: str | None = None,
|
||||
) -> dict:
|
||||
"""Hace estanca una malla GLB/OBJ/PLY por voxel-remesh o reparacion.
|
||||
|
||||
Args:
|
||||
in_path: ruta de la malla de entrada (.glb/.obj/.ply/.gltf/.stl/.off).
|
||||
method: "voxel" (default, garantiza is_watertight=True via voxeliza+fill+
|
||||
marching cubes) o "repair" (fill_holes + fix_normals, conserva detalle
|
||||
pero no siempre estanca). keyword-only.
|
||||
pitch: solo para method="voxel". Tamano de voxel absoluto. Si None, se
|
||||
calcula como diagonal_bbox / 200 (mas fino = mas caras y detalle).
|
||||
keyword-only.
|
||||
out_path: ruta de salida. Si None, escribe "<in>_watertight.glb" junto al
|
||||
original. keyword-only.
|
||||
|
||||
Returns:
|
||||
dict {ok, was_watertight, is_watertight, out_path, method, pitch, out_faces,
|
||||
error}. was/is_watertight = estanqueidad antes/despues (trimesh). Si falla,
|
||||
ok=False y error explica.
|
||||
"""
|
||||
base_err = {
|
||||
"ok": False, "was_watertight": None, "is_watertight": None,
|
||||
"out_path": "", "method": method, "pitch": None, "out_faces": 0,
|
||||
}
|
||||
try:
|
||||
import trimesh
|
||||
except ImportError as exc:
|
||||
return {**base_err, "error": f"falta trimesh: {exc}. cd python && uv add trimesh"}
|
||||
|
||||
if method not in ("voxel", "repair"):
|
||||
return {**base_err, "error": f"method '{method}' invalido (usa 'voxel' o 'repair')"}
|
||||
if not os.path.exists(in_path):
|
||||
return {**base_err, "error": f"no existe el archivo de entrada: {in_path!r}"}
|
||||
if out_path is None:
|
||||
out_path = os.path.splitext(in_path)[0] + "_watertight.glb"
|
||||
|
||||
try:
|
||||
mesh = _load_mesh(in_path)
|
||||
except Exception as exc:
|
||||
return {**base_err, "error": f"no se pudo cargar la malla {in_path!r}: {exc}"}
|
||||
was = bool(mesh.is_watertight)
|
||||
|
||||
try:
|
||||
if method == "voxel":
|
||||
m = mesh.copy()
|
||||
m.merge_vertices()
|
||||
if pitch is None:
|
||||
diag = float(np.linalg.norm(m.extents))
|
||||
pitch = diag / 200.0
|
||||
vg = m.voxelized(pitch=float(pitch)).fill()
|
||||
out = vg.marching_cubes
|
||||
out.merge_vertices()
|
||||
trimesh.repair.fix_normals(out)
|
||||
else: # repair
|
||||
out = mesh.copy()
|
||||
out.merge_vertices()
|
||||
trimesh.repair.fix_winding(out)
|
||||
trimesh.repair.fill_holes(out)
|
||||
trimesh.repair.fix_normals(out)
|
||||
|
||||
parent = os.path.dirname(out_path)
|
||||
if parent:
|
||||
os.makedirs(parent, exist_ok=True)
|
||||
out.export(out_path)
|
||||
except ImportError as exc:
|
||||
return {**base_err, "was_watertight": was,
|
||||
"error": f"falta dependencia para method='{method}': {exc}. "
|
||||
f"El voxel-remesh necesita scikit-image: cd python && uv add scikit-image"}
|
||||
except Exception as exc:
|
||||
return {**base_err, "was_watertight": was,
|
||||
"error": f"fallo en method='{method}': {type(exc).__name__}: {exc}"}
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"was_watertight": was,
|
||||
"is_watertight": bool(out.is_watertight),
|
||||
"out_path": out_path,
|
||||
"method": method,
|
||||
"pitch": round(float(pitch), 6) if pitch is not None else None,
|
||||
"out_faces": int(len(out.faces)),
|
||||
"error": "",
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import json
|
||||
import sys
|
||||
|
||||
src = sys.argv[1] if len(sys.argv) > 1 else (
|
||||
os.path.expanduser("~/ComfyUI/output/3d_robot_mesh_00001__dec80k.glb"))
|
||||
method = sys.argv[2] if len(sys.argv) > 2 else "voxel"
|
||||
out = sys.argv[3] if len(sys.argv) > 3 else None
|
||||
print(json.dumps(comfyui_make_watertight(src, method=method, out_path=out), indent=2))
|
||||
@@ -0,0 +1,82 @@
|
||||
---
|
||||
name: comfyui_simplify_mesh
|
||||
kind: function
|
||||
lang: py
|
||||
domain: ml
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def comfyui_simplify_mesh(in_path: str, *, target_faces: int = 80000, weld: bool = True, out_path: str | None = None) -> dict"
|
||||
description: "Decima (simplifica) una malla 3D GLB/OBJ/PLY a target_faces conservando su apariencia. Post-proceso de las mallas densas de Hunyuan3D/ComfyUI (60 MB / ~1.67 M caras). Suelda los vertices duplicados ANTES del quadric edge collapse (pymeshlab) — las mallas cube-soup de VoxelToMeshBasic no decimar sin soldar primero. Conserva vertex colors (ColorVisuals, los interpola pymeshlab a traves del collapse) o textura+UV (TextureVisuals, reproyecta UV por vertice mas cercano con scipy y readjunta la imagen). Impura: lee y escribe en disco con trimesh + pymeshlab + scipy."
|
||||
tags: [comfyui, 3d, mesh, hunyuan3d, decimation, simplify, pymeshlab, trimesh, ml]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: error_go_core
|
||||
imports: []
|
||||
params:
|
||||
- name: in_path
|
||||
desc: "Ruta de la malla de entrada (.glb/.obj/.ply/.gltf/.stl/.off). Si es un GLB Scene con varias geometrias, se concatenan en una sola malla."
|
||||
- name: target_faces
|
||||
desc: "Numero de caras objetivo tras la decimacion. Si es >= a las caras soldadas, no decima (decimate_status lo indica). keyword-only. Default 80000."
|
||||
- name: weld
|
||||
desc: "Si True (default), suelda vertices duplicados (meshing_remove_duplicate_vertices) antes del collapse. CLAVE para las mallas cube-soup de VoxelToMeshBasic: sin el, el quadric edge collapse devuelve las caras intactas. keyword-only."
|
||||
- name: out_path
|
||||
desc: "Ruta de salida. Si None, escribe '<in>_simplified.glb' junto al original (NO sobrescribe el original). keyword-only."
|
||||
output: "dict {ok, in_faces, out_faces, in_mb, out_mb, out_path, welded_faces, decimate_status, appearance, error}. in/out_faces = caras antes/despues; in/out_mb = tamano de archivo en MB; welded_faces = caras tras soldar; decimate_status = 'ok' o 'skipped_target_ge_welded(N)'; appearance = 'vertex_color'|'texture'|'none'. Si falla, ok=False y error explica (dependencia ausente, archivo no existe, carga o export)."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "python/functions/ml/comfyui_simplify_mesh.py"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
|
||||
from ml.comfyui_simplify_mesh import comfyui_simplify_mesh
|
||||
|
||||
# Decima la malla densa de un personaje (964k caras, 34.7 MB) a 80k caras,
|
||||
# conservando los vertex colors. No toca el original.
|
||||
res = comfyui_simplify_mesh(
|
||||
os.path.expanduser("~/ComfyUI/output/character_3d_00001_.glb"),
|
||||
target_faces=80000,
|
||||
out_path="/tmp/character_simplified.glb",
|
||||
)
|
||||
# res == {"ok": True, "in_faces": 964332, "out_faces": 80000,
|
||||
# "in_mb": 34.718, "out_mb": 1.429, "appearance": "vertex_color",
|
||||
# "decimate_status": "ok", "out_path": "/tmp/character_simplified.glb", ...}
|
||||
```
|
||||
|
||||
Lanzar con el python del venv del registry (import de arriba o heredoc). `./fn run`
|
||||
directo no aplica: la firma usa `*` (keyword-only), no soportado por el runner de `fn run`.
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando una malla 3D de ComfyUI/Hunyuan3D sale demasiado densa (decenas de MB,
|
||||
millones de caras) y necesitas una version ligera para visor web, repo, deploy o
|
||||
texturizado posterior. Es el post-proceso de los .glb que baja
|
||||
`comfyui_fetch_output_mesh`. Para hacerla ademas estanca, encadena con
|
||||
`comfyui_make_watertight`. Si la malla ya es ligera (caras <= target_faces) no hace
|
||||
nada destructivo: el campo `decimate_status` lo refleja.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Impura: lee `in_path` y escribe `out_path` en disco. Nunca sobrescribe el
|
||||
original salvo que pases `out_path` apuntando a el.
|
||||
- `weld=True` es imprescindible para las mallas cube-soup de `VoxelToMeshBasic`
|
||||
(4 vertices por cara sin compartir aristas). Con `weld=False` sobre una cube-soup
|
||||
el quadric edge collapse no reduce caras. Solo pon `weld=False` si sabes que la
|
||||
malla ya tiene topologia soldada y quieres preservarla exacta.
|
||||
- Vertex colors: pymeshlab los interpola a traves del collapse y se readjuntan
|
||||
fielmente (verificado). Textura+UV: la reproyeccion de UV es por vertice mas
|
||||
cercano (scipy cKDTree), aproximada — fiel para mallas densas, puede mostrar
|
||||
ligero corrimiento en bordes de isla UV; la imagen de textura se readjunta sin
|
||||
recortar. La via de raiz para malla texturizada estanca es generar con
|
||||
`VoxelToMesh algorithm='surface net'` (ver report 0088), no este post-proceso.
|
||||
- El voxel-remesh de `comfyui_make_watertight` descarta la apariencia; si quieres
|
||||
malla ligera Y con color, decima con esta funcion y NO la pases por watertight
|
||||
voxel (o reproyecta el color despues).
|
||||
- Requiere trimesh + pymeshlab + scipy en el venv del registry (`cd python && uv
|
||||
add trimesh pymeshlab scipy`). Sin ellas devuelve ok=False con el detalle.
|
||||
@@ -0,0 +1,171 @@
|
||||
"""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))
|
||||
Reference in New Issue
Block a user