kwabs22 Claude Opus 5.5 commited on
Commit
65f605e
·
1 Parent(s): e0f9b43

Fall back to vertex clustering when quadric decimation stalls

Browse files

Live check after e0f9b43: the cat now reaches 400 faces, but the SimReady
desk lamp stayed at 22,228 (floor: true, a 522k-character inline page).
Quadric decimation stalls on its sliver triangles at ~21k whatever the
aggression or welding. When that happens, decimate snaps vertices to the
finest grid that reaches the target. Tested on the lamp's real geometry:
it reaches 20000/5000/1000/400/250/60, keeping the arms, shade and base,
which quadric-after-clustering dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Files changed (2) hide show
  1. SPEC.md +4 -1
  2. vli/simplify.py +31 -2
SPEC.md CHANGED
@@ -106,7 +106,10 @@ that exists but maps to the wrong thing shows up before submitting.
106
 
107
  Ladder rungs: original → 20k / 5k / 1k / 250 / 60 faces → convex hull → oriented
108
  box. Where the object stops reading as its word is judged in the discussion.
109
- Decimation welds split vertices (UV and normal seams) between passes. Rungs are
 
 
 
110
  labelled with the faces they actually have, and the ladder stops descending when
111
  a rung doesn't reduce, so a stalled mesh never shows as several identical rungs.
112
 
 
106
 
107
  Ladder rungs: original → 20k / 5k / 1k / 250 / 60 faces → convex hull → oriented
108
  box. Where the object stops reading as its word is judged in the discussion.
109
+ Decimation welds split vertices (UV and normal seams) between passes. When quadric
110
+ decimation still stalls (CAD meshes with sliver triangles, e.g. a SimReady lamp stuck
111
+ at ~21k faces), vertex clustering takes over: cruder, but it always reduces and keeps
112
+ thin parts. Rungs are
113
  labelled with the faces they actually have, and the ladder stops descending when
114
  a rung doesn't reduce, so a stalled mesh never shows as several identical rungs.
115
 
vli/simplify.py CHANGED
@@ -34,11 +34,38 @@ def _welded(mesh):
34
  process=False)
35
 
36
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  def decimate(mesh, faces):
38
  """Reduce toward `faces`, re-welding between passes while that still helps.
39
 
40
- The result can stay above `faces` when the mesh has no more structure to collapse
41
- (many disconnected parts): callers compare len(result.faces) with what they asked.
 
42
  """
43
  color = base_color(mesh)
44
  out = _welded(mesh)
@@ -47,6 +74,8 @@ def decimate(mesh, faces):
47
  if len(nxt.faces) > 0.95 * len(out.faces):
48
  break
49
  out = nxt
 
 
50
  out.visual = trimesh.visual.ColorVisuals(out, face_colors=np.tile(color, (len(out.faces), 1)))
51
  return out
52
 
 
34
  process=False)
35
 
36
 
37
+ def _clustered(mesh, cell):
38
+ """Vertex clustering: snap vertices to a grid of `cell` metres and merge each cell."""
39
+ v, f = np.asarray(mesh.vertices, dtype=np.float64), np.asarray(mesh.faces, dtype=np.int64)
40
+ inv = np.unique(np.floor((v - v.min(axis=0)) / cell).astype(np.int64), axis=0, return_inverse=True)[1].ravel()
41
+ nv = np.zeros((inv.max() + 1, 3))
42
+ np.add.at(nv, inv, v)
43
+ nv /= np.bincount(inv)[:, None]
44
+ nf = inv[f]
45
+ nf = nf[(nf[:, 0] != nf[:, 1]) & (nf[:, 1] != nf[:, 2]) & (nf[:, 0] != nf[:, 2])]
46
+ return trimesh.Trimesh(nv, nf, process=False)
47
+
48
+
49
+ def _cluster_to(mesh, faces):
50
+ """The finest grid that gets to `faces` or fewer (log-scale bisection on the cell size)."""
51
+ ext = float(mesh.extents.max()) or 1.0
52
+ lo, hi, best = ext / 2000, ext / 2, None
53
+ for _ in range(20):
54
+ cell = (lo * hi) ** 0.5
55
+ m = _clustered(mesh, cell)
56
+ if len(m.faces) > faces:
57
+ lo = cell
58
+ else:
59
+ hi, best = cell, m
60
+ return best
61
+
62
+
63
  def decimate(mesh, faces):
64
  """Reduce toward `faces`, re-welding between passes while that still helps.
65
 
66
+ Quadric decimation stalls on some CAD meshes (long sliver triangles: a SimReady
67
+ lamp stopped at ~21k faces whatever the settings). Then vertex clustering takes
68
+ over: cruder, but it always reduces and keeps thin parts that quadric drops.
69
  """
70
  color = base_color(mesh)
71
  out = _welded(mesh)
 
74
  if len(nxt.faces) > 0.95 * len(out.faces):
75
  break
76
  out = nxt
77
+ if len(out.faces) > faces:
78
+ out = _cluster_to(out, faces) or out
79
  out.visual = trimesh.visual.ColorVisuals(out, face_colors=np.tile(color, (len(out.faces), 1)))
80
  return out
81