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_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

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

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →