Spaces:
Sleeping
Sleeping
| """Export mesh data (vertices + triangles) to glTF binary (.glb) format.""" | |
| from __future__ import annotations | |
| import numpy as np | |
| import pygltflib | |
| def _compute_normals(vertices: np.ndarray, triangles: np.ndarray) -> np.ndarray: | |
| """Compute smooth per-vertex normals by averaging face normals.""" | |
| normals = np.zeros_like(vertices, dtype=np.float32) | |
| v0 = vertices[triangles[:, 0]] | |
| v1 = vertices[triangles[:, 1]] | |
| v2 = vertices[triangles[:, 2]] | |
| face_normals = np.cross(v1 - v0, v2 - v0) | |
| # Accumulate face normals to each vertex | |
| for i in range(3): | |
| np.add.at(normals, triangles[:, i], face_normals) | |
| # Normalize | |
| lengths = np.linalg.norm(normals, axis=1, keepdims=True) | |
| lengths = np.maximum(lengths, 1e-10) | |
| normals /= lengths | |
| return normals.astype(np.float32) | |
| def mesh_to_glb(vertices: np.ndarray, triangles: np.ndarray) -> bytes: | |
| """Convert vertices and triangles to a binary glTF (.glb) buffer. | |
| Includes computed vertex normals for proper lighting/shading. | |
| Args: | |
| vertices: Mesh vertices with shape (N, 3), float64. | |
| triangles: Triangle indices with shape (M, 3), int32. | |
| Returns: | |
| Bytes of the .glb file. | |
| """ | |
| vertices_f32 = vertices.astype(np.float32) | |
| triangles_u32 = triangles.astype(np.uint32) | |
| normals_f32 = _compute_normals(vertices_f32, triangles_u32) | |
| # Byte buffers | |
| vert_bytes = vertices_f32.tobytes() | |
| norm_bytes = normals_f32.tobytes() | |
| tri_bytes = triangles_u32.tobytes() | |
| # Align each buffer to 4 bytes | |
| def pad4(b: bytes) -> bytes: | |
| pad = (4 - len(b) % 4) % 4 | |
| return b + b"\x00" * pad | |
| vert_padded = pad4(vert_bytes) | |
| norm_padded = pad4(norm_bytes) | |
| blob = vert_padded + norm_padded + tri_bytes | |
| total_bytes = len(blob) | |
| norm_offset = len(vert_padded) | |
| tri_offset = norm_offset + len(norm_padded) | |
| v_min = vertices_f32.min(axis=0).tolist() | |
| v_max = vertices_f32.max(axis=0).tolist() | |
| gltf = pygltflib.GLTF2( | |
| scene=0, | |
| scenes=[pygltflib.Scene(nodes=[0])], | |
| nodes=[pygltflib.Node(mesh=0)], | |
| meshes=[ | |
| pygltflib.Mesh( | |
| primitives=[ | |
| pygltflib.Primitive( | |
| attributes=pygltflib.Attributes(POSITION=0, NORMAL=1), | |
| indices=2, | |
| ) | |
| ] | |
| ) | |
| ], | |
| accessors=[ | |
| # Accessor 0: vertex positions | |
| pygltflib.Accessor( | |
| bufferView=0, | |
| componentType=pygltflib.FLOAT, | |
| count=len(vertices_f32), | |
| type=pygltflib.VEC3, | |
| max=v_max, | |
| min=v_min, | |
| ), | |
| # Accessor 1: vertex normals | |
| pygltflib.Accessor( | |
| bufferView=1, | |
| componentType=pygltflib.FLOAT, | |
| count=len(normals_f32), | |
| type=pygltflib.VEC3, | |
| ), | |
| # Accessor 2: triangle indices | |
| pygltflib.Accessor( | |
| bufferView=2, | |
| componentType=pygltflib.UNSIGNED_INT, | |
| count=triangles_u32.size, | |
| type=pygltflib.SCALAR, | |
| max=[int(triangles_u32.max())], | |
| min=[int(triangles_u32.min())], | |
| ), | |
| ], | |
| bufferViews=[ | |
| # BufferView 0: positions | |
| pygltflib.BufferView( | |
| buffer=0, | |
| byteOffset=0, | |
| byteLength=len(vert_bytes), | |
| target=pygltflib.ARRAY_BUFFER, | |
| ), | |
| # BufferView 1: normals | |
| pygltflib.BufferView( | |
| buffer=0, | |
| byteOffset=norm_offset, | |
| byteLength=len(norm_bytes), | |
| target=pygltflib.ARRAY_BUFFER, | |
| ), | |
| # BufferView 2: indices | |
| pygltflib.BufferView( | |
| buffer=0, | |
| byteOffset=tri_offset, | |
| byteLength=len(tri_bytes), | |
| target=pygltflib.ELEMENT_ARRAY_BUFFER, | |
| ), | |
| ], | |
| buffers=[pygltflib.Buffer(byteLength=total_bytes)], | |
| ) | |
| gltf.set_binary_blob(blob) | |
| return b"".join(gltf.save_to_bytes()) | |