How Modly's Mesh Smoothing and Decimation Works to Optimize 3D Assets
Modly optimizes 3D meshes through two integrated pipelines—mesh smoothing that eliminates geometric artifacts using Taubin or Laplacian algorithms, and mesh decimation that reduces polygon count via quadric edge-collapse—all powered by pymeshlab and trimesh to produce clean, texture-preserving GLB outputs.
Modly's mesh optimization system transforms raw 3D geometry into production-ready assets through a dual-pipeline architecture. Whether you're cleaning up noisy AI-generated meshes or reducing polygon density for real-time rendering, the platform leverages pymeshlab (wrapping MeshLab's proven algorithms) and trimesh for reliable I/O handling. This guide breaks down exactly how Modly's mesh smoothing and decimation process works, with direct references to the implementation in lightningpixel/modly.
Core Architecture: From Input to Optimized GLB
Both pipelines follow an identical seven-stage workflow designed to protect source files and preserve asset integrity.
Input Resolution and Validation
The API receives optimization requests via POST endpoints (/mesh for decimation, /smooth for smoothing) with a JSON payload containing the mesh path, parameters, and workspace directory. The _resolve_input_path helper in api/routers/optimize.py validates and resolves this to an absolute workspace location:
# Lines 49-62 in api/routers/optimize.py
def _resolve_input_path(path: str) -> Path:
base = Path(WORKSPACE_DIR)
full = base / path
# Security check: path must be inside workspace
if not str(full.resolve()).startswith(str(base.resolve())):
raise ValueError("Path outside workspace")
return full
Temporary Workspace Isolation
To guarantee the original mesh remains untouched, each operation creates a temporary directory via tempfile.mkdtemp():
# Lines 72-74, 84-88 in api/routers/optimize.py
tmp_dir = tempfile.mkdtemp()
# ... processing happens here ...
shutil.rmtree(tmp_dir) # cleanup after completion
This pattern appears in optimize_mesh(), _decimate(), and _smooth() functions.
Format Conversion Strategy
MeshLab requires specific formats, so trimesh handles conversion:
- PLY for geometry-only operations (smoothing pipeline)
- OBJ when UVs/textures are present (decimation pipeline with texture preservation)
The smoother uses geom.export(ply_in) (src/areas/workflows/nodes/mesh-smoother/processor.py:81-84), while the decimator branches on _has_texture(geom) to choose between geom.export(obj_in) and geom.export(ply_in) (api/routers/optimize.py:25-28, 64-68).
Mesh Smoothing: Eliminating Artifacts with Taubin and Laplacian
The mesh smoothing pipeline targets "zipper-triangles" and saw-tooth edges common in photogrammetry or AI-generated geometry.
Algorithm Selection
Two modes are exposed through the mode parameter:
| Mode | Method | Best For |
|---|---|---|
taubin |
Volume-preserving forward-backward Laplacian | Organic shapes, preventing shrinkage |
laplacian |
Simple iterative smoothing | Flat surfaces, aggressive noise removal |
The Taubin implementation uses lambda_ (smoothing strength, default 0.5) with mu = -lambda_ - 0.01 for the backward pass. From src/areas/workflows/nodes/mesh-smoother/processor.py:90-99:
if mode == "taubin":
ms.apply_coord_taubin_smoothing(
stepsmoothnum=iterations,
lambda_=lambda_value,
mu=-lambda_value - 0.01
)
else:
ms.apply_coord_laplacian_smoothing(stepsmoothnum=iterations)
Smoothing Parameter Defaults
iterations: 5 (UI-clamped to prevent over-smoothing)lambda_: 0.5 (strength multiplier)- Output encoding:
_smooth{iterations}suffix on filename
Result Reconstruction
After MeshLab processing, the mesh reloads with trimesh.load(ply_out, process=False) to avoid expensive post-processing like color conversion (processor.py:105-109). The final GLB contains repositioned vertices with original texture data intact.
Mesh Decimation: Polygon Reduction with Detail Preservation
The decimation pipeline reduces face count while fighting to retain visual surface detail and texture mapping.
Target Face Control
The client specifies target_faces, which the server clamps to 100–500,000 to prevent degenerate outputs. The core algorithm is meshing_decimation_quadric_edge_collapse (api/routers/optimize.py:44-51, 62-71):
ms.meshing_decimation_quadric_edge_collapse(
targetfacenum=target_faces,
preservenormal=True,
preservetopology=True,
autoclean=True,
# ... texture-aware parameters when needed
)
Texture-Aware vs. Geometry-Only Paths
The pipeline automatically detects textures via _has_texture(geom) and branches:
With textures: OBJ route with explicit UV preservation
- Exports to
obj_inwith companion MTL - Copies texture image as
texture.png - Patches MTL to reference the standardized texture name (
api/routers/optimize.py:24-34,37-44) - Sets
preseretexcoord=Truein decimation call
Without textures: Fast PLY route
- Direct vertex/face data only
- No material handling overhead
Decimation Output Format
Final files encode the target in their name: {stem}_opt{target_faces}.glb. The API response includes both URL and actual face_count, which may differ slightly from target due to algorithm constraints.
End-to-End Processing Flow
Both pipelines complete these stages in sequence:
- Request validation – JSON schema check, path resolution
- Temp directory creation – isolated workspace for intermediates
- Format export – trimesh to PLY or OBJ based on texture presence
- MeshLab execution – pymeshlab MeshSet with algorithm-specific filters
- Result import – trimesh.load with appropriate flags
- GLB export – to workspace Workflows folder or original location
- Cleanup and response – temp removal, JSON with URL and metadata
Real-time progress streams via WebSocket or SSE provide type: "progress", type: "log", and type: "error" messages for UI feedback.
Code Examples: Calling the Optimization API
Smoothing a Noisy Mesh
import requests
response = requests.post(
"https://modly.example.com/optimize/smooth",
json={
"path": "scans/artifact_scan.glb",
"iterations": 8, # stronger smoothing
"mode": "taubin", # preserve volume
"lambda_": 0.6
}
)
# Returns: {"url": "/workspace/scans/artifact_scan_smooth8.glb"}
Decimating for Real-Time Use
import requests
response = requests.post(
"https://modly.example.com/optimize/mesh",
json={
"path": "assets/hero_character.obj",
"target_faces": 25000 # game-ready poly count
}
)
# Returns: {"url": "/workspace/assets/hero_character_opt25000.glb", "face_count": 24987}
Key Implementation Files
| File | Purpose |
|---|---|
api/routers/optimize.py |
HTTP endpoints, texture detection, temp file orchestration |
src/areas/workflows/nodes/mesh-smoother/processor.py |
Standalone smoothing processor, Taubin/Laplacian implementation |
api/services/generator_registry.py |
Workspace directory configuration |
Summary
- Architecture: Both pipelines use temporary directories, trimesh format conversion, pymeshlab processing, and GLB output to protect source files
- Smoothing: Taubin mode preserves volume while Laplacian offers simpler iteration—both controlled by
iterationsandlambda_parameters - Decimation: Quadric edge-collapse reduces faces to a target count, with automatic OBJ/PLY branching based on texture presence
- Texture safety: UV coordinates and texture images survive optimization through explicit MTL patching and
preseretexcoordflags - Output: Consistent GLB format with descriptive filenames encoding operation parameters
Frequently Asked Questions
What algorithms does Modly use for mesh smoothing?
Modly implements Taubin smoothing (volume-preserving forward-backward Laplacian) and Laplacian smoothing (simple iterative averaging) through pymeshlab.apply_coord_taubin_smoothing and apply_coord_laplacian_smoothing. Taubin is preferred for organic models to prevent shrinkage, while Laplacian works well for flat surfaces requiring aggressive noise removal according to the lightningpixel/modly source.
How does Modly preserve textures during decimation?
When _has_texture(geom) returns true, Modly switches to an OBJ-based workflow: it exports geometry with UVs intact, copies the texture image as texture.png, patches the MTL file to reference this standardized name, and calls meshing_decimation_quadric_edge_collapse with preseretexcoord=True. This guarantees texture coordinates survive polygon reduction.
Why does Modly use temporary directories for processing?
The tempfile.mkdtemp() pattern in api/routers/optimize.py ensures the original user file is never mutated, supports concurrent operations on the same source asset, and enables clean rollback if processing fails. Intermediate PLY/OBJ files are deleted after successful GLB export.
What file formats does Modly output for optimized meshes?
All optimization pipelines output GLB (GL Transmission Format binary) as the final format, with filenames encoding the operation performed—_smooth{iterations} for smoothing and _opt{target_faces} for decimation. This provides universal compatibility with web viewers, game engines, and AR/ML pipelines.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →