File size: 3,916 Bytes
07c2969
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
import argparse, io, math, os, sys
from pathlib import Path
from typing import Tuple

import numpy as np, requests
from PIL import Image
import trimesh, pyrender
from trimesh.transformations import translation_matrix, rotation_matrix  # NEW

os.environ.setdefault("PYOPENGL_PLATFORM", "egl")


def download_to_memory(url: str) -> bytes:
    resp = requests.get(url, timeout=30)
    resp.raise_for_status()
    return resp.content


def load_mesh(data: bytes) -> trimesh.Trimesh:
    return trimesh.load(io.BytesIO(data), file_type="glb", force="mesh")


def build_scene(mesh: trimesh.Trimesh) -> Tuple[pyrender.Scene, float]:
    tm_mesh = pyrender.Mesh.from_trimesh(mesh, smooth=False)
    scene = pyrender.Scene(bg_color=[1, 1, 1, 0])
    scene.add(tm_mesh)

    bb = mesh.bounding_box_oriented.extents          # fixed name
    if not np.all(bb):
        bb = mesh.extents
    radius = np.linalg.norm(bb) * 0.6
    return scene, radius


def add_lighting(scene: pyrender.Scene, radius: float) -> None:
    key  = pyrender.PointLight(color=np.ones(3), intensity=40.0)
    fill = pyrender.PointLight(color=np.ones(3), intensity=20.0)
    back = pyrender.PointLight(color=np.ones(3), intensity=10.0)

    scene.add(key,  pose=translation_matrix([ radius,  radius,  radius]))
    scene.add(fill, pose=translation_matrix([-radius,  radius,  radius]))
    scene.add(back, pose=translation_matrix([     0, -radius, -radius]))


def setup_camera(scene: pyrender.Scene, radius: float) -> None:
    cam = pyrender.PerspectiveCamera(yfov=np.radians(45.0))
    cam_pose = translation_matrix([0, 0, radius * 2.5])
    scene.add(cam, pose=cam_pose)


def render_thumbnail(scene: pyrender.Scene, size: int) -> Image.Image:
    r = pyrender.OffscreenRenderer(viewport_width=size, viewport_height=size)
    try:
        color, _ = r.render(scene)
    finally:
        r.delete()
    return Image.fromarray(color)


def generate_thumbnail(url: str, out_path: Path, size: int = 512) -> None:
    raw = download_to_memory(url)
    mesh = load_mesh(raw)
    
    # Get mesh dimensions before any transformations
    original_extents = mesh.extents
    longest_dimension = np.max(original_extents)
    
    # Print dimension info
    print(f"Mesh dimensions (X, Y, Z): {original_extents}")
    print(f"Longest dimension: {longest_dimension:.4f}")
    
    # Scaling constant - you can use this to normalize models to a target size
    target_size = 2.5  # Target longest dimension
    scale_factor = target_size / longest_dimension
    print(f"Scale factor to normalize to {target_size}: {scale_factor:.4f}")
    
    # Calculate radius BEFORE scaling for consistent camera/lighting positioning
    bb = mesh.bounding_box_oriented.extents
    if not np.all(bb):
        bb = mesh.extents
    fixed_radius = np.linalg.norm(bb) * 0.6
    
    mesh.apply_translation(-mesh.bounding_box.centroid)
    
    # Apply the scaling transformation
    mesh.apply_scale(scale_factor)
    
    # Rotate 45 degrees to the left (around Y-axis)
    rotation = rotation_matrix(np.radians(30), [0.3, -0.5, 0])
    mesh.apply_transform(rotation)

    # Build scene but use fixed radius for camera/lighting
    tm_mesh = pyrender.Mesh.from_trimesh(mesh, smooth=False)
    scene = pyrender.Scene(bg_color=[0.15, 0.15, 0.15, 1])  # Gray background
    scene.add(tm_mesh)
    
    add_lighting(scene, fixed_radius)
    setup_camera(scene, fixed_radius)
    img = render_thumbnail(scene, size)

    img.save(out_path, "PNG")
    print(f"Saved thumbnail → {out_path.resolve()}")


def parse_args(argv):
    p = argparse.ArgumentParser(description="Render a GLB file to a PNG thumbnail")
    p.add_argument("url"), p.add_argument("output")
    p.add_argument("size", nargs="?", type=int, default=512)
    return p.parse_args(argv)


if __name__ == "__main__":
    args = parse_args(sys.argv[1:])
    generate_thumbnail(args.url, Path(args.output), args.size)