diff options
Diffstat (limited to 'io_mesh_ply')
-rw-r--r-- | io_mesh_ply/__init__.py | 134 | ||||
-rw-r--r-- | io_mesh_ply/export_ply.py | 203 | ||||
-rw-r--r-- | io_mesh_ply/import_ply.py | 326 |
3 files changed, 663 insertions, 0 deletions
diff --git a/io_mesh_ply/__init__.py b/io_mesh_ply/__init__.py new file mode 100644 index 00000000..eabfc54e --- /dev/null +++ b/io_mesh_ply/__init__.py @@ -0,0 +1,134 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# <pep8 compliant> + +bl_info = { + "name": "Stanford PLY format", + "author": "Bruce Merry, Campbell Barton", + "blender": (2, 5, 7), + "api": 35622, + "location": "File > Import-Export", + "description": "Import-Export PLY mesh data withs UV's and vertex colors", + "warning": "", + "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.5/Py/"\ + "Scripts/Import-Export/Stanford_PLY", + "tracker_url": "", + "support": 'OFFICIAL', + "category": "Import-Export"} + +# To support reload properly, try to access a package var, if it's there, reload everything +if "bpy" in locals(): + import imp + if "export_ply" in locals(): + imp.reload(export_ply) + if "import_ply" in locals(): + imp.reload(import_ply) + + +import os +import bpy +from bpy.props import CollectionProperty, StringProperty, BoolProperty +from io_utils import ImportHelper, ExportHelper + + +class ImportPLY(bpy.types.Operator, ImportHelper): + '''Load a PLY geometry file''' + bl_idname = "import_mesh.ply" + bl_label = "Import PLY" + + files = CollectionProperty(name="File Path", + description="File path used for importing " + "the PLY file", + type=bpy.types.OperatorFileListElement) + + directory = StringProperty() + + filename_ext = ".ply" + filter_glob = StringProperty(default="*.ply", options={'HIDDEN'}) + + def execute(self, context): + paths = [os.path.join(self.directory, name.name) for name in self.files] + if not paths: + paths.append(self.filepath) + + from . import import_ply + + for path in paths: + import_ply.load(self, context, path) + + return {'FINISHED'} + + +class ExportPLY(bpy.types.Operator, ExportHelper): + '''Export a single object as a stanford PLY with normals, colours and texture coordinates.''' + bl_idname = "export_mesh.ply" + bl_label = "Export PLY" + + filename_ext = ".ply" + filter_glob = StringProperty(default="*.ply", options={'HIDDEN'}) + + use_modifiers = BoolProperty(name="Apply Modifiers", description="Apply Modifiers to the exported mesh", default=True) + use_normals = BoolProperty(name="Normals", description="Export Normals for smooth and hard shaded faces", default=True) + use_uv_coords = BoolProperty(name="UVs", description="Exort the active UV layer", default=True) + use_colors = BoolProperty(name="Vertex Colors", description="Exort the active vertex color layer", default=True) + + @classmethod + def poll(cls, context): + return context.active_object != None + + def execute(self, context): + filepath = self.filepath + filepath = bpy.path.ensure_ext(filepath, self.filename_ext) + from . import export_ply + return export_ply.save(self, context, **self.as_keywords(ignore=("check_existing", "filter_glob"))) + + def draw(self, context): + layout = self.layout + + row = layout.row() + row.prop(self, "use_modifiers") + row.prop(self, "use_normals") + row = layout.row() + row.prop(self, "use_uv_coords") + row.prop(self, "use_colors") + + +def menu_func_import(self, context): + self.layout.operator(ImportPLY.bl_idname, text="Stanford (.ply)") + + +def menu_func_export(self, context): + self.layout.operator(ExportPLY.bl_idname, text="Stanford (.ply)") + + +def register(): + bpy.utils.register_module(__name__) + + bpy.types.INFO_MT_file_import.append(menu_func_import) + bpy.types.INFO_MT_file_export.append(menu_func_export) + + +def unregister(): + bpy.utils.unregister_module(__name__) + + bpy.types.INFO_MT_file_import.remove(menu_func_import) + bpy.types.INFO_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 new file mode 100644 index 00000000..6e2f43db --- /dev/null +++ b/io_mesh_ply/export_ply.py @@ -0,0 +1,203 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# <pep8 compliant> + +# Copyright (C) 2004, 2005: Bruce Merry, bmerry@cs.uct.ac.za +# Contributors: Bruce Merry, Campbell Barton + +""" +This script exports Stanford PLY files from Blender. It supports normals, +colours, and texture coordinates per face or per vertex. +Only one mesh can be exported at a time. +""" + +import bpy +import os + + +def save(operator, context, filepath="", use_modifiers=True, use_normals=True, use_uv_coords=True, use_colors=True): + + def rvec3d(v): + return round(v[0], 6), round(v[1], 6), round(v[2], 6) + + def rvec2d(v): + return round(v[0], 6), round(v[1], 6) + + scene = context.scene + obj = context.object + + if not obj: + raise Exception("Error, Select 1 active object") + + file = open(filepath, "w", encoding="utf8", newline="\n") + + if scene.objects.active: + bpy.ops.object.mode_set(mode='OBJECT') + + if use_modifiers: + mesh = obj.to_mesh(scene, True, 'PREVIEW') + else: + mesh = obj.data + + if not mesh: + raise Exception("Error, could not get mesh data from active object") + + # mesh.transform(obj.matrix_world) # XXX + + faceUV = (len(mesh.uv_textures) > 0) + vertexUV = (len(mesh.sticky) > 0) + vertexColors = len(mesh.vertex_colors) > 0 + + if (not faceUV) and (not vertexUV): + use_uv_coords = False + if not vertexColors: + use_colors = False + + if not use_uv_coords: + faceUV = vertexUV = False + if not use_colors: + vertexColors = False + + if faceUV: + active_uv_layer = mesh.uv_textures.active + if not active_uv_layer: + use_uv_coords = False + faceUV = None + else: + active_uv_layer = active_uv_layer.data + + if vertexColors: + active_col_layer = mesh.vertex_colors.active + if not active_col_layer: + use_colors = False + vertexColors = None + else: + active_col_layer = active_col_layer.data + + # incase + color = uvcoord = uvcoord_key = normal = normal_key = None + + mesh_verts = mesh.vertices # save a lookup + ply_verts = [] # list of dictionaries + # vdict = {} # (index, normal, uv) -> new index + vdict = [{} for i in range(len(mesh_verts))] + ply_faces = [[] for f in range(len(mesh.faces))] + vert_count = 0 + for i, f in enumerate(mesh.faces): + + smooth = f.use_smooth + if not smooth: + normal = tuple(f.normal) + normal_key = rvec3d(normal) + + if faceUV: + uv = active_uv_layer[i] + uv = uv.uv1, uv.uv2, uv.uv3, uv.uv4 # XXX - crufty :/ + if vertexColors: + col = active_col_layer[i] + col = col.color1[:], col.color2[:], col.color3[:], col.color4[:] + + f_verts = f.vertices + + pf = ply_faces[i] + for j, vidx in enumerate(f_verts): + v = mesh_verts[vidx] + + if smooth: + normal = tuple(v.normal) + normal_key = rvec3d(normal) + + if faceUV: + uvcoord = uv[j][0], 1.0 - uv[j][1] + uvcoord_key = rvec2d(uvcoord) + elif vertexUV: + uvcoord = v.uvco[0], 1.0 - v.uvco[1] + uvcoord_key = rvec2d(uvcoord) + + if vertexColors: + color = col[j] + color = int(color[0] * 255.0), int(color[1] * 255.0), int(color[2] * 255.0) + + key = normal_key, uvcoord_key, color + + 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) + pf_vidx = vdict_local[key] = vert_count + ply_verts.append((vidx, normal, uvcoord, color)) + vert_count += 1 + + pf.append(pf_vidx) + + file.write('ply\n') + file.write('format ascii 1.0\n') + file.write('comment Created by Blender %s - www.blender.org, source file: %r\n' % (bpy.app.version_string, os.path.basename(bpy.data.filepath))) + + file.write('element vertex %d\n' % len(ply_verts)) + + file.write('property float x\n') + file.write('property float y\n') + file.write('property float z\n') + + if use_normals: + file.write('property float nx\n') + file.write('property float ny\n') + file.write('property float nz\n') + if use_uv_coords: + file.write('property float s\n') + file.write('property float t\n') + if use_colors: + file.write('property uchar red\n') + file.write('property uchar green\n') + file.write('property uchar blue\n') + + file.write('element face %d\n' % len(mesh.faces)) + file.write('property list uchar uint vertex_indices\n') + file.write('end_header\n') + + for i, v in enumerate(ply_verts): + file.write('%.6f %.6f %.6f ' % mesh_verts[v[0]].co[:]) # co + if use_normals: + file.write('%.6f %.6f %.6f ' % v[1]) # no + if use_uv_coords: + file.write('%.6f %.6f ' % v[2]) # uv + if use_colors: + file.write('%u %u %u' % v[3]) # col + file.write('\n') + + for pf in ply_faces: + if len(pf) == 3: + file.write('3 %d %d %d\n' % tuple(pf)) + else: + file.write('4 %d %d %d %d\n' % tuple(pf)) + + file.close() + print("writing %r done" % filepath) + + if use_modifiers: + bpy.data.meshes.remove(mesh) + + # XXX + """ + if is_editmode: + Blender.Window.EditMode(1, '', 0) + """ + + return {'FINISHED'} diff --git a/io_mesh_ply/import_ply.py b/io_mesh_ply/import_ply.py new file mode 100644 index 00000000..8e5db68e --- /dev/null +++ b/io_mesh_ply/import_ply.py @@ -0,0 +1,326 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# <pep8 compliant> + +import re +import struct + + +class element_spec(object): + __slots__ = ("name", + "count", + "properties", + ) + + def __init__(self, name, count): + self.name = name + self.count = count + self.properties = [] + + def load(self, format, stream): + if format == b'ascii': + stream = re.split(b'\s+', stream.readline()) + return [x.load(format, stream) for x in self.properties] + + def index(self, name): + for i, p in enumerate(self.properties): + if p.name == name: + return i + return -1 + + +class property_spec(object): + __slots__ = ("name", + "list_type", + "numeric_type", + ) + + def __init__(self, name, list_type, numeric_type): + self.name = name + self.list_type = list_type + self.numeric_type = numeric_type + + def read_format(self, format, count, num_type, stream): + if format == b'ascii': + if num_type == 's': + ans = [] + for i in range(count): + s = stream[i] + if len(s) < 2 or s[0] != '"' or s[-1] != '"': + print('Invalid string', s) + print('Note: ply_import.py does not handle whitespace in strings') + return None + ans.append(s[1:-1]) + stream[:count] = [] + return ans + if num_type == 'f' or num_type == 'd': + mapper = float + else: + mapper = int + ans = [mapper(x) for x in stream[:count]] + stream[:count] = [] + return ans + else: + if num_type == 's': + ans = [] + for i in range(count): + fmt = format + 'i' + data = stream.read(struct.calcsize(fmt)) + length = struct.unpack(fmt, data)[0] + fmt = '%s%is' % (format, length) + data = stream.read(struct.calcsize(fmt)) + s = struct.unpack(fmt, data)[0] + ans.append(s[:-1]) # strip the NULL + return ans + else: + fmt = '%s%i%s' % (format, count, num_type) + data = stream.read(struct.calcsize(fmt)) + return struct.unpack(fmt, data) + + def load(self, format, stream): + if self.list_type is not None: + count = int(self.read_format(format, 1, self.list_type, stream)[0]) + return self.read_format(format, count, self.numeric_type, stream) + else: + return self.read_format(format, 1, self.numeric_type, stream)[0] + + +class object_spec(object): + __slots__ = ("specs", + ) + 'A list of element_specs' + def __init__(self): + 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] = [] + for j in range(i.count): + if not j % 100 and meshtools.show_progress: + Blender.Window.DrawProgressBar(float(j) / i.count, 'Loading ' + i.name) + answer[i.name].append(i.load(format, stream)) + return answer + ''' + + +def read(filepath): + format = b'' + version = b'1.0' + format_specs = {b'binary_little_endian': '<', + b'binary_big_endian': '>', + b'ascii': b'ascii'} + type_specs = {b'char': 'b', + b'uchar': 'B', + b'int8': 'b', + b'uint8': 'B', + b'int16': 'h', + b'uint16': 'H', + b'ushort': 'H', + b'int': 'i', + b'int32': 'i', + b'uint': 'I', + b'uint32': 'I', + b'float': 'f', + b'float32': 'f', + b'float64': 'd', + b'double': 'd', + b'string': 's'} + obj_spec = object_spec() + + file = open(filepath, 'rb') # Only for parsing the header, not binary data + signature = file.readline() + + if not signature.startswith(b'ply'): + print('Signature line was invalid') + return None + + while 1: + tokens = re.split(r'[ \n]+'.encode("ASCII"), file.readline()) + + if len(tokens) == 0: + continue + if tokens[0] == b'end_header': + break + elif tokens[0] == b'comment' or tokens[0] == b'obj_info': + continue + elif tokens[0] == b'format': + if len(tokens) < 3: + print('Invalid format line') + return None + if tokens[1] not in format_specs: # .keys(): # keys is implicit + print('Unknown format', tokens[1]) + return None + if tokens[2] != version: + print('Unknown version', tokens[2]) + return None + format = tokens[1] + elif tokens[0] == b'element': + if len(tokens) < 3: + print(b'Invalid element line') + return None + obj_spec.specs.append(element_spec(tokens[1], int(tokens[2]))) + elif tokens[0] == b'property': + if not len(obj_spec.specs): + print('Property without element') + return None + if tokens[1] == b'list': + obj_spec.specs[-1].properties.append(property_spec(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]])) + + if format != b'ascii': + file.close() # was ascii, now binary + file = open(filepath, 'rb') + + # skip the header... + while not file.readline().startswith(b'end_header'): + pass + + obj = obj_spec.load(format_specs[format], file) + + return obj_spec, obj + + +import bpy + + +def load_ply(filepath): + import time + from io_utils import load_image, unpack_list, unpack_face_list + + t = time.time() + obj_spec, obj = read(filepath) + if obj is None: + print('Invalid file') + return + + uvindices = colindices = None + # noindices = None # Ignore normals + + for el in obj_spec.specs: + if el.name == b'vertex': + vindices = vindices_x, vindices_y, vindices_z = (el.index(b'x'), el.index(b'y'), el.index(b'z')) + # noindices = (el.index('nx'), el.index('ny'), el.index('nz')) + # if -1 in noindices: noindices = None + uvindices = (el.index(b's'), el.index(b't')) + if -1 in uvindices: + uvindices = None + colindices = (el.index(b'red'), el.index(b'green'), el.index(b'blue')) + if -1 in colindices: + colindices = None + elif el.name == b'face': + findex = el.index(b'vertex_indices') + + mesh_faces = [] + mesh_uvs = [] + mesh_colors = [] + + def add_face(vertices, indices, uvindices, colindices): + mesh_faces.append(indices) + if uvindices: + mesh_uvs.append([(vertices[index][uvindices[0]], 1.0 - vertices[index][uvindices[1]]) for index in indices]) + if colindices: + mesh_colors.append([(vertices[index][colindices[0]] / 255.0, vertices[index][colindices[1]] / 255.0, vertices[index][colindices[2]] / 255.0) for index in indices]) + + if uvindices or colindices: + # If we have Cols or UVs then we need to check the face order. + add_face_simple = add_face + + # EVIL EEKADOODLE - face order annoyance. + def add_face(vertices, indices, uvindices, colindices): + if len(indices) == 4: + if indices[2] == 0 or indices[3] == 0: + indices = indices[2], indices[3], indices[0], indices[1] + elif len(indices) == 3: + if indices[2] == 0: + indices = indices[1], indices[2], indices[0] + + add_face_simple(vertices, indices, uvindices, colindices) + + verts = obj[b'vertex'] + + if b'face' in obj: + for f in obj[b'face']: + ind = f[findex] + len_ind = len(ind) + if len_ind <= 4: + add_face(verts, ind, uvindices, colindices) + else: + # Fan fill the face + for j in range(len_ind - 2): + add_face(verts, (ind[0], ind[j + 1], ind[j + 2]), uvindices, colindices) + + ply_name = bpy.path.display_name_from_filepath(filepath) + + mesh = bpy.data.meshes.new(name=ply_name) + + mesh.vertices.add(len(obj[b'vertex'])) + + mesh.vertices.foreach_set("co", [a for v in obj[b'vertex'] for a in (v[vindices_x], v[vindices_y], v[vindices_z])]) + + if mesh_faces: + mesh.faces.add(len(mesh_faces)) + mesh.faces.foreach_set("vertices_raw", unpack_face_list(mesh_faces)) + + if uvindices or colindices: + if uvindices: + uvlay = mesh.uv_textures.new() + if colindices: + vcol_lay = mesh.vertex_colors.new() + + if uvindices: + for i, f in enumerate(uvlay.data): + ply_uv = mesh_uvs[i] + for j, uv in enumerate(f.uv): + uv[0], uv[1] = ply_uv[j] + + if colindices: + for i, f in enumerate(vcol_lay.data): + # XXX, colors dont come in right, needs further investigation. + ply_col = mesh_colors[i] + if len(ply_col) == 4: + f_col = f.color1, f.color2, f.color3, f.color4 + else: + f_col = f.color1, f.color2, f.color3 + + for j, col in enumerate(f_col): + col.r, col.g, col.b = ply_col[j] + + mesh.validate() + mesh.update() + + scn = bpy.context.scene + #scn.objects.selected = [] # XXX25 + + obj = bpy.data.objects.new(ply_name, mesh) + scn.objects.link(obj) + scn.objects.active = obj + obj.select = True + + print('\nSuccessfully imported %r in %.3f sec' % (filepath, time.time() - t)) + + +def load(operator, context, filepath=""): + load_ply(filepath) + return {'FINISHED'} |