Rawal Khirodkar commited on
Commit
a90c10a
·
1 Parent(s): b10b215

Pointmap: switch back to triangulated mesh — MoGe-2 recipe (UV-textured PBR, smooth normals via lazy compute)

Browse files
Files changed (1) hide show
  1. app.py +52 -42
app.py CHANGED
@@ -236,60 +236,70 @@ def _triangulate_grid(pointmap_hwc: np.ndarray, mask_hw: np.ndarray,
236
 
237
 
238
  def _make_glb(image_pil_texture: Image.Image, pointmap_hwc: np.ndarray,
239
- mask_hw: np.ndarray, max_points: int = 25_000) -> str:
240
- """Render the pointmap as a cloud of small icospheres one per pixel,
241
- coloured by the source image. With KHR_materials_unlit injected post-
242
- export, each ball renders as a flat-coloured 3D primitive (no lighting
243
- variation), giving a clean "candy ball cloud" surface."""
 
 
 
 
244
  H, W = pointmap_hwc.shape[:2]
 
 
 
245
  z = pointmap_hwc[:, :, 2]
246
  valid = mask_hw & np.isfinite(pointmap_hwc).all(axis=2) & (z > 0.05) & (z < 25.0)
 
247
  yy, xx = np.where(valid)
248
- if len(yy) > max_points:
249
- idx = np.random.default_rng(0).choice(len(yy), max_points, replace=False)
250
- yy, xx = yy[idx], xx[idx]
251
 
252
- pts = pointmap_hwc[yy, xx].astype(np.float32)
253
- image_native = np.asarray(image_pil_texture.resize((W, H), Image.LANCZOS))
254
- cols = image_native[yy, xx].astype(np.uint8)
255
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
256
  flip = np.array([1.0, -1.0, -1.0], dtype=np.float32)
257
- pts = pts * flip
258
- centroid = pts.mean(axis=0).astype(np.float32) if len(pts) else np.zeros(3, np.float32)
259
- pts = pts - centroid
260
-
261
- # Sphere radius: surface-spacing estimate = bbox diagonal / sqrt(N).
262
- # Factor 1.0 → balls just kiss neighbours; 1.5 → distinct overlap.
263
- if len(pts) >= 2:
264
- extent_diag = float(np.linalg.norm(np.ptp(pts, axis=0)))
265
- r = max(extent_diag / np.sqrt(len(pts)) * 0.5, 1e-4)
266
- else:
267
- r = 0.005
268
-
269
- # Smooth icosphere template (subdivisions=1 → 42 verts, 80 faces).
270
- sphere = trimesh.creation.icosphere(subdivisions=1, radius=r)
271
- sv = sphere.vertices.astype(np.float32)
272
- sf = sphere.faces.astype(np.int64)
273
- V, F = len(sv), len(sf)
274
- N = len(pts)
275
-
276
- # Vectorized instancing: place a sphere at every point.
277
- verts = (sv[None, :, :] + pts[:, None, :]).reshape(-1, 3)
278
- face_off = (np.arange(N, dtype=np.int64) * V)[:, None, None]
279
- faces = (sf[None, :, :] + face_off).reshape(-1, 3)
280
- vc = np.empty((N, V, 4), dtype=np.uint8)
281
- vc[..., :3] = cols[:, None, :]
282
- vc[..., 3] = 255
283
- vertex_colors = vc.reshape(-1, 4)
284
 
285
  mesh = trimesh.Trimesh(
286
- vertices=verts, faces=faces,
287
- vertex_colors=vertex_colors,
288
- process=False,
289
  )
 
 
 
 
 
290
  out_path = tempfile.NamedTemporaryFile(delete=False, suffix=".glb").name
291
  mesh.export(out_path)
292
- _glb_inject_unlit(out_path) # → flat-coloured balls, no shading
293
  return out_path
294
 
295
 
 
236
 
237
 
238
  def _make_glb(image_pil_texture: Image.Image, pointmap_hwc: np.ndarray,
239
+ mask_hw: np.ndarray, max_edge: float = 0.04) -> str:
240
+ """Triangulated mesh, UV-textured with the input imageMoGe-2's recipe.
241
+
242
+ Each valid pixel = vertex; adjacent valid pixels form quads → 2 triangles.
243
+ Triangles whose edges exceed `max_edge` (meters) are dropped to kill
244
+ stretched skin at depth jumps. The input image is used as the GLB's albedo
245
+ texture (per-triangle PBR sampling), and trimesh's lazy vertex_normals get
246
+ exported so Three.js applies smooth shading instead of flat facets.
247
+ """
248
  H, W = pointmap_hwc.shape[:2]
249
+ image_native = image_pil_texture.resize((W, H), Image.LANCZOS)
250
+
251
+ # Triangulate the (H, W) grid over the foreground mask.
252
  z = pointmap_hwc[:, :, 2]
253
  valid = mask_hw & np.isfinite(pointmap_hwc).all(axis=2) & (z > 0.05) & (z < 25.0)
254
+ idx_map = np.full((H, W), -1, dtype=np.int64)
255
  yy, xx = np.where(valid)
256
+ idx_map[yy, xx] = np.arange(len(yy))
 
 
257
 
258
+ verts = pointmap_hwc[yy, xx].astype(np.float32)
259
+ uvs = np.stack([xx / max(W - 1, 1), yy / max(H - 1, 1)], axis=1).astype(np.float32)
 
260
 
261
+ a = idx_map[:-1, :-1]; b = idx_map[:-1, 1:]
262
+ c = idx_map[1:, :-1]; d = idx_map[1:, 1:]
263
+ quad_valid = (a != -1) & (b != -1) & (c != -1) & (d != -1)
264
+ a_v, b_v, c_v, d_v = a[quad_valid], b[quad_valid], c[quad_valid], d[quad_valid]
265
+ tri1 = np.stack([a_v, c_v, b_v], axis=1)
266
+ tri2 = np.stack([b_v, c_v, d_v], axis=1)
267
+ faces = np.concatenate([tri1, tri2], axis=0)
268
+
269
+ p0 = verts[faces[:, 0]]; p1 = verts[faces[:, 1]]; p2 = verts[faces[:, 2]]
270
+ e01 = np.linalg.norm(p1 - p0, axis=1)
271
+ e12 = np.linalg.norm(p2 - p1, axis=1)
272
+ e20 = np.linalg.norm(p0 - p2, axis=1)
273
+ keep = (e01 < max_edge) & (e12 < max_edge) & (e20 < max_edge)
274
+ faces = faces[keep].astype(np.int64)
275
+
276
+ # MoGe-2: y/z flip on positions, v-flip on UVs.
277
  flip = np.array([1.0, -1.0, -1.0], dtype=np.float32)
278
+ verts = verts * flip
279
+ centroid = verts.mean(axis=0).astype(np.float32) if len(verts) else np.zeros(3, np.float32)
280
+ verts = verts - centroid
281
+
282
+ uvs = uvs * np.array([1.0, -1.0], dtype=np.float32) + np.array([0.0, 1.0], dtype=np.float32)
283
+
284
+ # PBR with image as albedo texture — MoGe's exact material settings.
285
+ material = trimesh.visual.material.PBRMaterial(
286
+ baseColorTexture=image_native,
287
+ metallicFactor=0.5,
288
+ roughnessFactor=1.0,
289
+ doubleSided=True,
290
+ )
291
+ visual = trimesh.visual.texture.TextureVisuals(uv=uvs, material=material)
 
 
 
 
 
 
 
 
 
 
 
 
 
292
 
293
  mesh = trimesh.Trimesh(
294
+ vertices=verts, faces=faces, visual=visual, process=False,
 
 
295
  )
296
+ # Touch vertex_normals so trimesh caches them; the GLB exporter reads
297
+ # the cached normals and writes them into the file → smooth shading
298
+ # in Three.js (instead of the per-face fallback that looked like facets).
299
+ _ = mesh.vertex_normals
300
+
301
  out_path = tempfile.NamedTemporaryFile(delete=False, suffix=".glb").name
302
  mesh.export(out_path)
 
303
  return out_path
304
 
305