diff options
Diffstat (limited to 'io_mesh_ply')
-rw-r--r-- | io_mesh_ply/__init__.py | 51 | ||||
-rw-r--r-- | io_mesh_ply/export_ply.py | 248 | ||||
-rw-r--r-- | io_mesh_ply/import_ply.py | 117 |
3 files changed, 222 insertions, 194 deletions
diff --git a/io_mesh_ply/__init__.py b/io_mesh_ply/__init__.py index b89732fd..c9a79e1f 100644 --- a/io_mesh_ply/__init__.py +++ b/io_mesh_ply/__init__.py @@ -21,11 +21,10 @@ bl_info = { "name": "Stanford PLY format", "author": "Bruce Merry, Campbell Barton", - "version": (1, 0, 0), - "blender": (2, 81, 6), + "version": (1, 1, 0), + "blender": (2, 82, 0), "location": "File > Import-Export", - "description": "Import-Export PLY mesh data with UV's and vertex colors", - "warning": "", + "description": "Import-Export PLY mesh data with UVs and vertex colors", "wiki_url": "https://docs.blender.org/manual/en/latest/addons/io_mesh_ply.html", "support": 'OFFICIAL', "category": "Import-Export", @@ -34,8 +33,6 @@ bl_info = { # Copyright (C) 2004, 2005: Bruce Merry, bmerry@cs.uct.ac.za # Contributors: Bruce Merry, Campbell Barton -# To support reload properly, try to access a package var, -# if it's there, reload everything if "bpy" in locals(): import importlib if "export_ply" in locals(): @@ -44,13 +41,11 @@ if "bpy" in locals(): importlib.reload(import_ply) -import os import bpy from bpy.props import ( CollectionProperty, StringProperty, BoolProperty, - EnumProperty, FloatProperty, ) from bpy_extras.io_utils import ( @@ -81,6 +76,8 @@ class ImportPLY(bpy.types.Operator, ImportHelper): filter_glob: StringProperty(default="*.ply", options={'HIDDEN'}) def execute(self, context): + import os + paths = [os.path.join(self.directory, name.name) for name in self.files] if not paths: @@ -96,14 +93,18 @@ class ImportPLY(bpy.types.Operator, ImportHelper): @orientation_helper(axis_forward='Y', axis_up='Z') class ExportPLY(bpy.types.Operator, ExportHelper): - """Export a single object as a Stanford PLY with normals, """ \ - """colors and texture coordinates""" bl_idname = "export_mesh.ply" bl_label = "Export PLY" + bl_description = "Export as a Stanford PLY with normals, vertex colors and texture coordinates" filename_ext = ".ply" filter_glob: StringProperty(default="*.ply", options={'HIDDEN'}) + use_selection: BoolProperty( + name="Selection Only", + description="Export selected objects only", + default=False, + ) use_mesh_modifiers: BoolProperty( name="Apply Modifiers", description="Apply Modifiers to the exported mesh", @@ -136,10 +137,6 @@ class ExportPLY(bpy.types.Operator, ExportHelper): default=1.0, ) - @classmethod - def poll(cls, context): - return context.active_object is not None - def execute(self, context): from . import export_ply @@ -169,6 +166,30 @@ class ExportPLY(bpy.types.Operator, ExportHelper): pass +class PLY_PT_export_include(bpy.types.Panel): + bl_space_type = 'FILE_BROWSER' + bl_region_type = 'TOOL_PROPS' + bl_label = "Include" + bl_parent_id = "FILE_PT_operator" + + @classmethod + def poll(cls, context): + sfile = context.space_data + operator = sfile.active_operator + + return operator.bl_idname == "EXPORT_MESH_OT_ply" + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False # No animation. + + sfile = context.space_data + operator = sfile.active_operator + + layout.prop(operator, "use_selection") + + class PLY_PT_export_transform(bpy.types.Panel): bl_space_type = 'FILE_BROWSER' bl_region_type = 'TOOL_PROPS' @@ -233,6 +254,7 @@ def menu_func_export(self, context): classes = ( ImportPLY, ExportPLY, + PLY_PT_export_include, PLY_PT_export_transform, PLY_PT_export_geometry, ) @@ -253,5 +275,6 @@ def unregister(): bpy.types.TOPBAR_MT_file_import.remove(menu_func_import) bpy.types.TOPBAR_MT_file_export.remove(menu_func_export) + if __name__ == "__main__": register() diff --git a/io_mesh_ply/export_ply.py b/io_mesh_ply/export_ply.py index db79e950..812aeb54 100644 --- a/io_mesh_ply/export_ply.py +++ b/io_mesh_ply/export_ply.py @@ -21,20 +21,12 @@ """ This script exports Stanford PLY files from Blender. It supports normals, colors, and texture coordinates per face or per vertex. -Only one mesh can be exported at a time. """ -import bpy -import os - -def save_mesh( - filepath, - mesh, - use_normals=True, - use_uv_coords=True, - use_colors=True, -): +def save_mesh(filepath, mesh, use_normals=True, use_uv_coords=True, use_colors=True): + import os + import bpy def rvec3d(v): return round(v[0], 6), round(v[1], 6), round(v[2], 6) @@ -42,47 +34,26 @@ def save_mesh( def rvec2d(v): return round(v[0], 6), round(v[1], 6) - file = open(filepath, "w", encoding="utf8", newline="\n") - fw = file.write - - has_uv = bool(mesh.uv_layers) - has_vcol = bool(mesh.vertex_colors) - - if not has_uv: + if use_uv_coords and mesh.uv_layers: + active_uv_layer = mesh.uv_layers.active.data + else: use_uv_coords = False - if not has_vcol: - use_colors = False - if not use_uv_coords: - has_uv = False - if not use_colors: - has_vcol = False - - if has_uv: - active_uv_layer = mesh.uv_layers.active - if not active_uv_layer: - use_uv_coords = False - has_uv = False - else: - active_uv_layer = active_uv_layer.data - - if has_vcol: - active_col_layer = mesh.vertex_colors.active - if not active_col_layer: - use_colors = False - has_vcol = False - else: - active_col_layer = active_col_layer.data + if use_colors and mesh.vertex_colors: + active_col_layer = mesh.vertex_colors.active.data + else: + use_colors = False # in case color = uvcoord = uvcoord_key = normal = normal_key = None - mesh_verts = mesh.vertices # save a lookup - ply_verts = [] # list of dictionaries + mesh_verts = mesh.vertices # vdict = {} # (index, normal, uv) -> new index vdict = [{} for i in range(len(mesh_verts))] + ply_verts = [] ply_faces = [[] for f in range(len(mesh.polygons))] vert_count = 0 + for i, f in enumerate(mesh.polygons): smooth = not use_normals or f.use_smooth @@ -90,12 +61,12 @@ def save_mesh( normal = f.normal[:] normal_key = rvec3d(normal) - if has_uv: + if use_uv_coords: uv = [ active_uv_layer[l].uv[:] for l in range(f.loop_start, f.loop_start + f.loop_total) ] - if has_vcol: + if use_colors: col = [ active_col_layer[l].color[:] for l in range(f.loop_start, f.loop_start + f.loop_total) @@ -109,11 +80,11 @@ def save_mesh( normal = v.normal[:] normal_key = rvec3d(normal) - if has_uv: + if use_uv_coords: uvcoord = uv[j][0], uv[j][1] uvcoord_key = rvec2d(uvcoord) - if has_vcol: + if use_colors: color = col[j] color = ( int(color[0] * 255.0), @@ -126,106 +97,141 @@ def save_mesh( vdict_local = vdict[vidx] pf_vidx = vdict_local.get(key) # Will be None initially - if pf_vidx is None: # same as vdict_local.has_key(key) + if pf_vidx is None: # Same as vdict_local.has_key(key) pf_vidx = vdict_local[key] = vert_count ply_verts.append((vidx, normal, uvcoord, color)) vert_count += 1 pf.append(pf_vidx) - fw("ply\n") - fw("format ascii 1.0\n") - fw("comment Created by Blender %s - " - "www.blender.org, source file: %r\n" % - (bpy.app.version_string, os.path.basename(bpy.data.filepath))) - - fw("element vertex %d\n" % len(ply_verts)) - - fw("property float x\n" - "property float y\n" - "property float z\n") - - if use_normals: - fw("property float nx\n" - "property float ny\n" - "property float nz\n") - if use_uv_coords: - fw("property float s\n" - "property float t\n") - if use_colors: - fw("property uchar red\n" - "property uchar green\n" - "property uchar blue\n" - "property uchar alpha\n") - - fw("element face %d\n" % len(mesh.polygons)) - fw("property list uchar uint vertex_indices\n") - fw("end_header\n") - - for i, v in enumerate(ply_verts): - fw("%.6f %.6f %.6f" % mesh_verts[v[0]].co[:]) # co + with open(filepath, "w", encoding="utf-8", newline="\n") as file: + fw = file.write + + # Header + # --------------------------- + + fw("ply\n") + fw("format ascii 1.0\n") + fw( + f"comment Created by Blender {bpy.app.version_string} - " + f"www.blender.org, source file: {os.path.basename(bpy.data.filepath)!r}\n" + ) + + fw(f"element vertex {len(ply_verts)}\n") + fw( + "property float x\n" + "property float y\n" + "property float z\n" + ) if use_normals: - fw(" %.6f %.6f %.6f" % v[1]) # no + fw( + "property float nx\n" + "property float ny\n" + "property float nz\n" + ) if use_uv_coords: - fw(" %.6f %.6f" % v[2]) # uv + fw( + "property float s\n" + "property float t\n" + ) if use_colors: - fw(" %u %u %u %u" % v[3]) # col - fw("\n") - - for pf in ply_faces: - # fw(f"{len(pf)} {' '.join(str(x) for x in pf)}\n") - fw("%d" % len(pf)) - for v in pf: - fw(" %d" % v) - fw("\n") - - file.close() - print("writing %r done" % filepath) + fw( + "property uchar red\n" + "property uchar green\n" + "property uchar blue\n" + "property uchar alpha\n" + ) + + fw(f"element face {len(mesh.polygons)}\n") + fw("property list uchar uint vertex_indices\n") + + fw("end_header\n") + + # Vertex data + # --------------------------- + + for i, v in enumerate(ply_verts): + fw("%.6f %.6f %.6f" % mesh_verts[v[0]].co[:]) + if use_normals: + fw(" %.6f %.6f %.6f" % v[1]) + if use_uv_coords: + fw(" %.6f %.6f" % v[2]) + if use_colors: + fw(" %u %u %u %u" % v[3]) + fw("\n") + + # Face data + # --------------------------- + + for pf in ply_faces: + fw(f"{len(pf)}") + for v in pf: + fw(f" {v}") + fw("\n") + + print(f"Writing {filepath!r} done") return {'FINISHED'} def save( - operator, - context, - filepath="", - use_mesh_modifiers=True, - use_normals=True, - use_uv_coords=True, - use_colors=True, - global_matrix=None + operator, + context, + filepath="", + use_selection=False, + use_mesh_modifiers=True, + use_normals=True, + use_uv_coords=True, + use_colors=True, + global_matrix=None ): - obj = context.active_object - - if global_matrix is None: - from mathutils import Matrix - global_matrix = Matrix() + import bpy + import bmesh if bpy.ops.object.mode_set.poll(): bpy.ops.object.mode_set(mode='OBJECT') - mesh_owner_object = None - if use_mesh_modifiers and obj.modifiers: - depsgraph = context.evaluated_depsgraph_get() - mesh_owner_object = obj.evaluated_get(depsgraph) - mesh = mesh_owner_object.to_mesh() + if use_selection: + obs = context.selected_objects else: - mesh_owner_object = obj - mesh = mesh_owner_object.to_mesh() + obs = context.scene.objects - if not mesh: - raise Exception("Error, could not get mesh data from active object") + depsgraph = context.evaluated_depsgraph_get() + bm = bmesh.new() + + for ob in obs: + if use_mesh_modifiers: + ob_eval = ob.evaluated_get(depsgraph) + else: + ob_eval = ob + + try: + me = ob_eval.to_mesh() + except RuntimeError: + continue + + me.transform(ob.matrix_world) + bm.from_mesh(me) + ob_eval.to_mesh_clear() + + mesh = bpy.data.meshes.new("TMP PLY EXPORT") + bm.to_mesh(mesh) + bm.free() + + if global_matrix is not None: + mesh.transform(global_matrix) - mesh.transform(global_matrix @ obj.matrix_world) if use_normals: mesh.calc_normals() - ret = save_mesh(filepath, mesh, - use_normals=use_normals, - use_uv_coords=use_uv_coords, - use_colors=use_colors, - ) + ret = save_mesh( + filepath, + mesh, + use_normals=use_normals, + use_uv_coords=use_uv_coords, + use_colors=use_colors, + ) - mesh_owner_object.to_mesh_clear() + bpy.data.meshes.remove(mesh) return ret diff --git a/io_mesh_ply/import_ply.py b/io_mesh_ply/import_ply.py index 5da7f9d6..2bf91442 100644 --- a/io_mesh_ply/import_ply.py +++ b/io_mesh_ply/import_ply.py @@ -18,11 +18,8 @@ # <pep8 compliant> -import re -import struct - -class element_spec(object): +class ElementSpec: __slots__ = ( "name", "count", @@ -46,7 +43,7 @@ class element_spec(object): return -1 -class property_spec(object): +class PropertySpec: __slots__ = ( "name", "list_type", @@ -59,14 +56,16 @@ class property_spec(object): self.numeric_type = numeric_type def read_format(self, format, count, num_type, stream): + import struct + if format == b'ascii': if num_type == 's': ans = [] for i in range(count): s = stream[i] if not (len(s) >= 2 and s.startswith(b'"') and s.endswith(b'"')): - print('Invalid string', s) - print('Note: ply_import.py does not handle whitespace in strings') + print("Invalid string", s) + print("Note: ply_import.py does not handle whitespace in strings") return None ans.append(s[1:-1]) stream[:count] = [] @@ -103,18 +102,18 @@ class property_spec(object): return self.read_format(format, 1, self.numeric_type, stream)[0] -class object_spec(object): - __slots__ = ("specs", - ) - 'A list of element_specs' +class ObjectSpec: + __slots__ = ("specs",) + def __init__(self): + # A list of element_specs self.specs = [] def load(self, format, stream): return dict([(i.name, [i.load(format, stream) for j in range(i.count)]) for i in self.specs]) - ''' # Longhand for above LC + """ answer = {} for i in self.specs: answer[i.name] = [] @@ -123,10 +122,12 @@ class object_spec(object): Blender.Window.DrawProgressBar(float(j) / i.count, 'Loading ' + i.name) answer[i.name].append(i.load(format, stream)) return answer - ''' + """ def read(filepath): + import re + format = b'' texture = b'' version = b'1.0' @@ -154,14 +155,14 @@ def read(filepath): b'double': 'd', b'string': 's', } - obj_spec = object_spec() + obj_spec = ObjectSpec() invalid_ply = (None, None, None) with open(filepath, 'rb') as plyf: signature = plyf.readline() if not signature.startswith(b'ply'): - print('Signature line was invalid') + print("Signature line was invalid") return invalid_ply valid_header = False @@ -178,7 +179,7 @@ def read(filepath): continue elif tokens[1] == b'TextureFile': if len(tokens) < 4: - print('Invalid texture line') + print("Invalid texture line") else: texture = tokens[2] continue @@ -187,34 +188,34 @@ def read(filepath): continue elif tokens[0] == b'format': if len(tokens) < 3: - print('Invalid format line') + print("Invalid format line") return invalid_ply if tokens[1] not in format_specs: - print('Unknown format', tokens[1]) + print("Unknown format", tokens[1]) return invalid_ply try: version_test = float(tokens[2]) except Exception as ex: - print('Unknown version', ex) + print("Unknown version", ex) version_test = None if version_test != float(version): - print('Unknown version', tokens[2]) + print("Unknown version", tokens[2]) return invalid_ply del version_test format = tokens[1] elif tokens[0] == b'element': if len(tokens) < 3: - print(b'Invalid element line') + print("Invalid element line") return invalid_ply - obj_spec.specs.append(element_spec(tokens[1], int(tokens[2]))) + obj_spec.specs.append(ElementSpec(tokens[1], int(tokens[2]))) elif tokens[0] == b'property': if not len(obj_spec.specs): - print('Property without element') + print("Property without element") return invalid_ply if tokens[1] == b'list': - obj_spec.specs[-1].properties.append(property_spec(tokens[4], type_specs[tokens[2]], type_specs[tokens[3]])) + obj_spec.specs[-1].properties.append(PropertySpec(tokens[4], type_specs[tokens[2]], type_specs[tokens[3]])) else: - obj_spec.specs[-1].properties.append(property_spec(tokens[2], None, type_specs[tokens[1]])) + obj_spec.specs[-1].properties.append(PropertySpec(tokens[2], None, type_specs[tokens[1]])) if not valid_header: print("Invalid header ('end_header' line not found!)") return invalid_ply @@ -224,22 +225,20 @@ def read(filepath): return obj_spec, obj, texture -import bpy - - def load_ply_mesh(filepath, ply_name): - from bpy_extras.io_utils import unpack_face_list + import bpy obj_spec, obj, texture = read(filepath) # XXX28: use texture if obj is None: - print('Invalid file') + print("Invalid file") return uvindices = colindices = None colmultiply = None - # noindices = None # Ignore normals + # TODO import normals + # noindices = None for el in obj_spec.specs: if el.name == b'vertex': @@ -375,39 +374,39 @@ def load_ply_mesh(filepath, ply_name): if texture and uvindices: pass - # XXX28: add support for using texture. - ''' - import os - import sys - from bpy_extras.image_utils import load_image - - encoding = sys.getfilesystemencoding() - encoded_texture = texture.decode(encoding=encoding) - name = bpy.path.display_name_from_filepath(texture) - image = load_image(encoded_texture, os.path.dirname(filepath), recursive=True, place_holder=True) - - if image: - texture = bpy.data.textures.new(name=name, type='IMAGE') - texture.image = image - - material = bpy.data.materials.new(name=name) - material.use_shadeless = True - - mtex = material.texture_slots.add() - mtex.texture = texture - mtex.texture_coords = 'UV' - mtex.use_map_color_diffuse = True - - mesh.materials.append(material) - for face in mesh.uv_textures[0].data: - face.image = image - ''' + # TODO add support for using texture. + + # import os + # import sys + # from bpy_extras.image_utils import load_image + + # encoding = sys.getfilesystemencoding() + # encoded_texture = texture.decode(encoding=encoding) + # name = bpy.path.display_name_from_filepath(texture) + # image = load_image(encoded_texture, os.path.dirname(filepath), recursive=True, place_holder=True) + + # if image: + # texture = bpy.data.textures.new(name=name, type='IMAGE') + # texture.image = image + + # material = bpy.data.materials.new(name=name) + # material.use_shadeless = True + + # mtex = material.texture_slots.add() + # mtex.texture = texture + # mtex.texture_coords = 'UV' + # mtex.use_map_color_diffuse = True + + # mesh.materials.append(material) + # for face in mesh.uv_textures[0].data: + # face.image = image return mesh def load_ply(filepath): import time + import bpy t = time.time() ply_name = bpy.path.display_name_from_filepath(filepath) @@ -421,7 +420,7 @@ def load_ply(filepath): bpy.context.view_layer.objects.active = obj obj.select_set(True) - print('\nSuccessfully imported %r in %.3f sec' % (filepath, time.time() - t)) + print("\nSuccessfully imported %r in %.3f sec" % (filepath, time.time() - t)) return {'FINISHED'} |