Common 3D Model Formats
| Format | Extension | Features | Typical Use |
|---|---|---|---|
| OBJ | .obj |
Human-readable, .mtl for materials |
Universal interchange |
| STL | .stl |
Geometry only (triangles), ASCII or binary | 3D printing |
| GLTF | .gltf |
JSON + separate binary, PBR materials | Web 3D, Three.js |
| GLB | .glb |
Binary all-in-one GLTF | Web, AR/VR |
| FBX | .fbx |
Animations, Autodesk proprietary | Games, film |
| PLY | .ply |
Point cloud + mesh, scientific | 3D vision, AI |
| 3MF | .3mf |
Modern, ZIP of XML, rich metadata | Advanced 3D printing |
Installation
pip install trimesh numpy scipy
# For texture and material support:
pip install trimesh[easy]
# For GLB export with textures:
pip install trimesh[all]
Loading and Saving 3D Models
import trimesh
import numpy as np
from pathlib import Path
def load_model(path):
"""
Load a 3D model from any supported format.
Returns a Trimesh or Scene (for models with multiple meshes).
"""
model = trimesh.load(str(path), force='mesh')
if isinstance(model, trimesh.Scene):
model = model.dump(concatenate=True)
print(f"Model loaded: {Path(path).name}")
print(f" Vertices: {len(model.vertices):,}")
print(f" Faces: {len(model.faces):,}")
print(f" Volume: {model.volume:.4f} cm³")
print(f" Watertight: {model.is_watertight}")
return model
def convert_format(input_path, output_path, simplify=None):
"""
Convert a 3D model to another format.
simplify: fraction of faces to keep (e.g. 0.5 = keep 50% of faces)
"""
model = load_model(input_path)
if simplify and 0 < simplify < 1:
n_orig = len(model.faces)
n_target = int(n_orig * simplify)
model = model.simplify_quadratic_decimation(n_target)
print(f" Simplified: {n_orig:,} → {len(model.faces):,} faces")
model.export(str(output_path))
orig = Path(input_path).stat().st_size / 1024
new_sz = Path(output_path).stat().st_size / 1024
print(f"Converted: {input_path} → {output_path} ({orig:.0f}KB → {new_sz:.0f}KB)")
return model
# Most common conversions
convert_format('part.obj', 'part.stl') # For 3D printing
convert_format('scan.ply', 'model.glb') # For web 3D
convert_format('design.stl', 'design.obj') # For editing
convert_format('avatar.fbx', 'avatar.glb', simplify=0.5) # Optimized for web
OBJ to STL (for 3D Printing)
def obj_to_stl(obj_path, stl_output=None, repair=True, binary=True):
"""
Convert OBJ to STL optimized for 3D printing.
Optionally repairs the mesh (holes, flipped faces).
"""
model = trimesh.load(str(obj_path), force='mesh')
if isinstance(model, trimesh.Scene):
model = model.dump(concatenate=True)
if stl_output is None:
stl_output = Path(obj_path).with_suffix('.stl')
if repair:
trimesh.repair.fix_normals(model)
trimesh.repair.fix_inversion(model)
trimesh.repair.fill_holes(model)
if model.is_watertight:
print("✅ Watertight mesh (printable)")
else:
print("⚠️ Not watertight — may cause 3D printing issues")
model.export(str(stl_output), file_type='stl' if binary else 'stl_ascii')
orig = Path(obj_path).stat().st_size / 1024
new_sz = Path(stl_output).stat().st_size / 1024
print(f"OBJ→STL: {stl_output} ({orig:.0f}KB → {new_sz:.0f}KB)")
return model
def analyze_for_3d_printing(stl_path, unit='mm'):
"""Analyze an STL model for 3D printing."""
model = trimesh.load(str(stl_path), force='mesh')
extents = model.extents
print(f"=== 3D Print Analysis: {Path(stl_path).name} ===")
print(f"Dimensions ({unit}):")
print(f" Width (X): {extents[0]:.2f}")
print(f" Depth (Y): {extents[1]:.2f}")
print(f" Height (Z): {extents[2]:.2f}")
print(f"Volume: {model.volume:.2f} {unit}³")
print(f"Surface area: {model.area:.2f} {unit}²")
print(f"Faces: {len(model.faces):,}")
print(f"Watertight: {model.is_watertight}")
if not model.is_watertight:
loops = trimesh.graph.boundary_loops(model)
print(f"⚠️ Holes detected: {len(loops)}")
# Rough material estimate (PLA)
volume_cm3 = model.volume / 1000 # assuming mm units
grams_pla = volume_cm3 * 1.24 # PLA density ≈ 1.24 g/cm³
print(f"Estimated material (PLA): {grams_pla:.1f} g")
return {
'dimensions': extents.tolist(),
'volume': model.volume,
'area': model.area,
'watertight': model.is_watertight,
'faces': len(model.faces),
}
GLB/GLTF for Web (Three.js, React Three Fiber)
def optimize_for_web(input_path, glb_output=None, max_faces=50000):
"""
Convert and optimize a 3D model for web use.
Simplifies, centers and normalizes size.
"""
model = trimesh.load(str(input_path), force='mesh')
if isinstance(model, trimesh.Scene):
model = model.dump(concatenate=True)
n_faces = len(model.faces)
if n_faces > max_faces:
model = model.simplify_quadratic_decimation(max_faces)
print(f" Simplified: {n_faces:,} → {len(model.faces):,} faces")
# Center at origin
model.apply_translation(-model.centroid)
# Normalize size to unit scale (for scaling in Three.js)
scale = 1.0 / max(model.extents)
model.apply_scale(scale)
# Recalculate normals for correct lighting
model.fix_normals()
if glb_output is None:
glb_output = Path(input_path).with_suffix('.glb')
model.export(str(glb_output))
size_kb = Path(glb_output).stat().st_size / 1024
print(f"Web GLB: {glb_output} ({size_kb:.1f} KB, {len(model.faces):,} faces)")
return model
# Three.js loading code
print('''
// In your Three.js project:
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
const loader = new GLTFLoader();
loader.load("model.glb", (gltf) => {
scene.add(gltf.scene);
gltf.scene.scale.setScalar(2);
gltf.scene.position.set(0, 0, 0);
});
''')
Generating 3D Primitives
def generate_3d_primitives():
"""Generate basic 3D primitives with trimesh."""
trimesh.creation.icosphere(subdivisions=4, radius=1.0).export('sphere.stl')
trimesh.creation.box(extents=[2, 1, 0.5]).export('box.stl')
trimesh.creation.cylinder(radius=0.5, height=2.0, sections=32).export('cylinder.stl')
trimesh.creation.cone(radius=1.0, height=2.0, sections=32).export('cone.stl')
trimesh.creation.torus(major_radius=2.0, minor_radius=0.5).export('torus.stl')
print("Primitives generated: sphere, box, cylinder, cone, torus")
def batch_convert_3d(directory, output_format='stl', pattern='*.obj'):
"""Convert all 3D models in a folder to the specified format."""
folder = Path(directory)
models = list(folder.glob(pattern))
results = {'ok': 0, 'error': 0}
print(f"Converting {len(models)} models to {output_format}...")
for model_path in sorted(models):
output = model_path.with_suffix(f'.{output_format}')
try:
m = trimesh.load(str(model_path), force='mesh')
if isinstance(m, trimesh.Scene):
m = m.dump(concatenate=True)
m.export(str(output))
print(f" ✓ {model_path.name} → {output.name}")
results['ok'] += 1
except Exception as e:
print(f" ✗ {model_path.name}: {e}")
results['error'] += 1
print(f"Done: {results['ok']} ok, {results['error']} errors")
return results
generate_3d_primitives()
analyze_for_3d_printing('part.stl', unit='mm')
batch_convert_3d('obj_models/', output_format='stl', pattern='*.obj')
Conclusion
trimesh is the most complete Python library for working with 3D meshes: it supports over 15 input/output formats, offers boolean operations, simplification, mesh repair and geometric analysis. For 3D printing, the recommended workflow is OBJ/FBX → STL (with mesh repair) → verify watertight. For the web, OBJ/FBX → GLB (with simplification to < 50k faces) → Three.js/React Three Fiber.
Related conversions
Frequent conversions across the catalogue: