rayli commited on
Commit
f51af83
·
verified ·
1 Parent(s): 63ee9bd

Use fast software renderer for upright previews

Browse files
Files changed (1) hide show
  1. app.py +203 -1
app.py CHANGED
@@ -2428,6 +2428,197 @@ def _set_preview_axes_equal(ax: Any, vertices: np.ndarray) -> None:
2428
  ax.set_box_aspect((1, 1, 1))
2429
 
2430
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2431
  def _render_up_direction_preview_image(
2432
  mesh: Any,
2433
  *,
@@ -2479,9 +2670,20 @@ def _render_up_direction_previews(
2479
  ) -> list[tuple[str, str]]:
2480
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
2481
  preview_dir = OUTPUT_ROOT / "up_direction_previews" / f"{mesh_path.stem}_{timestamp}"
 
 
 
 
 
 
 
 
 
 
2482
  try:
2483
- return _render_up_direction_previews_with_blender(
2484
  mesh_path=mesh_path,
 
2485
  output_dir=preview_dir,
2486
  )
2487
  except Exception:
 
2428
  ax.set_box_aspect((1, 1, 1))
2429
 
2430
 
2431
+ def _preview_face_colors(mesh: Any) -> np.ndarray:
2432
+ faces = np.asarray(mesh.faces, dtype=np.int64)
2433
+ fallback = np.tile(np.asarray([[178, 190, 205, 255]], dtype=np.uint8), (len(faces), 1))
2434
+ visual = getattr(mesh, "visual", None)
2435
+ if visual is None:
2436
+ return fallback
2437
+
2438
+ uv = getattr(visual, "uv", None)
2439
+ material = getattr(visual, "material", None)
2440
+ texture = None
2441
+ if material is not None:
2442
+ texture = getattr(material, "baseColorTexture", None) or getattr(material, "image", None)
2443
+ if uv is not None and texture is not None:
2444
+ try:
2445
+ tex = np.asarray(texture.convert("RGBA"), dtype=np.float32)
2446
+ uv_array = np.asarray(uv, dtype=np.float32)
2447
+ if uv_array.ndim == 2 and uv_array.shape[0] >= int(faces.max()) + 1:
2448
+ face_uv = uv_array[faces].mean(axis=1)
2449
+ face_uv = np.clip(face_uv, 0.0, 1.0)
2450
+ height, width = tex.shape[:2]
2451
+ x = np.rint(face_uv[:, 0] * (width - 1)).astype(np.int64)
2452
+ y = np.rint((1.0 - face_uv[:, 1]) * (height - 1)).astype(np.int64)
2453
+ colors = tex[y, x]
2454
+ base_factor = getattr(material, "baseColorFactor", None)
2455
+ if base_factor is not None:
2456
+ factor = np.asarray(base_factor, dtype=np.float32).reshape(-1)
2457
+ if factor.size >= 3:
2458
+ colors[:, :3] *= factor[:3]
2459
+ if factor.size >= 4:
2460
+ colors[:, 3] *= factor[3]
2461
+ return np.clip(colors, 0, 255).astype(np.uint8)
2462
+ except Exception:
2463
+ traceback.print_exc()
2464
+
2465
+ for attr_name in ("face_colors", "vertex_colors"):
2466
+ try:
2467
+ color_array = np.asarray(getattr(visual, attr_name))
2468
+ except Exception:
2469
+ continue
2470
+ if color_array.ndim != 2 or color_array.shape[0] == 0:
2471
+ continue
2472
+ colors = color_array.astype(np.float32, copy=False)
2473
+ if colors.max(initial=0.0) <= 1.0:
2474
+ colors = colors * 255.0
2475
+ if colors.shape[1] == 3:
2476
+ colors = np.concatenate(
2477
+ [colors, np.full((colors.shape[0], 1), 255.0, dtype=np.float32)],
2478
+ axis=1,
2479
+ )
2480
+ if attr_name == "face_colors" and colors.shape[0] == len(faces):
2481
+ return np.clip(colors[:, :4], 0, 255).astype(np.uint8)
2482
+ if attr_name == "vertex_colors" and colors.shape[0] >= int(faces.max()) + 1:
2483
+ return np.clip(colors[faces].mean(axis=1)[:, :4], 0, 255).astype(np.uint8)
2484
+ return fallback
2485
+
2486
+
2487
+ def _camera_basis_for_preview(
2488
+ *,
2489
+ pitch_deg: float = 58.0,
2490
+ azimuth_deg: float = 225.0,
2491
+ camera_distance: float = 2.25,
2492
+ ) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
2493
+ pitch = np.deg2rad(float(pitch_deg))
2494
+ azimuth = np.deg2rad(float(azimuth_deg))
2495
+ horizontal_distance = float(camera_distance) * np.sin(pitch)
2496
+ camera = np.asarray(
2497
+ [
2498
+ horizontal_distance * np.sin(azimuth),
2499
+ horizontal_distance * np.cos(azimuth),
2500
+ float(camera_distance) * np.cos(pitch),
2501
+ ],
2502
+ dtype=np.float32,
2503
+ )
2504
+ forward = -camera
2505
+ forward /= max(float(np.linalg.norm(forward)), 1e-8)
2506
+ world_up = np.asarray([0.0, 0.0, 1.0], dtype=np.float32)
2507
+ right = np.cross(forward, world_up)
2508
+ right /= max(float(np.linalg.norm(right)), 1e-8)
2509
+ up = np.cross(right, forward)
2510
+ up /= max(float(np.linalg.norm(up)), 1e-8)
2511
+ return camera, right.astype(np.float32), up.astype(np.float32), forward.astype(np.float32)
2512
+
2513
+
2514
+ def _render_up_direction_preview_software(
2515
+ mesh: Any,
2516
+ *,
2517
+ up_dir: str,
2518
+ face_colors: np.ndarray,
2519
+ output_path: Path,
2520
+ resolution: int,
2521
+ ) -> None:
2522
+ from PIL import Image, ImageDraw
2523
+
2524
+ reoriented_mesh, _ = reorient_mesh_to_z_up(mesh, up_dir)
2525
+ normalized_mesh, _, _ = normalize_mesh(reoriented_mesh)
2526
+ vertices = np.asarray(normalized_mesh.vertices, dtype=np.float32)
2527
+ faces = np.asarray(normalized_mesh.faces, dtype=np.int64)
2528
+ if vertices.size == 0 or faces.size == 0:
2529
+ raise ValueError("Cannot render an empty mesh preview.")
2530
+
2531
+ camera, right, up, forward = _camera_basis_for_preview()
2532
+ view_x = vertices @ right
2533
+ view_y = vertices @ up
2534
+ view_z = (vertices - camera) @ forward
2535
+ xy = np.stack([view_x, view_y], axis=1)
2536
+ xy_min = xy.min(axis=0)
2537
+ xy_max = xy.max(axis=0)
2538
+ extent = np.maximum(xy_max - xy_min, 1e-6)
2539
+ scale = float(resolution) * 0.84 / float(np.max(extent))
2540
+ center = (xy_min + xy_max) * 0.5
2541
+ screen = np.empty((vertices.shape[0], 2), dtype=np.float32)
2542
+ screen[:, 0] = (view_x - center[0]) * scale + float(resolution) * 0.5
2543
+ screen[:, 1] = float(resolution) * 0.5 - (view_y - center[1]) * scale
2544
+
2545
+ tri = vertices[faces]
2546
+ normals = np.cross(tri[:, 1] - tri[:, 0], tri[:, 2] - tri[:, 0])
2547
+ normal_lengths = np.linalg.norm(normals, axis=1, keepdims=True)
2548
+ normals = np.divide(normals, np.maximum(normal_lengths, 1e-8), out=np.zeros_like(normals))
2549
+ light_dir = np.asarray([-0.35, -0.45, 0.82], dtype=np.float32)
2550
+ light_dir /= np.linalg.norm(light_dir)
2551
+ shade = 0.64 + 0.36 * np.maximum(normals @ light_dir, 0.0)
2552
+ colors = face_colors[: len(faces)].astype(np.float32, copy=False)
2553
+ if colors.shape[1] == 3:
2554
+ alpha = np.ones((colors.shape[0], 1), dtype=np.float32) * 255.0
2555
+ colors = np.concatenate([colors, alpha], axis=1)
2556
+ rgb = np.clip(colors[:, :3] * shade[:, None], 0, 255)
2557
+ alpha = np.clip(colors[:, 3:4] / 255.0, 0.0, 1.0)
2558
+ background = np.asarray([248.0, 250.0, 252.0], dtype=np.float32)
2559
+ rgb = rgb * alpha + background[None, :] * (1.0 - alpha)
2560
+
2561
+ image = Image.new("RGB", (int(resolution), int(resolution)), tuple(background.astype(np.uint8)))
2562
+ draw = ImageDraw.Draw(image)
2563
+ screen_faces = screen[faces]
2564
+ face_depth = view_z[faces].mean(axis=1)
2565
+ x0 = screen_faces[:, 0, 0]
2566
+ y0 = screen_faces[:, 0, 1]
2567
+ x1 = screen_faces[:, 1, 0]
2568
+ y1 = screen_faces[:, 1, 1]
2569
+ x2 = screen_faces[:, 2, 0]
2570
+ y2 = screen_faces[:, 2, 1]
2571
+ area = np.abs((x1 - x0) * (y2 - y0) - (x2 - x0) * (y1 - y0))
2572
+ image_limit = float(resolution + 2)
2573
+ valid = (
2574
+ (screen_faces[:, :, 0].max(axis=1) >= -2.0)
2575
+ & (screen_faces[:, :, 0].min(axis=1) <= image_limit)
2576
+ & (screen_faces[:, :, 1].max(axis=1) >= -2.0)
2577
+ & (screen_faces[:, :, 1].min(axis=1) <= image_limit)
2578
+ & (area >= float(os.environ.get("UPRIGHT_PREVIEW_MIN_TRIANGLE_AREA", "0.25")))
2579
+ )
2580
+ if os.environ.get("UPRIGHT_PREVIEW_CULL_BACKFACES", "1").strip().lower() not in {"0", "false", "no"}:
2581
+ valid &= (normals @ forward) < 0.03
2582
+ valid_face_ids = np.flatnonzero(valid)
2583
+ order = valid_face_ids[np.argsort(face_depth[valid_face_ids])[::-1]]
2584
+ for face_id in order:
2585
+ pts = screen_faces[face_id]
2586
+ x0, y0 = pts[0]
2587
+ x1, y1 = pts[1]
2588
+ x2, y2 = pts[2]
2589
+ fill = tuple(np.rint(rgb[face_id]).astype(np.uint8).tolist())
2590
+ draw.polygon(
2591
+ [(float(x0), float(y0)), (float(x1), float(y1)), (float(x2), float(y2))],
2592
+ fill=fill,
2593
+ )
2594
+
2595
+ output_path.parent.mkdir(parents=True, exist_ok=True)
2596
+ image.save(output_path)
2597
+
2598
+
2599
+ def _render_up_direction_previews_software(
2600
+ *,
2601
+ mesh_path: Path,
2602
+ mesh: Any,
2603
+ output_dir: Path,
2604
+ ) -> list[tuple[str, str]]:
2605
+ resolution = int(os.environ.get("UPRIGHT_PREVIEW_RESOLUTION", "288"))
2606
+ output_dir.mkdir(parents=True, exist_ok=True)
2607
+ face_colors = _preview_face_colors(mesh)
2608
+ gallery_items: list[tuple[str, str]] = []
2609
+ for up_dir in UP_DIR_CHOICES:
2610
+ output_path = output_dir / f"up_{_up_dir_slug(up_dir)}.png"
2611
+ _render_up_direction_preview_software(
2612
+ mesh,
2613
+ up_dir=up_dir,
2614
+ face_colors=face_colors,
2615
+ output_path=output_path,
2616
+ resolution=resolution,
2617
+ )
2618
+ gallery_items.append((str(output_path), f"{up_dir} up"))
2619
+ return gallery_items
2620
+
2621
+
2622
  def _render_up_direction_preview_image(
2623
  mesh: Any,
2624
  *,
 
2670
  ) -> list[tuple[str, str]]:
2671
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
2672
  preview_dir = OUTPUT_ROOT / "up_direction_previews" / f"{mesh_path.stem}_{timestamp}"
2673
+ preview_renderer = os.environ.get("UPRIGHT_PREVIEW_RENDERER", "software").strip().lower()
2674
+ if preview_renderer == "blender":
2675
+ try:
2676
+ return _render_up_direction_previews_with_blender(
2677
+ mesh_path=mesh_path,
2678
+ output_dir=preview_dir,
2679
+ )
2680
+ except Exception:
2681
+ traceback.print_exc()
2682
+
2683
  try:
2684
+ return _render_up_direction_previews_software(
2685
  mesh_path=mesh_path,
2686
+ mesh=mesh,
2687
  output_dir=preview_dir,
2688
  )
2689
  except Exception: