Formatos de modelos 3D más comunes
| Formato | Extensión | Características | Uso típico |
|---|---|---|---|
| OBJ | .obj |
Texto legible, con .mtl para materiales |
Intercambio universal |
| STL | .stl |
Solo geometría (triángulos), ASCII o binario | Impresión 3D |
| GLTF | .gltf |
JSON + binario separado, PBR materials | Web 3D, Three.js |
| GLB | .glb |
GLTF binario todo-en-uno | Web, AR/VR |
| FBX | .fbx |
Animaciones, propietario Autodesk | Videojuegos, cine |
| PLY | .ply |
Nube de puntos + malla, científico | Visión 3D, IA |
| 3MF | .3mf |
Moderno, zip de XML, metadatos | Impresión 3D avanzada |
| OFF | .off |
Simple, solo vértices+caras | Investigación |
Instalación
pip install trimesh numpy scipy pyglet
# Para soporte de texturas y materiales:
pip install trimesh[easy]
# Para exportar a GLB con texturas:
pip install trimesh[all]
Cargar y guardar modelos 3D
import trimesh
import numpy as np
from pathlib import Path
def cargar_modelo(ruta):
"""
Carga un modelo 3D desde cualquier formato soportado.
Devuelve un Trimesh o un Scene (para modelos con múltiples meshes).
"""
modelo = trimesh.load(str(ruta), force='mesh')
if isinstance(modelo, trimesh.Scene):
# Combinar todas las geometrías en una sola malla
modelo = modelo.dump(concatenate=True)
print(f"Modelo cargado: {Path(ruta).name}")
print(f" Vértices: {len(modelo.vertices):,}")
print(f" Caras: {len(modelo.faces):,}")
print(f" Volumen: {modelo.volume:.4f} cm³")
print(f" Watertight: {modelo.is_watertight}")
return modelo
def convertir_formato(entrada, salida, simplificar=None):
"""
Convierte un modelo 3D a otro formato.
simplificar: fracción de caras a conservar (ej: 0.5 = 50% de las caras)
"""
modelo = cargar_modelo(entrada)
# Simplificar si se solicita
if simplificar and 0 < simplificar < 1:
n_original = len(modelo.faces)
n_objetivo = int(n_original * simplificar)
modelo = modelo.simplify_quadratic_decimation(n_objetivo)
print(f" Simplificado: {n_original:,} → {len(modelo.faces):,} caras")
# Exportar al formato determinado por la extensión de salida
modelo.export(str(salida))
orig = Path(entrada).stat().st_size / 1024
nuevo = Path(salida).stat().st_size / 1024
print(f"Convertido: {entrada} → {salida} ({orig:.0f}KB → {nuevo:.0f}KB)")
return modelo
# Conversiones más comunes
convertir_formato('pieza.obj', 'pieza.stl') # Para impresión 3D
convertir_formato('escaneo.ply', 'modelo.glb') # Para web 3D
convertir_formato('diseno.stl', 'diseno.obj') # Para edición
convertir_formato('avatar.fbx', 'avatar.glb', simplificar=0.5) # Para web optimizado
OBJ → STL (para impresión 3D)
def obj_a_stl(obj_ruta, stl_salida=None, reparar=True, binario=True):
"""
Convierte OBJ a STL optimizado para impresión 3D.
Opcionalmente repara la malla (agujeros, caras invertidas).
"""
modelo = trimesh.load(str(obj_ruta), force='mesh')
if isinstance(modelo, trimesh.Scene):
modelo = modelo.dump(concatenate=True)
if stl_salida is None:
stl_salida = Path(obj_ruta).with_suffix('.stl')
if reparar:
# Reparar malla para impresión 3D
trimesh.repair.fix_normals(modelo)
trimesh.repair.fix_inversion(modelo)
trimesh.repair.fill_holes(modelo)
# Verificar si es "watertight" (imprimible)
if modelo.is_watertight:
print("✅ Malla watertight (imprimible)")
else:
print("⚠️ Malla no watertight — puede tener problemas en impresión 3D")
# Exportar en binario (más pequeño) o ASCII
if binario:
modelo.export(str(stl_salida), file_type='stl')
else:
modelo.export(str(stl_salida), file_type='stl_ascii')
orig = Path(obj_ruta).stat().st_size / 1024
nuevo = Path(stl_salida).stat().st_size / 1024
print(f"OBJ→STL: {stl_salida} ({orig:.0f}KB → {nuevo:.0f}KB)")
return modelo
# Información para impresión 3D
def analizar_para_impresion(stl_ruta, unidad='mm'):
"""Analiza un modelo STL para impresión 3D."""
modelo = trimesh.load(str(stl_ruta), force='mesh')
extents = modelo.extents # [ancho, alto, prof]
bounds = modelo.bounds # [[xmin,ymin,zmin], [xmax,ymax,zmax]]
print(f"=== Análisis para impresión 3D: {Path(stl_ruta).name} ===")
print(f"Dimensiones ({unidad}):")
print(f" Ancho (X): {extents[0]:.2f}")
print(f" Largo (Y): {extents[1]:.2f}")
print(f" Alto (Z): {extents[2]:.2f}")
print(f"Volumen: {modelo.volume:.2f} {unidad}³")
print(f"Área superficial: {modelo.area:.2f} {unidad}²")
print(f"Caras: {len(modelo.faces):,}")
print(f"Watertight: {modelo.is_watertight}")
if not modelo.is_watertight:
# Detectar agujeros
contornos = trimesh.graph.boundary_loops(modelo)
print(f"⚠️ Agujeros detectados: {len(contornos)}")
# Estimar tiempo de impresión (heurística muy aproximada)
volumen_cm3 = modelo.volume / 1000 # asumiendo unidades en mm
gramos_pla = volumen_cm3 * 1.24 # densidad PLA ≈ 1.24 g/cm³
print(f"Material estimado (PLA): {gramos_pla:.1f} g")
return {
'dimensiones': extents.tolist(),
'volumen': modelo.volume,
'area': modelo.area,
'watertight': modelo.is_watertight,
'caras': len(modelo.faces),
}
GLB/GLTF para web (Three.js, React Three Fiber)
def optimizar_para_web(entrada, glb_salida=None, max_caras=50000):
"""
Convierte y optimiza un modelo 3D para uso web.
Simplifica, centra y normaliza el tamaño.
"""
modelo = trimesh.load(str(entrada), force='mesh')
if isinstance(modelo, trimesh.Scene):
modelo = modelo.dump(concatenate=True)
# 1. Simplificar si tiene demasiadas caras
n_caras = len(modelo.faces)
if n_caras > max_caras:
fraccion = max_caras / n_caras
modelo = modelo.simplify_quadratic_decimation(max_caras)
print(f" Simplificado: {n_caras:,} → {len(modelo.faces):,} caras")
# 2. Centrar en el origen
modelo.apply_translation(-modelo.centroid)
# 3. Normalizar tamaño a unidad de 1 (para escalar en Three.js)
escala_norm = 1.0 / max(modelo.extents)
modelo.apply_scale(escala_norm)
# 4. Recalcular normales para iluminación correcta
modelo.fix_normals()
if glb_salida is None:
glb_salida = Path(entrada).with_suffix('.glb')
modelo.export(str(glb_salida))
tamaño = Path(glb_salida).stat().st_size / 1024
print(f"GLB web: {glb_salida} ({tamaño:.1f} KB, "
f"{len(modelo.faces):,} caras)")
return modelo
# Código Three.js de ejemplo para cargar el GLB generado
codigo_threejs = '''
// En tu proyecto Three.js:
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const loader = new GLTFLoader();
loader.load('modelo.glb', (gltf) => {
scene.add(gltf.scene);
gltf.scene.scale.setScalar(2); // ajustar escala
gltf.scene.position.set(0, 0, 0);
});
'''
print("Código Three.js de ejemplo:")
print(codigo_threejs)
Operaciones avanzadas con trimesh
def operaciones_booleanas(malla1_ruta, malla2_ruta, operacion='union'):
"""
Realiza operaciones booleanas entre dos mallas 3D.
operacion: 'union', 'difference', 'intersection'
Requiere: pip install manifold3d
"""
try:
import manifold3d
except ImportError:
print("pip install manifold3d (necesario para operaciones booleanas)")
return
malla1 = trimesh.load(str(malla1_ruta), force='mesh')
malla2 = trimesh.load(str(malla2_ruta), force='mesh')
if operacion == 'union':
resultado = malla1.union(malla2)
elif operacion == 'difference':
resultado = malla1.difference(malla2)
elif operacion == 'intersection':
resultado = malla1.intersection(malla2)
else:
raise ValueError(f"Operación desconocida: {operacion}")
salida = f'resultado_{operacion}.stl'
resultado.export(salida)
print(f"Operación booleana '{operacion}' → {salida}")
return resultado
def generar_primitivas_3d():
"""Genera primitivas 3D básicas con trimesh."""
# Esfera
esfera = trimesh.creation.icosphere(subdivisions=4, radius=1.0)
esfera.export('esfera.stl')
# Caja
caja = trimesh.creation.box(extents=[2, 1, 0.5])
caja.export('caja.stl')
# Cilindro
cilindro = trimesh.creation.cylinder(radius=0.5, height=2.0, sections=32)
cilindro.export('cilindro.stl')
# Cono
cono = trimesh.creation.cone(radius=1.0, height=2.0, sections=32)
cono.export('cono.stl')
# Toro (donut)
toro = trimesh.creation.torus(major_radius=2.0, minor_radius=0.5, sections=64)
toro.export('toro.stl')
print("Primitivas generadas: esfera, caja, cilindro, cono, toro")
def convertir_nube_puntos_a_malla(ply_ruta, stl_salida):
"""
Convierte una nube de puntos PLY (de escáner 3D) a malla STL.
Usa reconstrucción Poisson.
"""
nube = trimesh.load(str(ply_ruta))
if hasattr(nube, 'vertices') and not hasattr(nube, 'faces'):
# Es solo nube de puntos, reconstruir malla
malla, densidad = trimesh.convex_hull.hull_points(nube.vertices)
malla_obj = trimesh.Trimesh(vertices=malla.vertices, faces=malla.faces)
malla_obj.export(stl_salida)
print(f"Nube→Malla: {stl_salida} ({len(malla_obj.vertices):,} vértices)")
return malla_obj
else:
print("El archivo ya tiene geometría de malla")
return nube
def lote_convertir_3d(directorio, formato_salida='stl', patron='*.obj'):
"""Convierte todos los modelos 3D de una carpeta al formato especificado."""
carpeta = Path(directorio)
modelos = list(carpeta.glob(patron))
print(f"Convirtiendo {len(modelos)} modelos a {formato_salida}...")
resultados = {'ok': 0, 'error': 0}
for modelo_path in sorted(modelos):
salida = modelo_path.with_suffix(f'.{formato_salida}')
try:
m = trimesh.load(str(modelo_path), force='mesh')
if isinstance(m, trimesh.Scene):
m = m.dump(concatenate=True)
m.export(str(salida))
print(f" ✓ {modelo_path.name} → {salida.name}")
resultados['ok'] += 1
except Exception as e:
print(f" ✗ {modelo_path.name}: {e}")
resultados['error'] += 1
print(f"Completado: {resultados['ok']} ok, {resultados['error']} errores")
return resultados
# Uso
generar_primitivas_3d()
analizar_para_impresion('pieza.stl', unidad='mm')
lote_convertir_3d('modelos_obj/', formato_salida='stl', patron='*.obj')
Conclusión
trimesh es la biblioteca Python más completa para trabajar con mallas 3D: soporta más de 15 formatos de entrada y salida, ofrece operaciones booleanas, simplificación, reparación de mallas y análisis geométrico. Para impresión 3D, el flujo recomendado es OBJ/FBX → STL (con reparación de malla) → verificar watertight. Para web, OBJ/FBX → GLB (con simplificación a < 50k caras) → Three.js/React Three Fiber.
Conversiones relacionadas
Conversiones frecuentes del catálogo: