# ##### 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 ##### # # Script copyright (C) Campbell Barton # Contributors: Campbell Barton, Jiri Hnidek, Paolo Ciccone """ This script imports a Wavefront OBJ files to Blender. Usage: Run this script from "File->Import" menu and then load the desired OBJ file. Note, This loads mesh objects and materials only, nurbs and curves are not supported. http://wiki.blender.org/index.php/Scripts/Manual/Import/wavefront_obj """ import os import time import bpy import mathutils from mathutils.geometry import tesselate_polygon from io_utils import load_image, unpack_list, unpack_face_list def BPyMesh_ngon(from_data, indices, PREF_FIX_LOOPS=True): ''' Takes a polyline of indices (fgon) and returns a list of face indicie lists. Designed to be used for importers that need indices for an fgon to create from existing verts. from_data: either a mesh, or a list/tuple of vectors. indices: a list of indices to use this list is the ordered closed polyline to fill, and can be a subset of the data given. PREF_FIX_LOOPS: If this is enabled polylines that use loops to make multiple polylines are delt with correctly. ''' if not set: # Need sets for this, otherwise do a normal fill. PREF_FIX_LOOPS = False Vector = mathutils.Vector if not indices: return [] # return [] def rvec(co): return round(co.x, 6), round(co.y, 6), round(co.z, 6) def mlen(co): return abs(co[0]) + abs(co[1]) + abs(co[2]) # manhatten length of a vector, faster then length def vert_treplet(v, i): return v, rvec(v), i, mlen(v) def ed_key_mlen(v1, v2): if v1[3] > v2[3]: return v2[1], v1[1] else: return v1[1], v2[1] if not PREF_FIX_LOOPS: ''' Normal single concave loop filling ''' if type(from_data) in (tuple, list): verts = [Vector(from_data[i]) for ii, i in enumerate(indices)] else: verts = [from_data.vertices[i].co for ii, i in enumerate(indices)] for i in range(len(verts) - 1, 0, -1): # same as reversed(xrange(1, len(verts))): if verts[i][1] == verts[i - 1][0]: verts.pop(i - 1) fill = fill_polygon([verts]) else: ''' Seperate this loop into multiple loops be finding edges that are used twice This is used by lightwave LWO files a lot ''' if type(from_data) in (tuple, list): verts = [vert_treplet(Vector(from_data[i]), ii) for ii, i in enumerate(indices)] else: verts = [vert_treplet(from_data.vertices[i].co, ii) for ii, i in enumerate(indices)] edges = [(i, i - 1) for i in range(len(verts))] if edges: edges[0] = (0, len(verts) - 1) if not verts: return [] edges_used = set() edges_doubles = set() # We need to check if any edges are used twice location based. for ed in edges: edkey = ed_key_mlen(verts[ed[0]], verts[ed[1]]) if edkey in edges_used: edges_doubles.add(edkey) else: edges_used.add(edkey) # Store a list of unconnected loop segments split by double edges. # will join later loop_segments = [] v_prev = verts[0] context_loop = [v_prev] loop_segments = [context_loop] for v in verts: if v != v_prev: # Are we crossing an edge we removed? if ed_key_mlen(v, v_prev) in edges_doubles: context_loop = [v] loop_segments.append(context_loop) else: if context_loop and context_loop[-1][1] == v[1]: #raise "as" pass else: context_loop.append(v) v_prev = v # Now join loop segments def join_seg(s1, s2): if s2[-1][1] == s1[0][1]: s1, s2 = s2, s1 elif s1[-1][1] == s2[0][1]: pass else: return False # If were stuill here s1 and s2 are 2 segments in the same polyline s1.pop() # remove the last vert from s1 s1.extend(s2) # add segment 2 to segment 1 if s1[0][1] == s1[-1][1]: # remove endpoints double s1.pop() s2[:] = [] # Empty this segment s2 so we dont use it again. return True joining_segments = True while joining_segments: joining_segments = False segcount = len(loop_segments) for j in range(segcount - 1, -1, -1): # reversed(range(segcount)): seg_j = loop_segments[j] if seg_j: for k in range(j - 1, -1, -1): # reversed(range(j)): if not seg_j: break seg_k = loop_segments[k] if seg_k and join_seg(seg_j, seg_k): joining_segments = True loop_list = loop_segments for verts in loop_list: while verts and verts[0][1] == verts[-1][1]: verts.pop() loop_list = [verts for verts in loop_list if len(verts) > 2] # DONE DEALING WITH LOOP FIXING # vert mapping vert_map = [None] * len(indices) ii = 0 for verts in loop_list: if len(verts) > 2: for i, vert in enumerate(verts): vert_map[i + ii] = vert[2] ii += len(verts) fill = tesselate_polygon([[v[0] for v in loop] for loop in loop_list]) #draw_loops(loop_list) #raise 'done loop' # map to original indices fill = [[vert_map[i] for i in reversed(f)] for f in fill] if not fill: print('Warning Cannot scanfill, fallback on a triangle fan.') fill = [[0, i - 1, i] for i in range(2, len(indices))] else: # Use real scanfill. # See if its flipped the wrong way. flip = None for fi in fill: if flip != None: break for i, vi in enumerate(fi): if vi == 0 and fi[i - 1] == 1: flip = False break elif vi == 1 and fi[i - 1] == 0: flip = True break if not flip: for i, fi in enumerate(fill): fill[i] = tuple([ii for ii in reversed(fi)]) return fill def line_value(line_split): ''' Returns 1 string represneting the value for this line None will be returned if theres only 1 word ''' length = len(line_split) if length == 1: return None elif length == 2: return line_split[1] elif length > 2: return b' '.join(line_split[1:]) def obj_image_load(imagepath, DIR, use_image_search): if b'_' in imagepath: image = load_image(imagepath.replace(b'_', b' '), DIR) if image: return image image = load_image(imagepath, DIR) if image: return image print("failed to load %r doesn't exist" % imagepath) return None # def obj_image_load(imagepath, DIR, use_image_search): # ''' # Mainly uses comprehensiveImageLoad # but tries to replace '_' with ' ' for Max's exporter replaces spaces with underscores. # ''' # if '_' in imagepath: # image= BPyImage.comprehensiveImageLoad(imagepath, DIR, PLACE_HOLDER= False, RECURSIVE= use_image_search) # if image: return image # # Did the exporter rename the image? # image= BPyImage.comprehensiveImageLoad(imagepath.replace('_', ' '), DIR, PLACE_HOLDER= False, RECURSIVE= use_image_search) # if image: return image # # Return an image, placeholder if it dosnt exist # image= BPyImage.comprehensiveImageLoad(imagepath, DIR, PLACE_HOLDER= True, RECURSIVE= use_image_search) # return image def create_materials(filepath, material_libs, unique_materials, unique_material_images, use_image_search): ''' Create all the used materials in this obj, assign colors and images to the materials from all referenced material libs ''' DIR = os.path.dirname(filepath) #==================================================================================# # This function sets textures defined in .mtl file # #==================================================================================# def load_material_image(blender_material, context_material_name, imagepath, type): texture = bpy.data.textures.new(name=type, type='IMAGE') # Absolute path - c:\.. etc would work here image = obj_image_load(imagepath, DIR, use_image_search) has_data = False if image: texture.image = image has_data = image.has_data # Adds textures for materials (rendering) if type == 'Kd': if has_data and image.depth == 32: # Image has alpha mtex = blender_material.texture_slots.add() mtex.texture = texture mtex.texture_coords = 'UV' mtex.use_map_color_diffuse = True mtex.use_map_alpha = True texture.use_mipmap = True texture.use_interpolation = True texture.use_alpha = True blender_material.use_transparency = True blender_material.alpha = 0.0 else: mtex = blender_material.texture_slots.add() mtex.texture = texture mtex.texture_coords = 'UV' mtex.use_map_color_diffuse = True # adds textures to faces (Textured/Alt-Z mode) # Only apply the diffuse texture to the face if the image has not been set with the inline usemat func. unique_material_images[context_material_name] = image, has_data # set the texface image elif type == 'Ka': mtex = blender_material.texture_slots.add() mtex.texture = texture mtex.texture_coords = 'UV' mtex.use_map_ambient = True elif type == 'Ks': mtex = blender_material.texture_slots.add() mtex.texture = texture mtex.texture_coords = 'UV' mtex.use_map_specular = True elif type == 'Bump': mtex = blender_material.texture_slots.add() mtex.texture = texture mtex.texture_coords = 'UV' mtex.use_map_normal = True elif type == 'D': mtex = blender_material.texture_slots.add() mtex.texture = texture mtex.texture_coords = 'UV' mtex.use_map_alpha = True blender_material.use_transparency = True blender_material.transparency_method = 'Z_TRANSPARENCY' blender_material.alpha = 0.0 # Todo, unset deffuse material alpha if it has an alpha channel elif type == 'refl': mtex = blender_material.texture_slots.add() mtex.texture = texture mtex.texture_coords = 'UV' mtex.use_map_reflect = True else: raise Exception("invalid type %r" % type) # Add an MTL with the same name as the obj if no MTLs are spesified. temp_mtl = os.path.splitext((os.path.basename(filepath)))[0] + b'.mtl' if os.path.exists(os.path.join(DIR, temp_mtl)) and temp_mtl not in material_libs: material_libs.append(temp_mtl) del temp_mtl #Create new materials for name in unique_materials: # .keys() if name != None: unique_materials[name] = bpy.data.materials.new(name.decode('utf-8', "replace")) unique_material_images[name] = None, False # assign None to all material images to start with, add to later. unique_materials[None] = None unique_material_images[None] = None, False for libname in material_libs: # print(libname) mtlpath = os.path.join(DIR, libname) if not os.path.exists(mtlpath): print ("\tMaterial not found MTL: %r" % mtlpath) else: #print('\t\tloading mtl: %e' % mtlpath) context_material = None mtl = open(mtlpath, 'rb') for line in mtl: # .readlines(): line = line.strip() if not line or line.startswith(b'#'): pass elif line.startswith(b'newmtl'): context_material_name = line_value(line.split()) if context_material_name in unique_materials: context_material = unique_materials[context_material_name] else: context_material = None elif context_material: # we need to make a material to assign properties to it. line_split = line.split() line_lower = line.lower().lstrip() if line_lower.startswith(b'ka'): context_material.mirror_color = float(line_split[1]), float(line_split[2]), float(line_split[3]) elif line_lower.startswith(b'kd'): context_material.diffuse_color = float(line_split[1]), float(line_split[2]), float(line_split[3]) elif line_lower.startswith(b'ks'): context_material.specular_color = float(line_split[1]), float(line_split[2]), float(line_split[3]) elif line_lower.startswith(b'ns'): context_material.specular_hardness = int((float(line_split[1]) * 0.51)) elif line_lower.startswith(b'ni'): # Refraction index context_material.raytrace_transparency.ior = max(1, min(float(line_split[1]), 3)) # between 1 and 3 elif line_lower.startswith(b'd') or line_lower.startswith(b'tr'): context_material.alpha = float(line_split[1]) context_material.use_transparency = True context_material.transparency_method = 'Z_TRANSPARENCY' elif line_lower.startswith(b'tf'): # rgb, filter color, blender has no support for this. pass elif line_lower.startswith(b'illum'): illum = int(line_split[1]) do_ambient = True do_highlight = False do_reflection = False do_transparency = False do_glass = False do_fresnel = False do_raytrace = False # inline comments are from the spec, v4.2 if illum == 0: # Color on and Ambient off do_ambient = False elif illum == 1: # Color on and Ambient on pass elif illum == 2: # Highlight on do_highlight = True elif illum == 3: # Reflection on and Ray trace on do_reflection = True do_raytrace = True elif illum == 4: # Transparency: Glass on # Reflection: Ray trace on do_transparency = True do_reflection = True do_glass = True do_raytrace = True elif illum == 5: # Reflection: Fresnel on and Ray trace on do_reflection = True do_fresnel = True do_raytrace = True elif illum == 6: # Transparency: Refraction on # Reflection: Fresnel off and Ray trace on do_transparency = True do_reflection = True do_raytrace = True elif illum == 7: # Transparency: Refraction on # Reflection: Fresnel on and Ray trace on do_transparency = True do_reflection = True do_fresnel = True do_raytrace = True elif illum == 8: # Reflection on and Ray trace off do_reflection = True elif illum == 9: # Transparency: Glass on # Reflection: Ray trace off do_transparency = True do_reflection = True do_glass = True elif illum == 10: # Casts shadows onto invisible surfaces # blender cant do this pass if do_ambient: context_material.ambient = 1.0 else: context_material.ambient = 0.0 if do_highlight: # FIXME, how else to use this? context_material.specular_intensity = 1.0 if do_reflection: context_material.raytrace_mirror.use = True context_material.raytrace_mirror.reflect_factor = 1.0 if do_transparency: context_material.use_transparency = True context_material.transparency_method = 'RAYTRACE' if do_raytrace else 'Z_TRANSPARENCY' context_material.alpha = 0.0 if do_glass: context_material.raytrace_transparency.ior = 1.5 if do_fresnel: context_material.raytrace_mirror.fresnel = 1.0 # could be any value for 'ON' """ if do_raytrace: context_material.use_raytrace = True else: context_material.use_raytrace = False """ # XXX, this is not following the OBJ spec, but this was # written when raytracing wasnt default, annoying to disable for blender users. context_material.use_raytrace = True elif line_lower.startswith(b'map_ka'): img_filepath = line_value(line.split()) if img_filepath: load_material_image(context_material, context_material_name, img_filepath, 'Ka') elif line_lower.startswith(b'map_ks'): img_filepath = line_value(line.split()) if img_filepath: load_material_image(context_material, context_material_name, img_filepath, 'Ks') elif line_lower.startswith(b'map_kd'): img_filepath = line_value(line.split()) if img_filepath: load_material_image(context_material, context_material_name, img_filepath, 'Kd') elif line_lower.startswith(b'map_bump') or line_lower.startswith(b'bump'): # 'bump' is incorrect but some files use it. img_filepath = line_value(line.split()) if img_filepath: load_material_image(context_material, context_material_name, img_filepath, 'Bump') elif line_lower.startswith(b'map_d') or line_lower.startswith(b'map_tr'): # Alpha map - Dissolve img_filepath = line_value(line.split()) if img_filepath: load_material_image(context_material, context_material_name, img_filepath, 'D') elif line_lower.startswith(b'refl'): # reflectionmap img_filepath = line_value(line.split()) if img_filepath: load_material_image(context_material, context_material_name, img_filepath, 'refl') else: print("\t%r:%r (ignored)" % (filepath, line)) mtl.close() def split_mesh(verts_loc, faces, unique_materials, filepath, SPLIT_OB_OR_GROUP): ''' Takes vert_loc and faces, and separates into multiple sets of (verts_loc, faces, unique_materials, dataname) ''' filename = os.path.splitext((os.path.basename(filepath)))[0] if not SPLIT_OB_OR_GROUP: # use the filename for the object name since we arnt chopping up the mesh. return [(verts_loc, faces, unique_materials, filename)] def key_to_name(key): # if the key is a tuple, join it to make a string if not key: return filename # assume its a string. make sure this is true if the splitting code is changed else: return key # Return a key that makes the faces unique. face_split_dict = {} oldkey = -1 # initialize to a value that will never match the key for face in faces: key = face[4] if oldkey != key: # Check the key has changed. try: verts_split, faces_split, unique_materials_split, vert_remap = face_split_dict[key] except KeyError: faces_split = [] verts_split = [] unique_materials_split = {} vert_remap = [-1] * len(verts_loc) face_split_dict[key] = (verts_split, faces_split, unique_materials_split, vert_remap) oldkey = key face_vert_loc_indices = face[0] # Remap verts to new vert list and add where needed for enum, i in enumerate(face_vert_loc_indices): if vert_remap[i] == -1: new_index = len(verts_split) vert_remap[i] = new_index # set the new remapped index so we only add once and can reference next time. face_vert_loc_indices[enum] = new_index # remap to the local index verts_split.append(verts_loc[i]) # add the vert to the local verts else: face_vert_loc_indices[enum] = vert_remap[i] # remap to the local index matname = face[2] if matname and matname not in unique_materials_split: unique_materials_split[matname] = unique_materials[matname] faces_split.append(face) # remove one of the itemas and reorder return [(value[0], value[1], value[2], key_to_name(key)) for key, value in list(face_split_dict.items())] def create_mesh(new_objects, has_ngons, use_ngons, use_edges, verts_loc, verts_tex, faces, unique_materials, unique_material_images, unique_smooth_groups, vertex_groups, dataname): ''' Takes all the data gathered and generates a mesh, adding the new object to new_objects deals with fgons, sharp edges and assigning materials ''' if not has_ngons: use_ngons = False if unique_smooth_groups: sharp_edges = {} smooth_group_users = {context_smooth_group: {} for context_smooth_group in list(unique_smooth_groups.keys())} context_smooth_group_old = -1 # Split fgons into tri's fgon_edges = {} # Used for storing fgon keys if use_edges: edges = [] context_object = None # reverse loop through face indices for f_idx in range(len(faces) - 1, -1, -1): face_vert_loc_indices,\ face_vert_tex_indices,\ context_material,\ context_smooth_group,\ context_object = faces[f_idx] len_face_vert_loc_indices = len(face_vert_loc_indices) if len_face_vert_loc_indices == 1: faces.pop(f_idx) # cant add single vert faces elif not face_vert_tex_indices or len_face_vert_loc_indices == 2: # faces that have no texture coords are lines if use_edges: # generators are better in python 2.4+ but can't be used in 2.3 # edges.extend( (face_vert_loc_indices[i], face_vert_loc_indices[i+1]) for i in xrange(len_face_vert_loc_indices-1) ) edges.extend([(face_vert_loc_indices[i], face_vert_loc_indices[i + 1]) for i in range(len_face_vert_loc_indices - 1)]) faces.pop(f_idx) else: # Smooth Group if unique_smooth_groups and context_smooth_group: # Is a part of of a smooth group and is a face if context_smooth_group_old is not context_smooth_group: edge_dict = smooth_group_users[context_smooth_group] context_smooth_group_old = context_smooth_group for i in range(len_face_vert_loc_indices): i1 = face_vert_loc_indices[i] i2 = face_vert_loc_indices[i - 1] if i1 > i2: i1, i2 = i2, i1 try: edge_dict[i1, i2] += 1 except KeyError: edge_dict[i1, i2] = 1 # FGons into triangles if has_ngons and len_face_vert_loc_indices > 4: ngon_face_indices = BPyMesh_ngon(verts_loc, face_vert_loc_indices) faces.extend( [( [face_vert_loc_indices[ngon[0]], face_vert_loc_indices[ngon[1]], face_vert_loc_indices[ngon[2]]], [face_vert_tex_indices[ngon[0]], face_vert_tex_indices[ngon[1]], face_vert_tex_indices[ngon[2]]], context_material, context_smooth_group, context_object) for ngon in ngon_face_indices] ) # edges to make fgons if use_ngons: edge_users = {} for ngon in ngon_face_indices: for i in (0, 1, 2): i1 = face_vert_loc_indices[ngon[i]] i2 = face_vert_loc_indices[ngon[i - 1]] if i1 > i2: i1, i2 = i2, i1 try: edge_users[i1, i2] += 1 except KeyError: edge_users[i1, i2] = 1 for key, users in edge_users.items(): if users > 1: fgon_edges[key] = None # remove all after 3, means we dont have to pop this one. faces.pop(f_idx) # Build sharp edges if unique_smooth_groups: for edge_dict in list(smooth_group_users.values()): for key, users in list(edge_dict.items()): if users == 1: # This edge is on the boundry of a group sharp_edges[key] = None # map the material names to an index material_mapping = {name: i for i, name in enumerate(unique_materials)} # enumerate over unique_materials keys() materials = [None] * len(unique_materials) for name, index in list(material_mapping.items()): materials[index] = unique_materials[name] me = bpy.data.meshes.new(dataname.decode('utf-8', "replace")) # make sure the list isnt too big for material in materials: me.materials.append(material) me.vertices.add(len(verts_loc)) me.faces.add(len(faces)) # verts_loc is a list of (x, y, z) tuples me.vertices.foreach_set("co", unpack_list(verts_loc)) # faces is a list of (vert_indices, texco_indices, ...) tuples # XXX faces should contain either 3 or 4 verts # XXX no check for valid face indices me.faces.foreach_set("vertices_raw", unpack_face_list([f[0] for f in faces])) if verts_tex and me.faces: me.uv_textures.new() context_material_old = -1 # avoid a dict lookup mat = 0 # rare case it may be un-initialized. me_faces = me.faces for i, face in enumerate(faces): if len(face[0]) < 2: pass # raise "bad face" elif len(face[0]) == 2: if use_edges: edges.append(face[0]) else: blender_face = me.faces[i] face_vert_loc_indices,\ face_vert_tex_indices,\ context_material,\ context_smooth_group,\ context_object = face if context_smooth_group: blender_face.use_smooth = True if context_material: if context_material_old is not context_material: mat = material_mapping[context_material] context_material_old = context_material blender_face.material_index = mat # blender_face.mat= mat if verts_tex: blender_tface = me.uv_textures[0].data[i] if context_material: image, has_data = unique_material_images[context_material] if image: # Can be none if the material dosnt have an image. blender_tface.image = image blender_tface.use_image = True if has_data and image.depth == 32: blender_tface.blend_type = 'ALPHA' # BUG - Evil eekadoodle problem where faces that have vert index 0 location at 3 or 4 are shuffled. if len(face_vert_loc_indices) == 4: if face_vert_loc_indices[2] == 0 or face_vert_loc_indices[3] == 0: face_vert_tex_indices = face_vert_tex_indices[2], face_vert_tex_indices[3], face_vert_tex_indices[0], face_vert_tex_indices[1] else: # length of 3 if face_vert_loc_indices[2] == 0: face_vert_tex_indices = face_vert_tex_indices[1], face_vert_tex_indices[2], face_vert_tex_indices[0] # END EEEKADOODLE FIX # assign material, uv's and image blender_tface.uv1 = verts_tex[face_vert_tex_indices[0]] blender_tface.uv2 = verts_tex[face_vert_tex_indices[1]] blender_tface.uv3 = verts_tex[face_vert_tex_indices[2]] if len(face_vert_loc_indices) == 4: blender_tface.uv4 = verts_tex[face_vert_tex_indices[3]] # for ii, uv in enumerate(blender_face.uv): # uv.x, uv.y= verts_tex[face_vert_tex_indices[ii]] del me_faces # del ALPHA if use_edges and not edges: use_edges = False if use_edges: me.edges.add(len(edges)) # edges should be a list of (a, b) tuples me.edges.foreach_set("vertices", unpack_list(edges)) # me_edges.extend( edges ) # del me_edges # Add edge faces. # me_edges= me.edges def edges_match(e1, e2): return (e1[0] == e2[0] and e1[1] == e2[1]) or (e1[0] == e2[1] and e1[1] == e2[0]) # XXX slow # if use_ngons and fgon_edges: # for fgon_edge in fgon_edges.keys(): # for ed in me.edges: # if edges_match(fgon_edge, ed.vertices): # ed.is_fgon = True # if use_ngons and fgon_edges: # FGON= Mesh.EdgeFlags.FGON # for ed in me.findEdges( fgon_edges.keys() ): # if ed is not None: # me_edges[ed].flag |= FGON # del FGON # XXX slow # if unique_smooth_groups and sharp_edges: # for sharp_edge in sharp_edges.keys(): # for ed in me.edges: # if edges_match(sharp_edge, ed.vertices): # ed.use_edge_sharp = True # if unique_smooth_groups and sharp_edges: # SHARP= Mesh.EdgeFlags.SHARP # for ed in me.findEdges( sharp_edges.keys() ): # if ed is not None: # me_edges[ed].flag |= SHARP # del SHARP me.validate() me.update(calc_edges=use_edges) ob = bpy.data.objects.new("Mesh", me) new_objects.append(ob) # Create the vertex groups. No need to have the flag passed here since we test for the # content of the vertex_groups. If the user selects to NOT have vertex groups saved then # the following test will never run for group_name, group_indices in vertex_groups.items(): group = ob.vertex_groups.new(group_name.decode('utf-8', "replace")) group.add(group_indices, 1.0, 'REPLACE') def create_nurbs(context_nurbs, vert_loc, new_objects): ''' Add nurbs object to blender, only support one type at the moment ''' deg = context_nurbs.get(b'deg', (3,)) curv_range = context_nurbs.get(b'curv_range') curv_idx = context_nurbs.get(b'curv_idx', []) parm_u = context_nurbs.get(b'parm_u', []) parm_v = context_nurbs.get(b'parm_v', []) name = context_nurbs.get(b'name', b'ObjNurb') cstype = context_nurbs.get(b'cstype') if cstype is None: print('\tWarning, cstype not found') return if cstype != b'bspline': print('\tWarning, cstype is not supported (only bspline)') return if not curv_idx: print('\tWarning, curv argument empty or not set') return if len(deg) > 1 or parm_v: print('\tWarning, surfaces not supported') return cu = bpy.data.curves.new(name.decode('utf-8', "replace"), 'CURVE') cu.dimensions = '3D' nu = cu.splines.new('NURBS') nu.points.add(len(curv_idx) - 1) # a point is added to start with nu.points.foreach_set("co", [co_axis for vt_idx in curv_idx for co_axis in (vert_loc[vt_idx] + (1.0,))]) nu.order_u = deg[0] + 1 # get for endpoint flag from the weighting if curv_range and len(parm_u) > deg[0] + 1: do_endpoints = True for i in range(deg[0] + 1): if abs(parm_u[i] - curv_range[0]) > 0.0001: do_endpoints = False break if abs(parm_u[-(i + 1)] - curv_range[1]) > 0.0001: do_endpoints = False break else: do_endpoints = False if do_endpoints: nu.use_endpoint_u = True # close ''' do_closed = False if len(parm_u) > deg[0]+1: for i in xrange(deg[0]+1): #print curv_idx[i], curv_idx[-(i+1)] if curv_idx[i]==curv_idx[-(i+1)]: do_closed = True break if do_closed: nu.use_cyclic_u = True ''' ob = bpy.data.objects.new(name.decode('utf-8', "replace"), cu) new_objects.append(ob) def strip_slash(line_split): if line_split[-1][-1] == 92: # '\' char if len(line_split[-1]) == 1: line_split.pop() # remove the \ item else: line_split[-1] = line_split[-1][:-1] # remove the \ from the end last number return True return False def get_float_func(filepath): ''' find the float function for this obj file - whether to replace commas or not ''' file = open(filepath, 'rb') for line in file: # .readlines(): line = line.lstrip() if line.startswith(b'v'): # vn vt v if b',' in line: return lambda f: float(f.replace(b',', b'.')) elif b'.' in line: return float # incase all vert values were ints return float def load(operator, context, filepath, global_clamp_size=0.0, use_ngons=True, use_smooth_groups=True, use_edges=True, use_split_objects=True, use_split_groups=True, use_rotate_x90=True, use_image_search=True, use_groups_as_vgroups=False): ''' Called by the user interface or another script. load_obj(path) - should give acceptable results. This function passes the file and sends the data off to be split into objects and then converted into mesh objects ''' print('\nimporting obj %r' % filepath) filepath = filepath.encode() if use_split_objects or use_split_groups: use_groups_as_vgroups = False time_main = time.time() verts_loc = [] verts_tex = [] faces = [] # tuples of the faces material_libs = [] # filanems to material libs this uses vertex_groups = {} # when use_groups_as_vgroups is true # Get the string to float conversion func for this file- is 'float' for almost all files. float_func = get_float_func(filepath) # Context variables context_material = None context_smooth_group = None context_object = None context_vgroup = None # Nurbs context_nurbs = {} nurbs = [] context_parm = b'' # used by nurbs too but could be used elsewhere has_ngons = False # has_smoothgroups= False - is explicit with len(unique_smooth_groups) being > 0 # Until we can use sets unique_materials = {} unique_material_images = {} unique_smooth_groups = {} # unique_obects= {} - no use for this variable since the objects are stored in the face. # when there are faces that end with \ # it means they are multiline- # since we use xreadline we cant skip to the next line # so we need to know whether context_multi_line = b'' print("\tparsing obj file...") time_sub = time.time() # time_sub= sys.time() file = open(filepath, 'rb') for line in file: # .readlines(): line = line.lstrip() # rare cases there is white space at the start of the line if line.startswith(b"v "): line_split = line.split() # rotate X90: (x,-z,y) verts_loc.append((float_func(line_split[1]), -float_func(line_split[3]), float_func(line_split[2]))) elif line.startswith(b"vn "): pass elif line.startswith(b"vt "): line_split = line.split() verts_tex.append((float_func(line_split[1]), float_func(line_split[2]))) # Handel faces lines (as faces) and the second+ lines of fa multiline face here # use 'f' not 'f ' because some objs (very rare have 'fo ' for faces) elif line.startswith(b'f') or context_multi_line == b'f': if context_multi_line: # use face_vert_loc_indices and face_vert_tex_indices previously defined and used the obj_face line_split = line.split() else: line_split = line[2:].split() face_vert_loc_indices = [] face_vert_tex_indices = [] # Instance a face faces.append((\ face_vert_loc_indices,\ face_vert_tex_indices,\ context_material,\ context_smooth_group,\ context_object\ )) if strip_slash(line_split): context_multi_line = b'f' else: context_multi_line = b'' for v in line_split: obj_vert = v.split(b'/') vert_loc_index = int(obj_vert[0]) - 1 # Add the vertex to the current group # *warning*, this wont work for files that have groups defined around verts if use_groups_as_vgroups and context_vgroup: vertex_groups[context_vgroup].append(vert_loc_index) # Make relative negative vert indices absolute if vert_loc_index < 0: vert_loc_index = len(verts_loc) + vert_loc_index + 1 face_vert_loc_indices.append(vert_loc_index) if len(obj_vert) > 1 and obj_vert[1]: # formatting for faces with normals and textures us # loc_index/tex_index/nor_index vert_tex_index = int(obj_vert[1]) - 1 # Make relative negative vert indices absolute if vert_tex_index < 0: vert_tex_index = len(verts_tex) + vert_tex_index + 1 face_vert_tex_indices.append(vert_tex_index) else: # dummy face_vert_tex_indices.append(0) if len(face_vert_loc_indices) > 4: has_ngons = True elif use_edges and (line.startswith(b'l ') or context_multi_line == b'l'): # very similar to the face load function above with some parts removed if context_multi_line: # use face_vert_loc_indices and face_vert_tex_indices previously defined and used the obj_face line_split = line.split() else: line_split = line[2:].split() face_vert_loc_indices = [] face_vert_tex_indices = [] # Instance a face faces.append((\ face_vert_loc_indices,\ face_vert_tex_indices,\ context_material,\ context_smooth_group,\ context_object\ )) if strip_slash(line_split): context_multi_line = b'l' else: context_multi_line = b'' isline = line.startswith(b'l') for v in line_split: vert_loc_index = int(v) - 1 # Make relative negative vert indices absolute if vert_loc_index < 0: vert_loc_index = len(verts_loc) + vert_loc_index + 1 face_vert_loc_indices.append(vert_loc_index) elif line.startswith(b's'): if use_smooth_groups: context_smooth_group = line_value(line.split()) if context_smooth_group == b'off': context_smooth_group = None elif context_smooth_group: # is not None unique_smooth_groups[context_smooth_group] = None elif line.startswith(b'o'): if use_split_objects: context_object = line_value(line.split()) # unique_obects[context_object]= None elif line.startswith(b'g'): if use_split_groups: context_object = line_value(line.split()) # print 'context_object', context_object # unique_obects[context_object]= None elif use_groups_as_vgroups: context_vgroup = line_value(line.split()) if context_vgroup and context_vgroup != b'(null)': vertex_groups.setdefault(context_vgroup, []) else: context_vgroup = None # dont assign a vgroup elif line.startswith(b'usemtl'): context_material = line_value(line.split()) unique_materials[context_material] = None elif line.startswith(b'mtllib'): # usemap or usemat material_libs = list(set(material_libs) | set(line.split()[1:])) # can have multiple mtllib filenames per line, mtllib can appear more than once, so make sure only occurance of material exists # Nurbs support elif line.startswith(b'cstype '): context_nurbs[b'cstype'] = line_value(line.split()) # 'rat bspline' / 'bspline' elif line.startswith(b'curv ') or context_multi_line == b'curv': line_split = line.split() curv_idx = context_nurbs[b'curv_idx'] = context_nurbs.get(b'curv_idx', []) # incase were multiline if not context_multi_line: context_nurbs[b'curv_range'] = float_func(line_split[1]), float_func(line_split[2]) line_split[0:3] = [] # remove first 3 items if strip_slash(line_split): context_multi_line = b'curv' else: context_multi_line = b'' for i in line_split: vert_loc_index = int(i) - 1 if vert_loc_index < 0: vert_loc_index = len(verts_loc) + vert_loc_index + 1 curv_idx.append(vert_loc_index) elif line.startswith(b'parm') or context_multi_line == b'parm': line_split = line.split() if context_multi_line: context_multi_line = b'' else: context_parm = line_split[1] line_split[0:2] = [] # remove first 2 if strip_slash(line_split): context_multi_line = b'parm' else: context_multi_line = b'' if context_parm.lower() == b'u': context_nurbs.setdefault(b'parm_u', []).extend([float_func(f) for f in line_split]) elif context_parm.lower() == b'v': # surfaces not supported yet context_nurbs.setdefault(b'parm_v', []).extend([float_func(f) for f in line_split]) # else: # may want to support other parm's ? elif line.startswith(b'deg '): context_nurbs[b'deg'] = [int(i) for i in line.split()[1:]] elif line.startswith(b'end'): # Add the nurbs curve if context_object: context_nurbs[b'name'] = context_object nurbs.append(context_nurbs) context_nurbs = {} context_parm = b'' ''' # How to use usemap? depricated? elif line.startswith(b'usema'): # usemap or usemat context_image= line_value(line.split()) ''' file.close() time_new = time.time() print("%.4f sec" % (time_new - time_sub)) time_sub = time_new print('\tloading materials and images...') create_materials(filepath, material_libs, unique_materials, unique_material_images, use_image_search) time_new = time.time() print("%.4f sec" % (time_new - time_sub)) time_sub = time_new if not use_rotate_x90: verts_loc[:] = [(v[0], v[2], -v[1]) for v in verts_loc] # deselect all if bpy.ops.object.select_all.poll(): bpy.ops.object.select_all(action='DESELECT') scene = context.scene # scn.objects.selected = [] new_objects = [] # put new objects here print('\tbuilding geometry...\n\tverts:%i faces:%i materials: %i smoothgroups:%i ...' % (len(verts_loc), len(faces), len(unique_materials), len(unique_smooth_groups))) # Split the mesh by objects/materials, may if use_split_objects or use_split_groups: SPLIT_OB_OR_GROUP = True else: SPLIT_OB_OR_GROUP = False for verts_loc_split, faces_split, unique_materials_split, dataname in split_mesh(verts_loc, faces, unique_materials, filepath, SPLIT_OB_OR_GROUP): # Create meshes from the data, warning 'vertex_groups' wont support splitting create_mesh(new_objects, has_ngons, use_ngons, use_edges, verts_loc_split, verts_tex, faces_split, unique_materials_split, unique_material_images, unique_smooth_groups, vertex_groups, dataname) # nurbs support for context_nurbs in nurbs: create_nurbs(context_nurbs, verts_loc, new_objects) # Create new obj for obj in new_objects: base = scene.objects.link(obj) base.select = True scene.update() axis_min = [1000000000] * 3 axis_max = [-1000000000] * 3 if global_clamp_size: # Get all object bounds for ob in new_objects: for v in ob.bound_box: for axis, value in enumerate(v): if axis_min[axis] > value: axis_min[axis] = value if axis_max[axis] < value: axis_max[axis] = value # Scale objects max_axis = max(axis_max[0] - axis_min[0], axis_max[1] - axis_min[1], axis_max[2] - axis_min[2]) scale = 1.0 while global_clamp_size < max_axis * scale: scale = scale / 10.0 for obj in new_objects: obj.scale = scale, scale, scale # Better rotate the vert locations #if not use_rotate_x90: # for ob in new_objects: # ob.RotX = -1.570796326794896558 time_new = time.time() print("finished importing: %r in %.4f sec." % (filepath, (time_new - time_main))) return {'FINISHED'} # NOTES (all line numbers refer to 2.4x import_obj.py, not this file) # check later: line 489 # can convert now: edge flags, edges: lines 508-528 # ngon (uses python module BPyMesh): 384-414 # NEXT clamp size: get bound box with RNA # get back to l 140 (here) # search image in bpy.config.textureDir - load_image # replaced BPyImage.comprehensiveImageLoad with a simplified version that only checks additional directory specified, but doesn't search dirs recursively (obj_image_load) # bitmask won't work? - 132 # uses bpy.sys.time() if __name__ == "__main__": register()