|
|
| (One intermediate revision by one other user not shown) |
| Line 1: |
Line 1: |
| {{blender}} This is a script which exports SMD and VTA files from [[Blender]] 2.53 beta. Save it as a <code>.py</code> file in <code>.blender\scripts\io\</code>; see [[Blender]] for where to find that folder.
| | #redirect [[Blender Source Tools]] |
| | |
| '''This script exports all bones with rotations of (0,0,0) and cannot handle animations.''' If you can fix it, please do so!
| |
| | |
| <source lang=python>
| |
| # ##### 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 #####
| |
| | |
| __author__= "Tom Edwards"
| |
| __url__= ['http://developer.valvesoftware.com/wiki/Blender/SMD_export']
| |
| __version__= "0.3.1" # for Blender r30581 (2.53 beta)
| |
| | |
| import math, os, time, bpy, random, mathutils
| |
| from bpy import ops,data,context
| |
| vector = mathutils.Vector
| |
| euler = mathutils.Euler
| |
| matrix = mathutils.Matrix
| |
| rMat = mathutils.RotationMatrix
| |
| tMat = mathutils.TranslationMatrix
| |
| | |
| # I hate Python's var redefinition habits
| |
| class smd_info:
| |
| a = None # Armature object
| |
| m = None # Mesh object
| |
| file = None
| |
| jobName = None
| |
| jobType = None
| |
| startTime = 0
| |
| uiTime = 0
| |
|
| |
| def getFilename(filepath):
| |
| return filepath.split('\\')[-1].split('/')[-1].rsplit(".")[0]
| |
| def getFiledir(filepath):
| |
| return filepath.rstrip(filepath.split('\\')[-1].split('/')[-1])
| |
| | |
| import decimal
| |
| # rounds to 6 decimal places, converts between "1e-5" and "0.000001", outputs str
| |
| def getSmdFloat(fval):
| |
| return str(decimal.Decimal(str(round(float(fval),6)))) # yes, it really is all needed
| |
|
| |
| def appendExt(path,ext):
| |
| if not path.endswith("." + ext) and not path.endswith(".dmx"):
| |
| path += "." + ext
| |
| return path
| |
|
| |
| # nodes block
| |
| def addBones(quiet=False):
| |
|
| |
| smd.file.write("nodes\n")
| |
|
| |
| if not smd.a:
| |
| smd.file.write("0 \"root\" -1\nend\n")
| |
| if not quiet: print("- No skeleton to export")
| |
| return
| |
|
| |
| top_id = -1
| |
| new_ids_needed = False
| |
|
| |
| # See if any bones need IDs; record highest ID
| |
| for bone in smd.a.data.bones:
| |
| try:
| |
| top_id = max(top_id,int(bone['smd_id']))
| |
| except KeyError:
| |
| new_ids_needed = True
| |
|
| |
| # Assign new IDs if needed
| |
| if new_ids_needed:
| |
| for bone in smd.a.data.bones:
| |
| try:
| |
| bone['smd_id']
| |
| except KeyError:
| |
| top_id += 1
| |
| bone['smd_id'] = top_id # re-using lower IDs risks collision
| |
|
| |
| # Write to file
| |
| for bone in smd.a.data.bones:
| |
| line = str(bone['smd_id']) + " "
| |
|
| |
| try:
| |
| bone_name = bone['smd_name']
| |
| except KeyError:
| |
| bone_name = bone.name
| |
| line += "\"" + bone_name + "\" "
| |
|
| |
| try:
| |
| line += str(bone.parent['smd_id'])
| |
| except TypeError:
| |
| line += "-1"
| |
|
| |
| smd.file.write(line + "\n")
| |
|
| |
| smd.file.write("end\n")
| |
| if not quiet: print("- Exported",len(smd.a.data.bones),"bones")
| |
| | |
| def matrixToEuler( matrix ):
| |
| euler = matrix.to_euler()
| |
| return vector( [-euler.x, euler.y, -euler.z] )
| |
|
| |
| def vector_by_matrix( m, p ):
| |
| return vector( [ p[0] * m[0][0] + p[1] * m[1][0] + p[2] * m[2][0],
| |
| p[0] * m[0][1] + p[1] * m[1][1] + p[2] * m[2][1],
| |
| p[0] * m[0][2] + p[1] * m[1][2] + p[2] * m[2][2]] )
| |
| | |
| # OpenGL (Blender) is left-handed, DirectX (Source) is right-handed
| |
| def matrix_reverse_handedness( matrix ):
| |
| axisX = vector( [ -matrix[0][1], -matrix[0][0], matrix[0][2], 0 ] )
| |
| axisX.normalize()
| |
| axisY = vector( [ -matrix[1][1], -matrix[1][0], matrix[1][2], 0 ] )
| |
| axisY.normalize()
| |
| axisZ = vector( [ -matrix[2][1], -matrix[2][0], matrix[2][2], 0 ] )
| |
| axisZ.normalize()
| |
| pos = vector( [ -matrix[3][1], -matrix[3][0], matrix[3][2], 1 ] )
| |
| return mathutils.Matrix( axisY, axisX, axisZ, pos )
| |
|
| |
| # skeleton block
| |
| def addFrames():
| |
|
| |
| if smd.jobType == 'FLEX': # addShapes() does its own skeleton block
| |
| return
| |
|
| |
| smd.file.write("skeleton\n")
| |
|
| |
| if not smd.a:
| |
| smd.file.write("time 0\n0 0 0 0 0 0 0\nend\n")
| |
| return
| |
|
| |
| #context.scene.objects.active = smd.a
| |
| #ops.object.mode_set(mode='EDIT')
| |
| smd.file.write("time 0\n")
| |
| for bone in smd.a.data.bones:
| |
| pos = rot = ""
| |
|
| |
| # bone_rot = vector( [math.atan2(bone.vector[2], math.sqrt( (bone.vector[0]*bone.vector[0]*) + (bone.vector[1]*bone.vector[1]) )), math.atan2(bone.vector[0],bone.vector[1]), bone.roll] )
| |
|
| |
|
| |
| # bone_rot[0] += 1.570796
| |
| # bone_rot[1] = -bone_rot[2] # y
| |
| # bone_rot[2] = bone.roll # z = bone roll
| |
|
| |
| if bone.parent:
| |
| bone_rot = matrixToEuler( bone.matrix * bone.parent.matrix )
| |
| bone_pos = (bone.head_local - bone.parent.head_local)
| |
| else:
| |
| bone_rot = matrixToEuler( bone.matrix )
| |
| bone_pos = bone.head_local
| |
|
| |
| bone_rot[0] += 1.570796
| |
|
| |
| for i in range(3):
| |
| pos += getSmdFloat( bone_pos[i] )
| |
| rot += "0" #getSmdFloat(bone_rot[i])
| |
|
| |
| if i != 2:
| |
| pos += " "
| |
| rot += " "
| |
|
| |
| smd.file.write( str(bone['smd_id']) + " " + pos + " " + rot + "\n")
| |
|
| |
| smd.file.write("end\n")
| |
| #ops.object.mode_set(mode='OBJECT')
| |
| return
| |
| | |
| # triangles block
| |
| def addPolys():
| |
| smd.file.write("triangles\n")
| |
|
| |
| context.scene.objects.active = smd.m
| |
| ops.object.mode_set(mode='EDIT')
| |
| bpy.ops.mesh.quads_convert_to_tris() # ops calls make baby jesus cry
| |
| ops.object.mode_set(mode='OBJECT')
| |
| | |
| md = smd.m.data
| |
| face_index = 0
| |
| for face in md.faces:
| |
| if smd.m.material_slots:
| |
| smd.file.write(smd.m.material_slots[face.material_index].name + "\n")
| |
| else:
| |
| smd.file.write(smd.jobName + "\n")
| |
| for i in range(3):
| |
|
| |
| # Vertex locations, normal directions
| |
| verts = norms = ""
| |
| v = md.verts[face.verts[i]]
| |
| for j in range(3):
| |
| verts += " " + getSmdFloat(v.co[j])
| |
| norms += " " + getSmdFloat(v.normal[j])
| |
|
| |
| # UVs
| |
| if len(md.uv_textures):
| |
| uv = ""
| |
| for j in range(2):
| |
| uv += " " + getSmdFloat(md.uv_textures[0].data[face_index].uv[i][j])
| |
| else:
| |
| if i == 0:
| |
| uv = " 0 0"
| |
| elif i == 1:
| |
| uv = " 0 1"
| |
| else:
| |
| uv = " 1 1"
| |
|
| |
| # Weightmaps
| |
| if len(v.groups):
| |
| groups = " " + str(len(v.groups))
| |
| for j in range(len(v.groups)):
| |
| # There is no certainty that a bone and its vertex group will share the same ID. Thus this monster:
| |
| groups += " " + str(smd.a.data.bones[smd.m.vertex_groups[v.groups[j].group].name]['smd_id']) + " " + getSmdFloat(v.groups[j].weight)
| |
| else:
| |
| groups = " 0"
| |
|
| |
| # Finally, write it all to file
| |
| smd.file.write("0" + verts + norms + uv + groups + "\n")
| |
|
| |
| face_index += 1
| |
|
| |
| smd.file.write("end\n")
| |
| print("- Exported",face_index,"polys")
| |
| return
| |
| | |
| # vertexanimation block
| |
| def addShapes():
| |
| | |
| # VTAs are always separate files. The nodes block is handled by the normal function, but skeleton is done here to afford a nice little hack
| |
| smd.file.write("skeleton\n")
| |
| for i in range(len(smd.m.data.shape_keys.keys)):
| |
| smd.file.write("time %i\n" % i)
| |
| smd.file.write("end\n")
| |
|
| |
| # OK, on to the meat!
| |
| smd.file.write("vertexanimation\n")
| |
| num_shapes = 0
| |
|
| |
| for shape_id in range(len(smd.m.data.shape_keys.keys)):
| |
| shape = smd.m.data.shape_keys.keys[shape_id]
| |
| smd.file.write("time %i\n" % shape_id)
| |
|
| |
| smd_vert_id = 0
| |
| for face in smd.m.data.faces:
| |
| for vert in face.verts:
| |
| shape_vert = shape.data[vert]
| |
| mesh_vert = smd.m.data.verts[vert]
| |
| cos = norms = ""
| |
|
| |
| if shape_id == 0 or (shape_id > 0 and shape_vert.co != mesh_vert.co):
| |
| for i in range(3):
| |
| cos += " " + getSmdFloat(shape_vert.co[i])
| |
| norms += " " + getSmdFloat(mesh_vert.normal[i]) # Blender's shape keys do not store normals
| |
| smd.file.write(str(smd_vert_id) + cos + norms + "\n")
| |
| smd_vert_id +=1
| |
| num_shapes += 1
| |
| smd.file.write("end\n")
| |
| print("- Exported",num_shapes,"vertex animations")
| |
| return
| |
| | |
| def printTimeMessage(start_time,name):
| |
| elapsedtime = int(time.time() - start_time)
| |
| if elapsedtime == 1:
| |
| elapsedtime = "1 second"
| |
| elif elapsedtime > 1:
| |
| elapsedtime = str(elapsedtime) + " seconds"
| |
| else:
| |
| elapsedtime = "under 1 second"
| |
|
| |
| print(name,"exported successfully in",elapsedtime,"\n")
| |
|
| |
| # Creates an SMD file
| |
| def writeSMD( context, filepath, smd_type = None, doVTA = True, quiet = False ):
| |
| if filepath.endswith("dmx"):
| |
| print("Skipping DMX file export: format unsupported (%s)" % getFilename(filepath))
| |
| return
| |
| | |
|
| |
| global smd
| |
| smd = smd_info()
| |
| smd.jobName = bpy.context.object.name
| |
| smd.jobType = smd_type
| |
| smd.startTime = time.time()
| |
| smd.uiTime = 0
| |
|
| |
| if bpy.context.object.type == 'MESH':
| |
| if not smd.jobType:
| |
| smd.jobType = 'REF'
| |
| smd.m = bpy.context.object
| |
| if smd.m.modifiers:
| |
| for i in range(len(smd.m.modifiers)):
| |
| if smd.m.modifiers[i].type == 'ARMATURE':
| |
| smd.a = smd.m.modifiers[i].object
| |
| elif bpy.context.object.type == 'ARMATURE':
| |
| if not smd.jobType:
| |
| smd.jobType = 'ANIM'
| |
| smd.a = bpy.context.object
| |
| else:
| |
| print("ERROR: invalid object selected!")
| |
| del smd
| |
| return
| |
|
| |
| smd.file = open(filepath, 'w')
| |
| if not quiet: print("\nSMD EXPORTER: now working on",smd.jobName)
| |
| smd.file.write("version 1\n")
| |
| | |
| addBones()
| |
| addFrames()
| |
|
| |
| if smd.m:
| |
| if smd.jobType in ['REF','PHYS']:
| |
| addPolys()
| |
|
| |
| if doVTA and smd.m.data.shape_keys:
| |
| # Start a new file
| |
| smd.file.close()
| |
| smd.file = open(filepath[0:filepath.rfind(".")] + ".vta", 'w')
| |
| smd.jobType = 'FLEX'
| |
|
| |
| addBones(quiet=True)
| |
| addShapes()
| |
| | |
| smd.file.close()
| |
| if not quiet: printTimeMessage(smd.startTime,smd.jobName)
| |
| del smd
| |
|
| |
| from bpy.props import *
| |
| | |
| class SmdExporter(bpy.types.Operator):
| |
| '''Export to the Source engine SMD/VTA format'''
| |
| bl_idname = "export_scene.smd"
| |
| bl_label = "Export SMD/VTA"
| |
|
| |
| filepath = StringProperty(name="File path", description="File filepath used for importing the SMD/VTA file", maxlen=1024, default="")
| |
| filename = StringProperty(name="Filename", description="Name of SMD/VTA file", maxlen=1024, default="")
| |
| doVTA = BoolProperty(name="Export VTA", description="Export a mesh's shape key", default=True)
| |
|
| |
| def execute(self, context):
| |
| prev_mode = None
| |
| if bpy.context.mode != "OBJECT":
| |
| prev_mode = bpy.context.mode
| |
| if prev_mode.startswith("EDIT"):
| |
| prev_mode = "EDIT" # remove any suffixes
| |
| ops.object.mode_set(mode='OBJECT')
| |
|
| |
| #if self.properties.filepath.endswith('.qc') | self.properties.filepath.endswith('.qci'):
| |
| # writeQC(context, self.properties.filepath, self.properties.freshScene, self.properties.doVTA, self.properties.doAnim )
| |
| if self.properties.filepath.endswith('.smd'):
| |
| writeSMD(context, self.properties.filepath, doVTA = self.properties.doVTA )
| |
| elif self.properties.filepath.endswith ('.vta'):
| |
| writeSMD(context, self.properties.filepath, 'FLEX', True, False)
| |
| elif self.properties.filepath.endswith('.dmx'):
| |
| print("DMX export not supported")
| |
| else:
| |
| print("File format not recognised")
| |
|
| |
| if prev_mode:
| |
| ops.object.mode_set(mode=prev_mode)
| |
|
| |
| return {'FINISHED'}
| |
|
| |
| def invoke(self, context, event):
| |
| if not bpy.context.object:
| |
| print( "SMD Export error: no object selected.")
| |
| return {'CANCELLED'}
| |
|
| |
| wm = context.manager
| |
| wm.add_fileselect(self)
| |
| return {'RUNNING_MODAL'}
| |
| | |
| def menu_func(self, context):
| |
| self.layout.operator(SmdExporter.bl_idname, text="Studiomdl Data (.smd, .vta)").filepath = os.path.splitext(bpy.data.filepath)[0] + ".smd"
| |
| | |
| def register():
| |
| bpy.types.register(SmdExporter)
| |
| bpy.types.INFO_MT_file_export.append(menu_func)
| |
| | |
| def unregister():
| |
| bpy.types.unregister(SmdExporter)
| |
| bpy.types.INFO_MT_file_export.remove(menu_func)
| |
| | |
| if __name__ == "__main__":
| |
| register()
| |
| </source>
| |
| | |
| [[Category:Blender]]
| |