| from typing import Tuple, Dict |
| import numpy as np |
| from trimesh import grouping, util, remesh |
| import struct |
| import re |
| from plyfile import PlyData, PlyElement |
|
|
|
|
| def read_ply(filename): |
| """ |
| Read a PLY file and return vertices, triangle faces, and quad faces. |
| |
| Args: |
| filename (str): The file path to read from. |
| |
| Returns: |
| vertices (np.ndarray): Array of shape [N, 3] containing vertex positions. |
| tris (np.ndarray): Array of shape [M, 3] containing triangle face indices (empty if none). |
| quads (np.ndarray): Array of shape [K, 4] containing quad face indices (empty if none). |
| """ |
| with open(filename, 'rb') as f: |
| |
| header_bytes = b"" |
| while True: |
| line = f.readline() |
| if not line: |
| raise ValueError("PLY header not found") |
| header_bytes += line |
| if b"end_header" in line: |
| break |
| header = header_bytes.decode('utf-8') |
| |
| |
| is_ascii = "ascii" in header |
| |
| |
| vertex_match = re.search(r'element vertex (\d+)', header) |
| if vertex_match: |
| num_vertices = int(vertex_match.group(1)) |
| else: |
| raise ValueError("Vertex count not found in header") |
| |
| face_match = re.search(r'element face (\d+)', header) |
| if face_match: |
| num_faces = int(face_match.group(1)) |
| else: |
| raise ValueError("Face count not found in header") |
| |
| vertices = [] |
| tris = [] |
| quads = [] |
| |
| if is_ascii: |
| |
| for _ in range(num_vertices): |
| line = f.readline().decode('utf-8').strip() |
| if not line: |
| continue |
| parts = line.split() |
| vertices.append([float(parts[0]), float(parts[1]), float(parts[2])]) |
| |
| |
| for _ in range(num_faces): |
| line = f.readline().decode('utf-8').strip() |
| if not line: |
| continue |
| parts = line.split() |
| count = int(parts[0]) |
| indices = list(map(int, parts[1:])) |
| if count == 3: |
| tris.append(indices) |
| elif count == 4: |
| quads.append(indices) |
| else: |
| |
| pass |
| else: |
| |
| |
| for _ in range(num_vertices): |
| data = f.read(12) |
| if len(data) < 12: |
| raise ValueError("Insufficient vertex data") |
| v = struct.unpack('<fff', data) |
| vertices.append(v) |
| |
| |
| for _ in range(num_faces): |
| |
| count_data = f.read(1) |
| if len(count_data) < 1: |
| raise ValueError("Failed to read face vertex count") |
| count = struct.unpack('<B', count_data)[0] |
| if count == 3: |
| data = f.read(12) |
| if len(data) < 12: |
| raise ValueError("Insufficient data for triangle face") |
| indices = struct.unpack('<3i', data) |
| tris.append(indices) |
| elif count == 4: |
| data = f.read(16) |
| if len(data) < 16: |
| raise ValueError("Insufficient data for quad face") |
| indices = struct.unpack('<4i', data) |
| quads.append(indices) |
| else: |
| |
| data = f.read(count * 4) |
| |
| raise ValueError(f"Unsupported face with {count} vertices") |
| |
| |
| vertices = np.array(vertices, dtype=np.float32) |
| tris = np.array(tris, dtype=np.int32) if len(tris) > 0 else np.empty((0, 3), dtype=np.int32) |
| quads = np.array(quads, dtype=np.int32) if len(quads) > 0 else np.empty((0, 4), dtype=np.int32) |
| |
| return vertices, tris, quads |
|
|
|
|
| def write_ply( |
| filename: str, |
| vertices: np.ndarray, |
| tris: np.ndarray, |
| quads: np.ndarray, |
| vertex_colors: np.ndarray = None, |
| ascii: bool = False |
| ): |
| """ |
| Write a mesh to a PLY file, with the option to save in ASCII or binary format, |
| and optional per-vertex colors. |
| |
| Args: |
| filename (str): The filename to write to. |
| vertices (np.ndarray): [N, 3] The vertex positions. |
| tris (np.ndarray): [M, 3] The triangle indices. |
| quads (np.ndarray): [K, 4] The quad indices. |
| vertex_colors (np.ndarray, optional): [N, 3] or [N, 4] UInt8 colors for each vertex (RGB or RGBA). |
| ascii (bool): If True, write in ASCII format; otherwise binary little-endian. |
| """ |
| import struct |
|
|
| num_vertices = len(vertices) |
| num_faces = len(tris) + len(quads) |
|
|
| |
| header_lines = [ |
| "ply", |
| f"format {'ascii 1.0' if ascii else 'binary_little_endian 1.0'}", |
| f"element vertex {num_vertices}", |
| "property float x", |
| "property float y", |
| "property float z", |
| ] |
|
|
| |
| has_color = vertex_colors is not None |
| if has_color: |
| |
| header_lines += [ |
| "property uchar red", |
| "property uchar green", |
| "property uchar blue", |
| ] |
| |
| if vertex_colors.shape[1] == 4: |
| header_lines.append("property uchar alpha") |
|
|
| header_lines += [ |
| f"element face {num_faces}", |
| "property list uchar int vertex_index", |
| "end_header", |
| "" |
| ] |
| header = "\n".join(header_lines) |
|
|
| mode = 'w' if ascii else 'wb' |
| with open(filename, mode) as f: |
| |
| if ascii: |
| f.write(header) |
| else: |
| f.write(header.encode('utf-8')) |
|
|
| |
| for i, v in enumerate(vertices): |
| if ascii: |
| line = f"{v[0]} {v[1]} {v[2]}" |
| if has_color: |
| col = vertex_colors[i] |
| line += ' ' + ' '.join(str(int(c)) for c in col) |
| f.write(line + '\n') |
| else: |
| |
| f.write(struct.pack('<fff', *v)) |
| if has_color: |
| col = vertex_colors[i] |
| |
| if col.shape[0] == 3: |
| f.write(struct.pack('<BBB', *col)) |
| else: |
| f.write(struct.pack('<BBBB', *col)) |
|
|
| |
| if ascii: |
| for tri in tris: |
| f.write(f"3 {tri[0]} {tri[1]} {tri[2]}\n") |
| for quad in quads: |
| f.write(f"4 {quad[0]} {quad[1]} {quad[2]} {quad[3]}\n") |
| else: |
| for tri in tris: |
| f.write(struct.pack('<B3i', 3, *tri)) |
| for quad in quads: |
| f.write(struct.pack('<B4i', 4, *quad)) |
| |
|
|
| def write_pbr_ply( |
| filename: str, |
| vertices: np.ndarray, |
| faces: np.ndarray, |
| base_color: np.ndarray, |
| metallic: np.ndarray, |
| roughness: np.ndarray, |
| alpha: np.ndarray, |
| ascii: bool = False |
| ): |
| """ |
| Write a mesh to a PLY file, with the option to save in ASCII or binary format, |
| and optional per-vertex colors. |
| |
| Args: |
| filename (str): The filename to write to. |
| vertices (np.ndarray): [N, 3] The vertex positions. |
| faces (np.ndarray): [M, 3] The triangle indices. |
| base_color (np.ndarray): [N, 3] UInt8 colors for each vertex (RGB). |
| metallic (np.ndarray): [N] UInt8 values for metallicness. |
| roughness (np.ndarray): [N] UInt8 values for roughness. |
| alpha (np.ndarray): [N] UInt8 values for alpha. |
| ascii (bool): If True, write in ASCII format; otherwise binary little-endian. |
| """ |
| vertex_dtype = [ |
| ('x', 'f4'), ('y', 'f4'), ('z', 'f4'), |
| ('red', 'u1'), ('green', 'u1'), ('blue', 'u1'), |
| ('metallic', 'u1'), ('roughness', 'u1'), ('alpha', 'u1') |
| ] |
| |
| vertex_data = np.empty(len(vertices), dtype=vertex_dtype) |
| vertex_data['x'] = vertices[:, 0] |
| vertex_data['y'] = vertices[:, 1] |
| vertex_data['z'] = vertices[:, 2] |
| vertex_data['red'] = base_color[:, 0] |
| vertex_data['green'] = base_color[:, 1] |
| vertex_data['blue'] = base_color[:, 2] |
| vertex_data['metallic'] = metallic |
| vertex_data['roughness'] = roughness |
| vertex_data['alpha'] = alpha |
| |
| face_dtype = [ |
| ('vertex_indices', 'i4', (3,)) |
| ] |
| |
| face_data = np.empty(len(faces), dtype=face_dtype) |
| face_data['vertex_indices'] = faces |
| |
| ply_data = PlyData([ |
| PlyElement.describe(vertex_data,'vertex'), |
| PlyElement.describe(face_data, 'face'), |
| ], text=ascii) |
| ply_data.write(filename) |
|
|