File headers: use SPDX license identifiers
[blender-addons.git] / io_mesh_ply / export_ply.py
blobe4fddc49ea8bc97926f6d36aca1da866c741cdbf
1 # SPDX-License-Identifier: GPL-2.0-or-later
3 # <pep8-80 compliant>
5 """
6 This script exports Stanford PLY files from Blender. It supports normals,
7 colors, and texture coordinates per face or per vertex.
8 """
10 import bpy
13 def _write_binary(fw, ply_verts: list, ply_faces: list) -> None:
14 from struct import pack
16 # Vertex data
17 # ---------------------------
19 for v, normal, uv, color in ply_verts:
20 fw(pack("<3f", *v.co))
21 if normal is not None:
22 fw(pack("<3f", *normal))
23 if uv is not None:
24 fw(pack("<2f", *uv))
25 if color is not None:
26 fw(pack("<4B", *color))
28 # Face data
29 # ---------------------------
31 for pf in ply_faces:
32 length = len(pf)
33 fw(pack(f"<B{length}I", length, *pf))
36 def _write_ascii(fw, ply_verts: list, ply_faces: list) -> None:
38 # Vertex data
39 # ---------------------------
41 for v, normal, uv, color in ply_verts:
42 fw(b"%.6f %.6f %.6f" % v.co[:])
43 if normal is not None:
44 fw(b" %.6f %.6f %.6f" % normal[:])
45 if uv is not None:
46 fw(b" %.6f %.6f" % uv)
47 if color is not None:
48 fw(b" %u %u %u %u" % color)
49 fw(b"\n")
51 # Face data
52 # ---------------------------
54 for pf in ply_faces:
55 fw(b"%d" % len(pf))
56 for index in pf:
57 fw(b" %d" % index)
58 fw(b"\n")
61 def save_mesh(filepath, bm, use_ascii, use_normals, use_uv, use_color):
62 uv_lay = bm.loops.layers.uv.active
63 col_lay = bm.loops.layers.color.active
65 use_uv = use_uv and uv_lay is not None
66 use_color = use_color and col_lay is not None
67 normal = uv = color = None
69 ply_faces = []
70 ply_verts = []
71 ply_vert_map = {}
72 ply_vert_id = 0
74 for f in bm.faces:
75 pf = []
76 ply_faces.append(pf)
78 for loop in f.loops:
79 v = map_id = loop.vert
81 if use_uv:
82 uv = loop[uv_lay].uv[:]
83 map_id = uv
85 # Identify vertex by pointer unless exporting UVs,
86 # in which case id by UV coordinate (will split edges by seams).
87 if (_id := ply_vert_map.get(map_id)) is not None:
88 pf.append(_id)
89 continue
91 if use_normals:
92 normal = v.normal
93 if use_color:
94 color = tuple(int(x * 255.0) for x in loop[col_lay])
96 ply_verts.append((v, normal, uv, color))
97 ply_vert_map[map_id] = ply_vert_id
98 pf.append(ply_vert_id)
99 ply_vert_id += 1
101 with open(filepath, "wb") as file:
102 fw = file.write
103 file_format = b"ascii" if use_ascii else b"binary_little_endian"
105 # Header
106 # ---------------------------
108 fw(b"ply\n")
109 fw(b"format %s 1.0\n" % file_format)
110 fw(b"comment Created by Blender %s - www.blender.org\n" % bpy.app.version_string.encode("utf-8"))
112 fw(b"element vertex %d\n" % len(ply_verts))
114 b"property float x\n"
115 b"property float y\n"
116 b"property float z\n"
118 if use_normals:
120 b"property float nx\n"
121 b"property float ny\n"
122 b"property float nz\n"
124 if use_uv:
126 b"property float s\n"
127 b"property float t\n"
129 if use_color:
131 b"property uchar red\n"
132 b"property uchar green\n"
133 b"property uchar blue\n"
134 b"property uchar alpha\n"
137 fw(b"element face %d\n" % len(ply_faces))
138 fw(b"property list uchar uint vertex_indices\n")
139 fw(b"end_header\n")
141 # Geometry
142 # ---------------------------
144 if use_ascii:
145 _write_ascii(fw, ply_verts, ply_faces)
146 else:
147 _write_binary(fw, ply_verts, ply_faces)
150 def save(
151 context,
152 filepath="",
153 use_ascii=False,
154 use_selection=False,
155 use_mesh_modifiers=True,
156 use_normals=True,
157 use_uv_coords=True,
158 use_colors=True,
159 global_matrix=None,
161 import time
162 import bmesh
164 t = time.time()
166 if bpy.ops.object.mode_set.poll():
167 bpy.ops.object.mode_set(mode='OBJECT')
169 if use_selection:
170 obs = context.selected_objects
171 else:
172 obs = context.scene.objects
174 depsgraph = context.evaluated_depsgraph_get()
175 bm = bmesh.new()
177 for ob in obs:
178 if use_mesh_modifiers:
179 ob_eval = ob.evaluated_get(depsgraph)
180 else:
181 ob_eval = ob
183 try:
184 me = ob_eval.to_mesh()
185 except RuntimeError:
186 continue
188 me.transform(ob.matrix_world)
189 bm.from_mesh(me)
190 ob_eval.to_mesh_clear()
192 # Workaround for hardcoded unsigned char limit in other DCCs PLY importers
193 if (ngons := [f for f in bm.faces if len(f.verts) > 255]):
194 bmesh.ops.triangulate(bm, faces=ngons)
196 if global_matrix is not None:
197 bm.transform(global_matrix)
199 if use_normals:
200 bm.normal_update()
202 save_mesh(
203 filepath,
205 use_ascii,
206 use_normals,
207 use_uv_coords,
208 use_colors,
211 bm.free()
213 t_delta = time.time() - t
214 print(f"Export completed {filepath!r} in {t_delta:.3f}")