Blender/SMD export: Difference between revisions

From Valve Developer Community
Jump to navigation Jump to search
(fixed bug when assigning SMD IDs to new bones)
(Redirected page to Blender Source Tools)
 
(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]]

Latest revision as of 06:47, 11 November 2013