Blender/SMD export
< Blender
Jump to navigation
Jump to search
This is a script which exports SMD and VTA files from Blender 2.53 beta. Save it as a .py file in .blender\scripts\io\; see Blender for where to find that folder.
This script exports all bones with rotations of (0,0,0) and cannot handle animations. If you can fix it, please do so!
# ##### 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()