← All entries
/v0.4.0 /Mesh Quality

Full Pipeline Audit + Mesh Quality Overhaul

Ran a full project audit across the stack, fixed the highest-value mesh-quality bugs, and added a real 3D mesh viewer so the Forge result is visible as a model instead of a placeholder image.

Project: Inter-Forge Version: v0.4.0 Area: Mesh Quality

What We Set Out To Do

Inter-Forge had a working pipeline end-to-end, but meshes coming out of the Forge stage still looked rough: angular, low-detail, and sometimes geometrically wrong. Before trying to tune isolated outputs, the session started with a full audit of the codebase across the Rust shell, React and TypeScript frontend, FastAPI backend, ComfyUI client, and mesh engine.

Audit Findings Summary

Bug 1: Inverted Depth Map

Meshes had inside-out surface topology. The thickest part of the geometry was being treated as if it belonged at the center of the silhouette instead of near the outer visible edges. The problem was that distance_transform_edt(binary) was being used directly as depth, even though the raw transform peaks at the center of the filled mask.

Bug / Fix

- dist = distance_transform_edt(binary)
- dist = dist / dist.max()
+ dist = distance_transform_edt(binary)
+ dist = dist / dist.max()
+ dist = (1.0 - dist) * binary   # invert: edges = high depth

Bug 2: Ring Scaling Destroying Taper

Every lofted mesh was being pushed toward a cylinder because align_rings() scaled all rings to a shared average radius before lofting. That destroys taper, flare, and any necking behavior outright. The fix was to stop normalizing per-ring radius and only keep centering and resampling, plus a clamp on extreme adjacent-ring ratios to avoid OCC degeneracy.

Bug / Fix

- common_r = mean([avg_radius(r) for r, _ in sorted_rings])
- processed = [(_scale_ring(r, common_r), z) for r, z in sorted_rings]
+ processed = []
+ for ring, z in sorted_rings:
+     c = _center_ring(ring)
+     r = _resample_ring(c, n_points)
+     processed.append((r, z))
+ # preserve taper, flare, and necking

Bug 3: Lossy STL Round-trip in CadQuery Export

Hard-surface meshes were picking up drift and cracking because CadQuery solids were exported to STL and re-imported through trimesh. STL pushes coordinates through a 32-bit float representation. The fix was to extract triangulation directly from the OCC B-Rep topology and keep the STL path only as a fallback.

Bug / Fix

- with tempfile.NamedTemporaryFile(suffix=".stl") as f:
-     shape.exportStl(f.name)
-     return trimesh.load(f.name)
+ BRepMesh_IncrementalMesh(shape.wrapped, 0.01, False, 0.5, True).Perform()
+ # extract OCC triangulation directly
+ return trimesh.Trimesh(vertices=verts, faces=faces)

Bug 4: Bilinear Interpolation for Vertex Colors

Curved surfaces were showing visible color banding because vertex color projection used truncating nearest-neighbor lookup. Adjacent vertices on a curve often hit the exact same pixel. The fix was to switch to bilinear interpolation using four-pixel weighted sampling.

Bug / Fix

- r = rgba[int(py), int(px), 0]
- g = rgba[int(py), int(px), 1]
- b = rgba[int(py), int(px), 2]
+ x0, y0 = int(px), int(py)
+ x1, y1 = min(x0 + 1, w - 1), min(y0 + 1, h - 1)
+ fx, fy = px - x0, py - y0
+ r, g, b = blerp(0), blerp(1), blerp(2)

Feature: 3D Mesh Viewer

The Forge tab was still showing a 2D concept image after generation, which made the completion state feel fake. A Three.js GLB viewer was added with orbit controls, ACES filmic tone mapping, 3-point lighting, auto-center, auto-scale, ResizeObserver handling, cleanup on unmount, and explicit loading plus error states.

Feature

+ const MeshViewer = lazy(() => import("../../components/MeshViewer/MeshViewer"));
+ {done && meshUrl && meshUrl.endsWith(".glb") && (
+   <Suspense fallback={<div className="spinner" />}>
+     <MeshViewer glbUrl={meshUrl} />
+   </Suspense>
+ )}

Improvement: Loft Resolution

The old loft grid was far too coarse. A 5 ring by 64 point setup only pushed about 320 vertices into the full silhouette. That was raised substantially.

Tuning

- n_loft_rings: int = 5
- n_loft_points: int = 64
- target_poly: int = 5000
- smooth_iter: int = 3
+ n_loft_rings: int = 16
+ n_loft_points: int = 192
+ target_poly: int = 15000
+ smooth_iter: int = 1

Improvement: Conditional Smoothing By Asset Type

Smoothing was being applied uniformly, which rounded hard-surface edges that should stay sharp. The fix was to preserve edges on hard-surface assets and keep only light smoothing for organic meshes.

Tuning

- mesh = smooth_mesh_laplacian(mesh, iterations=3)
+ mesh = smooth_mesh_laplacian(mesh, iterations=0)  # hard-surface
+ mesh = smooth_mesh_laplacian(mesh, iterations=1)  # organic

Improvement: Centralized Configuration

PROJECTS_ROOT, COMFYUI_BASE, and OUTPUTS_URL were duplicated across multiple files. The session introduced core/config.py so backend and workers can share one config source.

Bug Fix: Character Sampler Typo

Character generations were failing because the sampler string was invalid for ComfyUI. That issue was surfaced by unit tests.

Bug / Fix

- "sampler": "dpm_2m"
+ "sampler": "dpmpp_2m"

Performance Benchmarks

How To Reproduce / Verify

To reproduce the depth inversion issue, generate a character or weapon and inspect the mesh in Blender. Pre-fix, normals and topology logic read inside-out. Post-fix, the visible surface faces outward. To reproduce the taper issue, run the pipeline with a weapon prompt like “medieval sword.” Pre-fix, the mesh behaves like a cylinder. Post-fix, the blade tapers correctly.

To verify bilinear color projection, generate something with a visible gradient such as a blue-to-purple robe and inspect a curved region on the GLB. Pre-fix, colors step in blocks. Post-fix, the gradient is much smoother.

Test Results

What Is Still Missing

  • Before and after mesh comparison renders for the audit fixes.
  • Recorded clips of the viewer and quality differences.
  • Follow-up work on persistence, texture baking, sprite pipeline, and CSS consolidation.
← All entries