Spaces:
Running on Zero
Running on Zero
Use fast software renderer for upright previews
Browse files
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
|
| 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:
|