import argparse import os import platform import sys from pathlib import Path # pyrender needs an offscreen GL backend (EGL) on a headless Linux box. Must be # set before pyrender is imported (it is imported lazily further down). A # desktop session (DISPLAY set) is left untouched. if platform.system() == "Linux" and not os.environ.get("DISPLAY"): os.environ.setdefault("PYOPENGL_PLATFORM", "egl") import numpy as np sys.path.insert(0, str(Path(__file__).resolve().parent)) from draco_glb import load_glb_scene def angled_camera_pose(center, radius): """A 3/4 view pose looking at ``center`` (Z is treated as the up axis). Looking straight down a model's main axis (the old behaviour) often shows only a featureless silhouette, so frame it from an elevated corner instead. """ if radius < 1e-6: radius = 1.0 distance = radius * 2.2 azimuth = np.radians(35.0) elevation = np.radians(25.0) offset = np.array([ distance * np.cos(elevation) * np.sin(azimuth), -distance * np.cos(elevation) * np.cos(azimuth), distance * np.sin(elevation), ]) eye = center + offset forward = center - eye forward = forward / np.linalg.norm(forward) up = np.array([0.0, 0.0, 1.0]) if abs(np.dot(forward, up)) > 0.99: up = np.array([0.0, 1.0, 0.0]) right = np.cross(forward, up) right = right / np.linalg.norm(right) true_up = np.cross(right, forward) pose = np.eye(4) pose[:3, 0] = right pose[:3, 1] = true_up pose[:3, 2] = -forward pose[:3, 3] = eye return pose def load_flat_mesh(path): import trimesh print("Loading scene...") # load_glb_scene transparently decodes KHR_draco_mesh_compression, which a # plain trimesh.load cannot do (it would return all-zero vertices). loaded = load_glb_scene(path) print(f"Loaded object type: {type(loaded)}") if isinstance(loaded, trimesh.Trimesh): return loaded if not isinstance(loaded, trimesh.Scene): raise RuntimeError( f"Unsupported object type: {type(loaded)}" ) print(f"Scene geometries: {len(loaded.geometry)}") if len(loaded.geometry) == 0: raise RuntimeError( "Scene contains no geometry" ) # New trimesh versions if hasattr(loaded, "to_mesh"): try: print("Using Scene.to_mesh()") mesh = loaded.to_mesh() if mesh is not None and not mesh.is_empty: return mesh except Exception as e: print(f"Scene.to_mesh failed: {e}") # Fallback try: print("Using dump(concatenate=True)") mesh = loaded.dump(concatenate=True) if mesh is not None and not mesh.is_empty: return mesh except Exception as e: print(f"dump(concatenate=True) failed: {e}") # Nuclear option: manually bake transforms print("Manually baking scene graph...") meshes = [] for node_name in loaded.graph.nodes_geometry: try: transform, geom_name = loaded.graph[node_name] geom = loaded.geometry[geom_name].copy() if transform is not None: geom.apply_transform(transform) meshes.append(geom) except Exception as e: print(f"Failed node {node_name}: {e}") if not meshes: raise RuntimeError( "No mesh data extracted from scene" ) mesh = trimesh.util.concatenate(meshes) return mesh def repair_mesh(mesh): print("\nRepairing mesh...") try: mesh.remove_unreferenced_vertices() except: pass try: mesh.merge_vertices() except: pass try: mesh.remove_duplicate_faces() except: pass try: mesh.remove_degenerate_faces() except: pass # Remove NaN/Inf vertices try: valid = np.isfinite(mesh.vertices).all(axis=1) if not valid.all(): removed = (~valid).sum() print( f"Removed {removed} invalid vertices" ) mesh.update_vertices(valid) except Exception as e: print(f"Vertex cleanup failed: {e}") try: mesh.fix_normals() except: pass return mesh def center_and_scale(mesh, target_size=5.0): bounds = mesh.bounds vmin = bounds[0] vmax = bounds[1] extents = vmax - vmin center = (vmin + vmax) * 0.5 mesh.apply_translation(-center) largest = float(np.max(extents)) if largest > 1e-12: scale = target_size / largest mesh.apply_scale(scale) print( f"Scale factor: {scale:.6f}" ) return mesh def print_diagnostics(mesh): print("\n===== MESH INFO =====") print( f"Vertices: {len(mesh.vertices):,}" ) print( f"Faces: {len(mesh.faces):,}" ) print( f"Empty: {mesh.is_empty}" ) print( f"Watertight: {mesh.is_watertight}" ) print( f"Bounds:\n{mesh.bounds}" ) print( f"Extents: {mesh.extents}" ) print( f"Centroid: {mesh.centroid}" ) try: print( f"Volume: {mesh.volume}" ) except: pass print("=====================\n") def export_debug_render(mesh): try: import pyrender from PIL import Image scene = pyrender.Scene( bg_color=[30, 30, 30, 255], ambient_light=[0.5, 0.5, 0.5] ) material = ( pyrender.MetallicRoughnessMaterial( metallicFactor=0.0, roughnessFactor=0.8, baseColorFactor=[ 0.85, 0.85, 0.85, 1.0 ] ) ) render_mesh = ( pyrender.Mesh.from_trimesh( mesh, material=material, smooth=False ) ) scene.add(render_mesh) bbox = mesh.bounds center = bbox.mean(axis=0) radius = np.linalg.norm( bbox[1] - bbox[0] ) if radius < 0.001: radius = 1.0 camera = pyrender.PerspectiveCamera( yfov=np.pi / 3.0 ) pose = angled_camera_pose(center, radius * 0.5) scene.add(camera, pose=pose) light = pyrender.DirectionalLight( color=np.ones(3), intensity=8.0 ) scene.add(light, pose=pose) renderer = ( pyrender.OffscreenRenderer( 1024, 1024 ) ) color, depth = renderer.render(scene) Image.fromarray(color).save( "debug_render.png" ) print( "Saved debug_render.png" ) renderer.delete() except Exception as e: print( f"Debug render failed: {e}" ) def launch_viewer(mesh): import pyrender scene = pyrender.Scene( bg_color=[20, 20, 25, 255], ambient_light=[0.5, 0.5, 0.5] ) material = ( pyrender.MetallicRoughnessMaterial( metallicFactor=0.0, roughnessFactor=0.9, baseColorFactor=[ 0.85, 0.85, 0.85, 1.0 ] ) ) render_mesh = ( pyrender.Mesh.from_trimesh( mesh, material=material, smooth=False ) ) scene.add(render_mesh) bbox = mesh.bounds center = bbox.mean(axis=0) radius = np.linalg.norm( bbox[1] - bbox[0] ) if radius < 0.001: radius = 1.0 camera = pyrender.PerspectiveCamera( yfov=np.pi / 3.0 ) pose = angled_camera_pose(center, radius * 0.5) scene.add(camera, pose=pose) light = pyrender.DirectionalLight( color=np.ones(3), intensity=8.0 ) scene.add(light, pose=pose) print( f"Viewer center: {center}" ) print( f"Viewer radius: {radius}" ) pyrender.Viewer( scene, viewport_size=(1400, 900), use_raymond_lighting=True, point_size=2.0 ) def main(): parser = argparse.ArgumentParser( description="GLB Debug Viewer" ) parser.add_argument( "--model", required=True ) parser.add_argument( "--visualize", action="store_true" ) parser.add_argument( "--export-obj" ) args = parser.parse_args() model_path = Path(args.model) if not model_path.exists(): print( f"File not found: {model_path}", file=sys.stderr ) sys.exit(1) try: import trimesh mesh = load_flat_mesh( model_path ) mesh = repair_mesh( mesh ) print_diagnostics( mesh ) mesh = center_and_scale( mesh, target_size=5.0 ) export_debug_render( mesh ) if args.export_obj: export_path = Path( args.export_obj ) export_path.parent.mkdir( parents=True, exist_ok=True ) mesh.export(export_path) print( f"Exported OBJ: {export_path}" ) if args.visualize: try: launch_viewer(mesh) except Exception as e: print( f"Pyrender failed: {e}" ) print( "Trying trimesh viewer..." ) mesh.show() except Exception as e: print( f"\nFATAL ERROR: {e}", file=sys.stderr ) import traceback traceback.print_exc() sys.exit(1) if __name__ == "__main__": main()