diff --git a/release/scripts/3ds_export.py b/release/scripts/3ds_export.py index ebe06c3d4eb..938afcd1bc6 100644 --- a/release/scripts/3ds_export.py +++ b/release/scripts/3ds_export.py @@ -2,21 +2,22 @@ """ Name: '3D Studio (.3ds)...' -Blender: 237 +Blender: 241 Group: 'Export' Tooltip: 'Export to 3DS file format (.3ds).' """ -__author__ = ["Campbell Barton", "Bob Holcomb", "Richard Lärkäng", "Damien McGinnes"] -__url__ = ("blender", "elysiun", "http://www.gametutorials.com") -__version__ = "0.82" +__author__ = ["Campbell Barton", "Bob Holcomb", "Richard Lärkäng", "Damien McGinnes", "Mark Stijnman"] +__url__ = ("blender", "elysiun", "http://www.gametutorials.com", "http://lib3ds.sourceforge.net/") +__version__ = "0.90" __bpydoc__ = """\ 3ds Exporter -This script Exports a 3ds file and the materials into blender for editing. +This script Exports a 3ds file. -Exporting is based on 3ds loader from www.gametutorials.com(Thanks DigiBen). +Exporting is based on 3ds loader from www.gametutorials.com(Thanks DigiBen) and using information +from the lib3ds project (http://lib3ds.sourceforge.net/) sourcecode. """ # ***** BEGIN GPL LICENSE BLOCK ***** @@ -61,7 +62,7 @@ PRIMARY= long("0x4D4D",16) #------ Main Chunks OBJECTINFO = long("0x3D3D",16); #This gives the version of the mesh and is found right before the material and object information VERSION = long("0x0002",16); #This gives the version of the .3ds file -EDITKEYFRAME= long("0xB000",16); #This is the header for all of the key frame info +KFDATA = long("0xB000",16); #This is the header for all of the key frame info #------ sub defines of OBJECTINFO MATERIAL=45055 #0xAFFF // This stored the texture info @@ -94,580 +95,775 @@ OBJECT_MATERIAL = long("0x4130",16); # This is found if the object has a OBJECT_UV = long("0x4140",16); # The UV texture coordinates OBJECT_TRANS_MATRIX = long("0x4160",16); # The Object Matrix +#>------ sub defines of KFDATA +KFDATA_KFHDR = long("0xB00A",16); +KFDATA_KFSEG = long("0xB008",16); +KFDATA_KFCURTIME = long("0xB009",16); +KFDATA_OBJECT_NODE_TAG = long("0xB002",16); + +#>------ sub defines of OBJECT_NODE_TAG +OBJECT_NODE_ID = long("0xB030",16); +OBJECT_NODE_HDR = long("0xB010",16); +OBJECT_PIVOT = long("0xB013",16); +OBJECT_INSTANCE_NAME = long("0xB011",16); +POS_TRACK_TAG = long("0xB020",16); +ROT_TRACK_TAG = long("0xB021",16); +SCL_TRACK_TAG = long("0xB022",16); + + #==============================================# # Strips the slashes from the back of a string # #==============================================# def stripPath(path): + """Strips the slashes from the back of a string. + """ return path.split('/')[-1].split('\\')[-1] #==================================================# # New name based on old with a different extension # #==================================================# def newFName(ext): + """New name based on old with a different extension. + """ return Blender.Get('filename')[: -len(Blender.Get('filename').split('.', -1)[-1]) ] + ext -#the chunk class -class chunk: - ID=0 +# size defines: +SZ_SHORT = 2 +SZ_INT = 4 +SZ_FLOAT = 4 + +class _3ds_short: + """Class representing a short (2-byte integer) for a 3ds file.""" + value=0 + + def __init__(self, val=0): + self.value=val + + def get_size(self): + return SZ_SHORT + + def write(self,file): + data=struct.pack(" 2: + num_fv = len(face) + if num_fv==3: + new_tri = tri((face[0].index, face[1].index, face[2].index), face.mat) + if (do_uv): + new_tri.uvco = face.uv + tri_list.append(new_tri) + + elif num_fv==4: #it's a quad + first_tri, second_tri = split_into_tri(face, do_uv) + tri_list.append(first_tri) + tri_list.append(second_tri) + + return tri_list + + +def remove_face_uv(verts, tri_list): + """Remove face UV coordinates from a list of triangles. + + Since 3ds files only support one pair of uv coordinates for each vertex, face uv coordinates + need to be converted to vertex uv coordinates. That means that vertices need to be duplicated when + there are multiple uv coordinates per vertex.""" + + # initialize a list of UniqueLists, one per vertex: + uv_list = [UniqueList() for i in xrange(len(verts))] + + offset_list = [ ] + + # for each face uv coordinate, add it to the UniqueList of the vertex + for tri in tri_list: + offset = [] + for i in xrange(3): + # store the index into the UniqueList for future reference: + offset.append(uv_list[tri.vertex_index[i]].add(_3ds_point_uv(tri.uvco[i]))) + offset_list.append(offset) + # At this point, each vertex has a UniqueList containing every uv coordinate that is associated with it + # only once. + + # Now we need to duplicate every vertex as many times as it has uv coordinates and make sure the + # faces refer to the new face indices: + vert_index = 0 + vert_array = _3ds_array() + uv_array = _3ds_array() + index_list=[] + for i,vert in enumerate(verts): + index_list.append(vert_index) + for uv in uv_list[i].items: + # add a vertex duplicate to the vertex_array for every uv associated with this vertex: + vert_array.add(_3ds_point_3d(vert)) + # add the uv coordinate to the uv array: + uv_array.add(uv) + vert_index+=1 + + # Make sure the triangle vertex indices now refer to the new vertex list: + for tri, offset in zip(tri_list, offset_list): + for i in xrange(3): + offset[i]+=index_list[tri.vertex_index[i]] + tri.vertex_index = offset + + return vert_array, uv_array, tri_list + +def make_faces_chunk(tri_list, materials): + """Make a chunk for the faces. + + Also adds subchunks assigning materials to all faces.""" + face_chunk = _3ds_chunk(OBJECT_FACES) + face_list = _3ds_array() + + obj_material_faces=[] + obj_material_names=[] + for m in materials: + obj_material_names.append(_3ds_string(m.name)) + obj_material_faces.append(_3ds_array()) + n_materials = len(obj_material_names) + + for i,tri in enumerate(tri_list): + face_list.add(_3ds_face(tri.vertex_index)) + if (tri.mat < n_materials): + obj_material_faces[tri.mat].add(_3ds_short(i)) + + face_chunk.add_variable("faces", face_list) + + for i in xrange(n_materials): + obj_material_chunk=_3ds_chunk(OBJECT_MATERIAL) + obj_material_chunk.add_variable("name", obj_material_names[i]) + obj_material_chunk.add_variable("face_list", obj_material_faces[i]) + face_chunk.add_subchunk(obj_material_chunk) + + return face_chunk + +def make_vert_chunk(vert_array): + """Make a vertex chunk out of an array of vertices.""" + vert_chunk = _3ds_chunk(OBJECT_VERTICES) + vert_chunk.add_variable("vertices",vert_array) + return vert_chunk + +def make_uv_chunk(uv_array): + """Make a UV chunk out of an array of UVs.""" + uv_chunk = _3ds_chunk(OBJECT_UV) + uv_chunk.add_variable("uv coords", uv_array) + return uv_chunk + +def make_mesh_chunk(mesh): + """Make a chunk out of a Blender mesh.""" + + # Extract the triangles from the mesh: + tri_list = extract_triangles(mesh) + + if (mesh.hasFaceUV()): + # Remove the face UVs and convert it to vertex UV: + vert_array, uv_array, tri_list = remove_face_uv(mesh.verts, tri_list) + else: + # Add the vertices to the vertex array: + vert_array = _3ds_array() + for vert in mesh.verts: + vert_array.add(_3ds_point_3d(vert.co)) + # If the mesh has vertex UVs, create an array of UVs: + if (mesh.hasVertexUV()): + uv_array = _3ds_array() + for vert in mesh.verts: + uv_array.add(_3ds_point_uv(vert.uvco)) + else: + # no UV at all: + uv_array = None + + # create the chunk: + mesh_chunk = _3ds_chunk(OBJECT_MESH) + + # add vertex chunk: + mesh_chunk.add_subchunk(make_vert_chunk(vert_array)) + # add faces chunk: + mesh_chunk.add_subchunk(make_faces_chunk(tri_list, mesh.materials)) + # if available, add uv chunk: + if uv_array: + mesh_chunk.add_subchunk(make_uv_chunk(uv_array)) + + return mesh_chunk + +def make_kfdata(start=0, stop=0, curtime=0): + """Make the basic keyframe data chunk""" + kfdata = _3ds_chunk(KFDATA) + + kfhdr = _3ds_chunk(KFDATA_KFHDR) + kfhdr.add_variable("revision", _3ds_short(0)) + # Not really sure what filename is used for, but it seems it is usually used + # to identify the program that generated the .3ds: + kfhdr.add_variable("filename", _3ds_string("Blender")) + kfhdr.add_variable("animlen", _3ds_int(stop-start)) + + kfseg = _3ds_chunk(KFDATA_KFSEG) + kfseg.add_variable("start", _3ds_int(start)) + kfseg.add_variable("stop", _3ds_int(stop)) + + kfcurtime = _3ds_chunk(KFDATA_KFCURTIME) + kfcurtime.add_variable("curtime", _3ds_int(curtime)) + + kfdata.add_subchunk(kfhdr) + kfdata.add_subchunk(kfseg) + kfdata.add_subchunk(kfcurtime) + return kfdata + +def make_track_chunk(ID, obj): + """Make a chunk for track data. + + Depending on the ID, this will construct a position, rotation or scale track.""" + track_chunk = _3ds_chunk(ID) + track_chunk.add_variable("track_flags", _3ds_short()) + track_chunk.add_variable("unknown", _3ds_int()) + track_chunk.add_variable("unknown", _3ds_int()) + track_chunk.add_variable("nkeys", _3ds_int(1)) + # Next section should be repeated for every keyframe, but for now, animation is not actually supported. + track_chunk.add_variable("tcb_frame", _3ds_int(0)) + track_chunk.add_variable("tcb_flags", _3ds_short()) + if ID==POS_TRACK_TAG: + # position vector: + track_chunk.add_variable("position", _3ds_point_3d(obj.getLocation())) + elif ID==ROT_TRACK_TAG: + # rotation (quaternion, angle first, followed by axis): + q = obj.getEuler().toQuat() + track_chunk.add_variable("rotation", _3ds_point_4d((q.angle, q.axis[0], q.axis[1], q.axis[2]))) + elif ID==SCL_TRACK_TAG: + # scale vector: + track_chunk.add_variable("scale", _3ds_point_3d(obj.getSize())) + + return track_chunk + +def make_kf_obj_node(obj, name_to_id): + """Make a node chunk for a Blender object. + + Takes the Blender object as a parameter. Object id's are taken from the dictionary name_to_id. + Blender Empty objects are converted to dummy nodes.""" + + name = obj.getName() + # main object node chunk: + kf_obj_node = _3ds_chunk(KFDATA_OBJECT_NODE_TAG) + # chunk for the object id: + obj_id_chunk = _3ds_chunk(OBJECT_NODE_ID) + # object id is from the name_to_id dictionary: + obj_id_chunk.add_variable("node_id", _3ds_short(name_to_id[name])) + + # object node header: + obj_node_header_chunk = _3ds_chunk(OBJECT_NODE_HDR) + # object name: + if (obj.getType() == 'Empty'): + # Empties are called "$$$DUMMY" and use the OBJECT_INSTANCE_NAME chunk + # for their name (see below): + obj_node_header_chunk.add_variable("name", _3ds_string("$$$DUMMY")) + else: + # Add the name: + obj_node_header_chunk.add_variable("name", _3ds_string(name)) + # Add Flag variables (not sure what they do): + obj_node_header_chunk.add_variable("flags1", _3ds_short(0)) + obj_node_header_chunk.add_variable("flags2", _3ds_short(0)) + + # Check parent-child relationships: + parent = obj.getParent() + if (parent == None) or (parent.getName() not in name_to_id): + # If no parent, or the parents name is not in the name_to_id dictionary, + # parent id becomes -1: + obj_node_header_chunk.add_variable("parent", _3ds_short(-1)) + else: + # Get the parent's id from the name_to_id dictionary: + obj_node_header_chunk.add_variable("parent", _3ds_short(name_to_id[parent.getName()])) + + # Add pivot chunk: + obj_pivot_chunk = _3ds_chunk(OBJECT_PIVOT) + obj_pivot_chunk.add_variable("pivot", _3ds_point_3d(obj.getLocation())) + kf_obj_node.add_subchunk(obj_pivot_chunk) + + # add subchunks for object id and node header: + kf_obj_node.add_subchunk(obj_id_chunk) + kf_obj_node.add_subchunk(obj_node_header_chunk) + + # Empty objects need to have an extra chunk for the instance name: + if (obj.getType() == 'Empty'): + obj_instance_name_chunk = _3ds_chunk(OBJECT_INSTANCE_NAME) + obj_instance_name_chunk.add_variable("name", _3ds_string(name)) + kf_obj_node.add_subchunk(obj_instance_name_chunk) + + # Add track chunks for position, rotation and scale: + kf_obj_node.add_subchunk(make_track_chunk(POS_TRACK_TAG, obj)) + kf_obj_node.add_subchunk(make_track_chunk(ROT_TRACK_TAG, obj)) + kf_obj_node.add_subchunk(make_track_chunk(SCL_TRACK_TAG, obj)) + + return kf_obj_node + + def save_3ds(filename): + """Save the Blender scene to a 3ds file.""" # Time the export time1 = Blender.sys.time() - exported_materials = {} - - #fill the chunks full of data - primary=primary_chunk() - #get all the objects in this scene - object_list = [ ob for ob in Blender.Object.GetSelected() if ob.getType() == 'Mesh' ] - #fill up the data structures with objects - for obj in object_list: + # Initialize the main chunk (primary): + primary = _3ds_chunk(PRIMARY) + # Add version chunk: + version_chunk = _3ds_chunk(VERSION) + version_chunk.add_variable("version", _3ds_int(3)) + primary.add_subchunk(version_chunk) + + # init main object info chunk: + object_info = _3ds_chunk(OBJECTINFO) + + # init main key frame data chunk: + kfdata = make_kfdata() + + # Make chunks for all materials in the scene: + for material in Material.Get(): + object_info.add_subchunk(make_material_chunk(material)) + + + # Get all the supported objects in this scene + mesh_objects = [ ob for ob in Blender.Object.Get() if ob.getType() == 'Mesh' ] + empty_objects = [ ob for ob in Blender.Object.Get() if ob.getType() == 'Empty' ] + + all_objects = mesh_objects + empty_objects + + # Give all objects a unique ID and build a dictionary from object name to object id: + name_to_id = {} + for i,obj in enumerate(all_objects): + name_to_id[obj.getName()] = i + + # Create object chunks for all meshes: + for obj in mesh_objects: #create a new object chunk - primary.obj_info.obj_chunks.append(object_chunk()) + object_chunk = _3ds_chunk(OBJECT) + #get the mesh data blender_mesh = obj.getData() blender_mesh.transform(obj.getMatrix()) + #set the object name - primary.obj_info.obj_chunks[len(primary.obj_info.obj_chunks)-1].name=obj.getName() - - matrix = obj.getMatrix() - - #make a new mesh chunk object - mesh=mesh_chunk() + object_chunk.add_variable("name", _3ds_string(obj.getName())) - mesh.v_chunk.verts = blender_mesh.verts + # make a mesh chunk out of the mesh: + object_chunk.add_subchunk(make_mesh_chunk(blender_mesh)) + object_info.add_subchunk(object_chunk) - dummy = None # just incase... - - for m in blender_mesh.materials: - mesh.f_chunk.m_chunks.append(obj_material_chunk()) - mesh.f_chunk.m_chunks[len(mesh.f_chunk.m_chunks)-1].name = m.name + # make a kf object node for the object: + kfdata.add_subchunk(make_kf_obj_node(obj, name_to_id)) - # materials should only be exported once - try: - dummy = exported_materials[m.name] - - - except KeyError: - material = material_chunk() - material.matname_chunk.name=m.name - material.matambient_chunk.col1.col = m.mirCol - material.matambient_chunk.col2.col = m.mirCol - material.matdiffuse_chunk.col1.col = m.rgbCol - material.matdiffuse_chunk.col2.col = m.rgbCol - material.matspecular_chunk.col1.col = m.specCol - material.matspecular_chunk.col2.col = m.specCol - - primary.obj_info.mat_chunks.append(material) - - exported_materials[m.name] = None - - del dummy # unpolute the namespace - - valid_faces = [f for f in blender_mesh.faces if len(f) > 2] - facenr=0 - #fill in faces - for face in valid_faces: - - #is this a tri or a quad - num_fv=len(face.v) - - - #it's a tri - if num_fv==3: - mesh.f_chunk.faces.append((face[0].index, face[1].index, face[2].index)) - if (face.materialIndex < len(mesh.f_chunk.m_chunks)): - mesh.f_chunk.m_chunks[face.materialIndex].faces.append(facenr) - facenr+=1 - - else: #it's a quad - mesh.f_chunk.faces.append((face[0].index, face[1].index, face[2].index)) # 0,1,2 - mesh.f_chunk.faces.append((face[2].index, face[3].index, face[0].index)) # 2,3,0 - #first tri - if (face.materialIndex < len(mesh.f_chunk.m_chunks)): - mesh.f_chunk.m_chunks[face.materialIndex].faces.append(facenr) - facenr+=1 - #other tri - if (face.materialIndex < len(mesh.f_chunk.m_chunks)): - mesh.f_chunk.m_chunks[face.materialIndex].faces.append(facenr) - facenr+=1 - - - #fill in the UV info - if blender_mesh.hasVertexUV(): - for vert in blender_mesh.verts: - mesh.uv_chunk.uv.append((vert.uvco[0], vert.uvco[1])) - - elif blender_mesh.hasFaceUV(): - for face in valid_faces: - # Tri or quad. - for uv_coord in face.uv: - mesh.uv_chunk.uv.append((uv_coord[0], uv_coord[1])) - - #filled in our mesh, lets add it to the file - primary.obj_info.obj_chunks[len(primary.obj_info.obj_chunks)-1].mesh_chunks.append(mesh) - - #check the size + # Create chunks for all empties: + for obj in empty_objects: + # Empties only require a kf object node: + kfdata.add_subchunk(make_kf_obj_node(obj, name_to_id)) + + # Add main object info chunk to primary chunk: + primary.add_subchunk(object_info) + # Add main keyframe data chunk to primary chunk: + primary.add_subchunk(kfdata) + + # At this point, the chunk hierarchy is completely built. + + # Check the size: primary.get_size() - #open the files up for writing + # Open the file for writing: file = open( filename, "wb" ) - #recursively write the stuff to file + + # Recursively write the chunks to file: primary.write(file) + + # Close the file: file.close() + + # Debugging only: report the exporting time: print "3ds export time: %.2f" % (Blender.sys.time() - time1) + # Debugging only: dump the chunk hierarchy: + #primary.dump() + Blender.Window.FileSelector(save_3ds, "Export 3DS", newFName('3ds'))