Convert a single mesh file to GLB.
(input_path: Path, output_path: Path)
| 67 | ) |
| 68 | mesh.fix_normals() |
| 69 | |
| 70 | # Rotate -90 degrees around X for GLTF Y-up -> Unreal Z-up |
| 71 | rotation_matrix = trimesh.transformations.rotation_matrix(-np.radians(90), [1, 0, 0]) |
| 72 | mesh.apply_transform(rotation_matrix) |
| 73 | |
| 74 | print(f" Cleaned: {len(mesh.vertices)} vertices, {len(mesh.faces)} faces") |
| 75 | return mesh |
| 76 | |
| 77 | |
| 78 | _SCRIPT_MTIME = Path(__file__).stat().st_mtime |
| 79 | |
| 80 | |
| 81 | def glb_up_to_date(output_glb: Path, source_path: Path) -> bool: |
| 82 | """A GLB is stale when older than its source mesh OR older than this |
| 83 | script -- conversion fixes ship with the script, so GLBs produced by an |
| 84 | older version must be regenerated.""" |
| 85 | if not output_glb.exists(): |
| 86 | return False |
| 87 | mtime = output_glb.stat().st_mtime |
| 88 | return mtime > source_path.stat().st_mtime and mtime > _SCRIPT_MTIME |
| 89 | |
| 90 | |
| 91 | def convert_mesh(input_path: Path, output_path: Path) -> bool: |
| 92 | """Convert a single mesh file to GLB.""" |
| 93 | print(f"\n Converting: {input_path.name} -> {output_path.name}") |
| 94 | |
| 95 | try: |
| 96 | mesh = trimesh.load(str(input_path), force='mesh') |
| 97 | |
| 98 | if not isinstance(mesh, trimesh.Trimesh): |
| 99 | print(f" x Not a valid mesh: {input_path.name}") |
| 100 | return False |
| 101 | |
| 102 | cleaned_mesh = clean_mesh(mesh) |
| 103 | |
| 104 | # Strip embedded materials/textures to prevent Unreal's Interchange importer |
| 105 | # from creating a Texture2D instead of a StaticMesh. |
| 106 | # Preserve UV coordinates so textures can be applied via material instances. |
| 107 | if hasattr(cleaned_mesh.visual, 'uv') and cleaned_mesh.visual.uv is not None: |
| 108 | uv = cleaned_mesh.visual.uv.copy() |
| 109 | cleaned_mesh.visual = trimesh.visual.TextureVisuals(uv=uv) |
| 110 | else: |
| 111 | cleaned_mesh.visual = trimesh.visual.ColorVisuals() |
no test coverage detected