# How Modly's Mesh Smoothing and Decimation Works to Optimize 3D Assets

> Discover how Modly optimizes 3D assets with mesh smoothing and decimation. Learn about Taubin, Laplacian, and quadric edge-collapse algorithms for clean, texture-preserving GLB outputs.

- Repository: [lightningpixel/modly](https://github.com/lightningpixel/modly)
- Tags: internals
- Published: 2026-08-20

---

**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`](https://github.com/lightningpixel/modly/blob/main/api/routers/optimize.py) validates and resolves this to an absolute workspace location:

```python

# 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()`:

```python

# 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`:

```python
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`):

```python
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_in` with 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=True` in 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:

1. **Request validation** – JSON schema check, path resolution
2. **Temp directory creation** – isolated workspace for intermediates
3. **Format export** – trimesh to PLY or OBJ based on texture presence
4. **MeshLab execution** – pymeshlab MeshSet with algorithm-specific filters
5. **Result import** – trimesh.load with appropriate flags
6. **GLB export** – to workspace Workflows folder or original location
7. **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

```python
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

```python
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`](https://github.com/lightningpixel/modly/blob/main/api/routers/optimize.py) | HTTP endpoints, texture detection, temp file orchestration |
| [`src/areas/workflows/nodes/mesh-smoother/processor.py`](https://github.com/lightningpixel/modly/blob/main/src/areas/workflows/nodes/mesh-smoother/processor.py) | Standalone smoothing processor, Taubin/Laplacian implementation |
| [`api/services/generator_registry.py`](https://github.com/lightningpixel/modly/blob/main/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 `iterations` and `lambda_` 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 `preseretexcoord` flags
- **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`](https://github.com/lightningpixel/modly/blob/main/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.