---
title: "AI mesh cleanup"
slug: /workflows/ai-mesh-cleanup/
markdown: /workflows/ai-mesh-cleanup.md
section: art-audio-animation
tier: 2
summary: "Repair, reduce and re-UV generated meshes before import."
sources: ['ai-mesh-cleanup.md']
canonical: https://unityworkflowlibrary.dev/workflows/ai-mesh-cleanup/
read_first: /read-first.md
---

> Read first, on every prompt: [non-destructive rules](/read-first.md).

# AI mesh cleanup

> Repair, reduce and re-UV generated meshes before import.

> Load when you repair, reduce, re-UV, rebake or retexture a generated model. Generation rules
> (concepts, rerolls, rigging) live in the ai modelgen page; asset conventions and the FBX recipe in
> the blender page; how to drive Blender in the blender mcp page. Surveyed.

## Fix it at the generator first (AI-MESH-FIX-UPSTREAM)

A cleanup step you do not need is the cheapest one. Every generate call pins its model version
(`ai_model` / `model_version`) and asks for:

- **Meshy** `POST /openapi/v1/image-to-3d`:
  - `enable_pbr=true`
  - `remove_lighting=true` (meshy-6 only)
  - `pose_mode="a-pose"` for characters
  - either `should_remesh=true` with `topology` and `target_polycount`, or a later `POST /openapi/v1/remesh`
    (5 credits; 100–300k faces; exports glb, fbx, obj, blend and others).
- **Tripo** `POST /v2/openapi/task`, `image_to_model`:
  - `pbr=true`
  - `smart_low_poly` with `face_limit` (1000–20000), or `quad` (500–10000)
  - `generate_parts` when the model has separable parts
  - **`compress` off**: the meshopt or Draco default needs an importer extension Blender may lack.
  - Or run the separate smart-low-poly task with `bake=true`, so Tripo bakes the detail onto the low-poly mesh itself.
- Fused limbs, a weapon fused to the body, and lighting baked into the albedo have **no geometric
  repair**. Regenerate (the ai modelgen page WEAPON-FUSED-REGENERATE-NEVER-CUT,
  GEN-REF-NO-SHADOW-BLEED) before paying for a manual fix.
- Read back the consumed credits on every call.

## Measure before and after (AI-MESH-MEASURE-FIRST)

Run the numbers gate first and last:

```
blender -b --factory-startup --python-exit-code 1 --python tools/blender/ai_mesh_report.py -- <model> --budget <tris>
```

It prints one `AI_MESH_REPORT {json}` line with:
- triangle count
- **shells**, counted after joining vertices at the same position
- open and non-manifold edges, counted by position
- zero-area faces, UV layers, textures and their colour spaces, and size in metres
- a verdict: `solid` or `island-soup`, `automated_repair_allowed`, and which steps apply

- **GLTF-SEAM-SPLIT-SHELLS:** a glTF stores a separate vertex on every UV seam and hard edge, so an
  imported mesh looks like hundreds of loose pieces when it is one solid piece.
  - Measured on three Meshy building parts: 133, 423 and 3812 pieces as stored, each **1** after joining
    vertices at the same position.
  - Any island count, island cull or "largest component" check that runs on the as-stored mesh is
    measuring UV seams. Count by position (the report does) before calling a mesh island-soup.
- `island-soup` (largest positional shell under 90% of the vertices) follows the ai modelgen page:
  no automated cull, weld or remesh. Regenerate, or repair by hand.
- `solid` meshes may take the automated steps below.

## The pass, in order (AI-MESH-CLEANUP-PIPELINE)

Heavy steps run headless on a copy (the blender mcp page BLENDER-HEADLESS-RECIPE). The mesh
libraries run in their own environment (`uv run --project tools/blender/cleanup python ...`: pymeshlab,
pymeshfix, xatlas, trimesh, all pinned), never inside Blender's Python. Hand meshes between the two
as `.ply`, `.obj` or `.glb`.

| # | Step | Tool | Guard |
| --- | --- | --- | --- |
| 1 | Measure | `ai_mesh_report.py` | Decides which of the steps below run. |
| 2 | Weld | Merge by Distance, threshold about `1e-5 × largest dimension` | A fixed threshold collapses fingers and straps on small models. UVs are stored per face corner, so welding keeps the UV seams. |
| 3 | Normals | clear custom split normals, `normals_make_consistent(inside=False)`, smooth by angle | the blender page has the full normals step. |
| 4 | Holes, non-manifold, self-intersection | pymeshlab `meshing_repair_non_manifold_edges` / `_vertices`, `meshing_close_holes(maxholesize=...)`; self-intersecting faces removed, then holes closed. Last resort: pymeshfix. | pymeshfix and "remove connected component" keep only the **largest** piece by default, which deletes eyes, teeth and accessories. Cull by relative size (under 1–2% of the bounding box), never "keep largest". |
| 5 | Surface noise | pymeshlab `apply_coord_taubin_smoothing` (keeps volume) | Laplacian smoothing shrinks the mesh. Any smoothing moves vertices, so plan the rebake in step 8. |
| 6 | Reduce to budget | Decimate COLLAPSE, or pymeshlab `meshing_decimation_quadric_edge_collapse_with_texture` | **Decimation keeps UVs. Every remesher throws them away**: voxel, QuadriFlow, Instant Meshes, Quad Remesher and vendor quad remesh. A remesh commits you to steps 7 and 8. |
| 7 | UVs, only if lost | **xatlas** (segments, unwraps and packs in one call; `texels_per_unit` sets texel density) | SLIM or Minimum Stretch needs seams, and AI meshes have none, so it returns one distorted island. Smart UV Project is the in-Blender fallback. |
| 8 | Bake high to low | Cycles Selected to Active: diffuse colour, then tangent-space normal, then AO. Extrusion 0.5–2% of the bounding box, margin 16 px at 2k. | Every material needs an active Image Texture node. The image exists only in memory until `image.save`. A cage too small leaves holes; too large projects from the wrong surface (armpits, between fingers). |
| 9 | Albedo lighting | Regenerate with `remove_lighting`, or retexture (step 10) | No reliable headless de-lighter exists for UV atlases. Substance Sampler's delight is 2D and GUI-only. ArtEngine was discontinued in 2022. |
| 10 | Retexture | Meshy `POST /openapi/v1/retexture` with **`enable_original_uv=true`** and `enable_pbr=true` (10–15 credits, refunded on failure; recipe in the blender page) | Tripo `texture_model` accepts only a Tripo task id, and whether it keeps UVs is undocumented. Test one model before relying on it. |
| 11 | Lightmap UV2 | Unity importer "Generate Lightmap UVs"; a separate xatlas channel if that fails | Never reuse a tiled UV0. No UV2 on skinned meshes. |
| 12 | Size, pivot, export | the blender page prep pass and FBX recipe, or GLB | Re-run step 1 as the exit gate, then check in Unity. |

- **Live fixes in the GUI:** blend-ai's `analyze_mesh_quality`, its sculpt tools, and a screenshot after
  each change, for one local defect. the ai modelgen page LOCALIZED-REPAIR-PROJECTION covers repairing one region.
- **Paid tools are the developer's call:**
  - Quad Remesher gives the best quads, but it needs a GUI Blender and does not finish reliably under `-b`.
  - UVPackmaster 3 (from $44) only packs; it does not unwrap.
  - Neither is required by this pass.
- Skills on skills.sh (`meshy-dev/meshy-3d-agent`, tripo and retopology skills) mostly wrap the
  generation APIs. None implements this measure-repair-rebake-export loop, so treat them as references.

## BAKE-TRAPS-ON-GENERATED-MESHES

Measured while baking generated props to a budget mesh:

- A mirrored half keeps its winding: 15,049 of 30,666 faces rendered inside out until the mirror ran
  `reverse_faces`. Gate on an inverted-face count.
- Keep the generator's maps on a mirrored piece; re-baking from the mirrored source blurred edge detail (edge
  energy 9 vs 27). Shade mirrored donor copies flat: smoothed normals lean across a flat panel and the mirror
  flips the lean into a light seam.
- Cap occlusion reach at half the thinnest side: a 6 cm door baked 17% of its texels black at a reach of 5% of
  its 2.14 m size (8% after). Bake occlusion from the high-poly source; the budget mesh lacks the relief
  (median 255/255). Skip occlusion on flush sheets, where rays start under the face (37% black).
- Pin hole-fill face UVs to one point: a fill face copies neighbour UVs and stretches visible texture across
  the surface. Remove pinch edges (shared by three faces) before filling; 36 open edges survived otherwise.
- Cuts through a generated mesh: a per-vertex squash folded 349 faces, a boolean took 26 GB and dropped posts,
  EXACT left 294 islands; Blender's MANIFOLD boolean solver closed it (4 islands).
- Render review boards with ACES, not Blender's default AgX, to match HDRP, and review a lit/clay/albedo sheet:
  a flat Workbench board hid missing occlusion, roughness and normals.
- Held first-person items need their own texel density: 12k tris at 512 px read soft next to the source; a held
  pass (LOD0 40k tris at 4096 px/m) fixed it.
