Blender/SMD export

From Valve Developer Community
< Blender
Revision as of 23:31, 7 August 2010 by TomEdwards (talk | contribs) (fixed bug when assigning SMD IDs to new bones)
Jump to navigation Jump to search

Blender 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()