#!BPY # # Copyright (C) 2009, Doug Tyrrell ( dvondrake.com ) # Copyright (C) 2005, Pluss Roland ( roland@rptd.dnsalias.net ) # # 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., 675 Mass Ave, Cambridge, MA 02139, USA. # # Major credits to Pluss Roland for the original script. # All I did was reformat it, make it work, and recode a few things my way. # Most of the code in here is still 99% him. I take very little credit. """ Registration info for Blender menus: Name: 'Half-Life 2 (.smd)...' Blender: 235 Group: 'Export' Submenu: 'Static Mesh' static_sel Submenu: 'Physics Mesh' phys_sel Submenu: 'Animated Mesh' anim_sel Tip: 'Export to Half-Life 2 (.smd) format.' """ __author__ = "Doug Tyrrell/Pluss Roland" __version__ = "1.3 5/03/09" __bpydoc__ = """\ Exports models, UVs and animations from Blender to SMD files. """ import Blender from Blender.Mathutils import * import struct, os, cStringIO, time, re import math from math import * # constants PI = 3.14159265 HALF_PI = PI / 2.0 ONE_PI = PI / 180.0 # transformation matrices # hl2 coordinate system layout: # model faces along x axis ( is -y axis in blender ) # model right is along -y axis ( is -x axis in blender ) # model up is along z axis ( is z axis in blender ) tuv = 1.0 # Change this if you feel the need to. transformPosition = Matrix( [0,1,0,0], [-1,0,0,0], [0,0,1,0], [0,0,0,0] ) scalePosition = Matrix( [tuv,0,0,0], [0,tuv,0,0], [0,0,tuv,0], [0,0,0,1] ) transformBone = Matrix( [1,0,0,0], [0,-1,0,0], [0,0,1,0], [0,0,0,0] ) transformScalePosition = transformPosition * scalePosition transformScaleBone = transformBone * scalePosition class BoneType: def __init__( self, index, bone ): self.bone = bone self.index = index self.parent = None self.name = bone.name self.pos = Vector( [ 0, 0, 0 ] ) self.rot = Vector( [ 0, 0, 0 ] ) self.restMat = Matrix( [ 1, 0, 0, 0 ], [ 0, 1, 0, 0 ], [ 0, 0, 1, 0 ], [ 0, 0, 0, 1 ] ) class WeightType: def __init__( self, bone, weight ): self.bone = bone self.weight = weight def __repr__(self): return '[' + str( self.bone ) + '=' + str( self.weight ) + ']' def __str__(self): return self.__repr__() class VertexType: def __init__( self, index, vertex ): self.vertex = vertex self.edges = [] self.faces = [] self.index = index self.weights = [] def findEdge(self, edges, v2): for edge in self.edges: if edges[edge].vertices[1] == v2: return edge return -1 def getHardEdgeCount(self, edges): count = 0 for edge in self.edges: if edges[edge].hard: count = count + 1 return count class EdgeType: def __init__(self, v1, v2, index): self.vertices = [ v1, v2 ] self.faces = [ -1, -1 ] self.index = index self.hard = False class FaceType: def __init__(self, verts, index): self.vertices = [ verts[0].index, verts[1].index, verts[2].index ] self.normals = [ -1, -1, -1 ] self.edges = [ -1, -1, -1 ] self.index = index if len( verts ) == 4: self.vertices.append( verts[3].index ) self.normals.append( -1 ) self.edges.append( -1 ) def findCorner(self, vertex): for index in range( len(self.vertices) ): if self.vertices[index] == vertex: return index return -1 def setNormalFor(self, vertex, normal): for index in range( len(self.vertices) ): if self.vertices[index] == vertex: self.normals[index] = normal class BaseExport: def __init__( self ): self.armature = None self.mesh = None self.file = None self.object = None self.center = None self.bones = [] self.vertices = [] self.faces = [] self.edges = [] self.matrixMesh = Matrix() self.matrixArmature = Matrix() def addBone( self, bone, parent ): boneIndex = len( self.bones ) newBone = BoneType( boneIndex, bone ) newBone.parent = parent self.bones.append( newBone ) if bone.children: for child in bone.children: self.addBone( child, newBone ) def initBones( self ): print 'Initializing bones...' for bone in self.armature.bones.values(): if not bone.hasParent(): self.addBone( bone, None ) print '...Found %i bones.' % len( self.bones ) def addArmature( self, object ): if not self.armature: self.objArmature = object self.armature = self.objArmature.getData() print 'Found armature: %s' % self.objArmature.name self.matrixArmature = self.objArmature.getMatrix().rotationPart() self.matrixArmature.resize4x4() self.initBones() else: print 'Found duplicate armature: %s, ignoring...' % object.getData().name def switchAction( self, action ): if self.objArmature: action.setActive( self.objArmature ) def switchTime( self, time ): if self.armature: self.objArmature.evaluatePose( time ) def niceValue( self, value ): if fabs( value ) < 1e-6: return 0 else: return round( value, 6 ) def writeBones( self ): self.file.write( 'nodes\n' ) if len( self.bones ) > 0: #self.file.write( '0 "%s" -1\n' % ( self.armature.name ) ) for bone in self.bones: if bone.parent: self.file.write( '%i "%s" %i\n' % ( bone.index, bone.name, bone.parent.index ) ) else: self.file.write( '%i "%s" %i\n' % ( bone.index, bone.name, -1 ) ) else: self.file.write("0 \"joint0\" -1\n") self.file.write( 'end\n' ) self.file.write( 'skeleton\n' ) self.file.write( 'time 0\n' ) #self.file.write( '%i %f %f %f %f %f %f\n' % ( 0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 ) ) if len( self.bones ) > 0: for bone in self.bones: realBone = bone.bone bone.restMat = convertMatrix( realBone.matrix[ 'ARMATURESPACE' ] * self.matrixArmature ) if realBone.hasParent(): parentMat = convertMatrix( realBone.parent.matrix[ 'ARMATURESPACE' ] * self.matrixArmature ) parentMat.invert() bone.restMat = bone.restMat * parentMat bone.pos = vector_by_matrix( bone.restMat * transformScaleBone, Vector( [ 0, 0, 0 ] ) ) bone.rot = matrixToEuler( bone.restMat ) # write bone informations self.file.write( '%i %f %f %f %f %f %f\n' % ( bone.index, self.niceValue( bone.pos.x ), self.niceValue( bone.pos.y ), self.niceValue( bone.pos.z ), self.niceValue( bone.rot.x * ONE_PI ), self.niceValue( bone.rot.y * ONE_PI ), self.niceValue( bone.rot.z * ONE_PI ) ) ) else: self.file.write( '0 0 0 0 0 0 0\n' ) def writeNodes( self ): self.file.write( 'nodes\n' ) #self.file.write( '0 "%s" -1\n' % ( self.armature.name ) ) if len( self.bones ) > 0: for bone in self.bones: if bone.parent: self.file.write( '%i "%s" %i\n' % ( bone.index, bone.name, bone.parent.index ) ) else: self.file.write( '%i "%s" %i\n' % ( bone.index, bone.name, -1 ) ) else: self.file.write( '0 "%s" -1\n' % ( self.objMesh.data.name ) ) self.file.write( 'end\n' ) def addVertices( self ): if self.mesh: # add vertices for vert in self.mesh.verts: self.vertices.append( VertexType( vert.index, vert ) ) # add weights groups = self.mesh.getVertGroupNames() for group in groups: for bone in self.bones: if group == bone.bone.name: weights = self.mesh.getVertsFromGroup( group, 1 ) for weight in weights: if weight[ 1 ] > 0.001: self.vertices[ weight[ 0 ] ].weights.append( WeightType( bone, weight[ 1 ] ) ) break def addEdges( self ): if self.mesh: if not self.mesh.edges: self.mesh.addEdgesData() for index in range( len( self.mesh.edges ) ): edge = self.mesh.edges[ index ] newEdge = EdgeType( edge.v1.index, edge.v2.index, index ) newEdge.crease = edge.crease / 255 if edge.crease > 128: newEdge.hard = True self.edges.append( newEdge ) self.vertices[ edge.v1.index ].edges.append( index ) self.vertices[ edge.v2.index ].edges.append( index ) def addFaces( self ): if self.mesh: for index in range( len( self.mesh.faces ) ): face = self.mesh.faces[ index ] newFace = FaceType( face.v, index ) self.faces.append( newFace ) # gather vertices and indices vi = [ face.v[ 0 ].index, face.v[ 1 ].index, face.v[ 2 ].index ] vo = [ self.vertices[ vi[ 0 ] ], self.vertices[ vi[ 1 ] ], self.vertices[ vi[ 2 ] ] ] if len( face.v ) == 4: vi.append( face.v[ 3 ].index ) vo.append( self.vertices[ vi[ 3 ] ] ) # corners count = len( face.v ) for c in range( count ): self.initProcessFaceCorner( vi, vo, c, (c + 1) % count, index ) def initProcessFaceCorner( self, vi, vo, c1, c2, fi ): vo[c1].faces.append( fi ) edge = vo[c1].findEdge( self.edges, vi[c2] ) if edge == -1: edge = vo[c2].findEdge( self.edges, vi[c1] ) theEdge = self.edges[edge] if theEdge.faces[0] == -1: theEdge.faces[0] = fi elif theEdge.faces[1] == -1: theEdge.faces[1] = fi else: self.multiFoldMesh = True self.faces[fi].edges[c1] = edge def initCalcCornerNormals(self): self.normalCount = 0 for vert in self.vertices: hardEdgeCount = vert.getHardEdgeCount( self.edges ) if hardEdgeCount == 0: for face in vert.faces: self.faces[face].setNormalFor( vert.index, self.normalCount ) self.normalCount = self.normalCount + 1 else: changed = True while changed: changed = False for index in vert.faces: face = self.faces[index] c = face.findCorner( vert.index ) if face.normals[c] == -1: edge = self.edges[ face.edges[c] ] if edge.faces[0] == face.index: neighbor = edge.faces[1] else: neighbor = edge.faces[0] if edge.hard or neighbor == -1: face.normals[c] = self.normalCount self.normalCount = self.normalCount + 1 changed = True else: neighborFace = self.faces[ neighbor ] neighborCorner = neighborFace.findCorner( vert.index ) if neighborFace.normals[ neighborCorner ] != -1: face.normals[c] = neighborFace.normals[ neighborCorner ] changed = True class StaticExport ( BaseExport ): def __init__( self, phys ): BaseExport.__init__( self ) physics = '' if phys: physics = ' physics' print '\nExporting static%s mesh to SMD...' % physics self.DoPhys = phys def writeFaces( self ): print 'Writing Faces...' matrix = self.matrixObject * transformScalePosition for face in self.faces: # texture mat = "undefined.tga" if self.mesh.hasFaceUV(): material = self.mesh.materials[ self.mesh.faces[ face.index ].materialIndex ] mat = material.getName() # write triangle (for both cases) if len( face.vertices ) == 3: self.file.write( '%s\n' % ( mat ) ) self.writeIndexedFace( face, [ 0, 1, 2 ], matrix ) elif len( face.vertices ) == 4: self.file.write( '%s\n' % ( mat ) ) self.writeIndexedFace( face, [ 0, 1, 2 ], matrix ) self.file.write( '%s\n' % ( mat ) ) self.writeIndexedFace( face, [ 0, 2, 3 ], matrix ) print '...Wrote %i faces.' % len( self.faces ) # helper function to write one single face using the given indices def writeIndexedFace( self, face, indexList, matrix ): meshFace = self.mesh.faces[ face.index ] for t in indexList: vertex = self.vertices[ face.vertices[ t ] ] vertPos = vector_by_matrix( matrix, meshFace.v[ t ].co ) vertNormal = axis_by_matrix( matrix, meshFace.v[ t ].no ) vertU = 0 vertV = 0 if self.mesh.hasFaceUV(): vertU = meshFace.uv[ t ][ 0 ] vertV = meshFace.uv[ t ][ 1 ] # write to file self.file.write( '0 %f %f %f %f %f %f %f %f %i' % ( self.niceValue( vertPos.x ), self.niceValue( vertPos.y ), self.niceValue( vertPos.z ), self.niceValue( vertNormal.x ), self.niceValue( vertNormal.y ), self.niceValue( vertNormal.z ), self.niceValue( vertU ), self.niceValue( vertV ), len( vertex.weights ) ) ) if len( vertex.weights ) > 0: for weight in vertex.weights: self.file.write( ' %i %f' % ( weight.bone.index, self.niceValue( weight.weight ) ) ) else: self.file.write( ' 0 1' ) self.file.write( '\n' ) def writeChunk( self ): self.file.write("version 1\n") self.writeBones() self.file.write("end\n") self.file.write("triangles\n") self.writeFaces() def getExportFilename( self, filename ): if self.DoPhys: return filename[: -( len( filename.split( '.', -1 )[ -1 ] ) + 1 ) ] + '-phy' + '.smd' else: return filename[: -len( filename.split( '.', -1 )[ -1 ] ) ] + 'smd' def Export( self, filename ): try: self.file = open(filename, 'w') except IOError, (errno, strerror): errmsg = "IOError #%s" % errno errmsg = errmsg + "%t|" + strerror Blender.Draw.PupMenu(errmsg) return None objects = Blender.Object.GetSelected() for object in objects: if object.getType() == 'Armature': self.addArmature( object ) elif object.getType() == 'Mesh': #if not self.object: print 'Object: ' + object.name self.object = object self.mesh = object.getData() self.matrixObject = self.object.getMatrix().rotationPart() self.matrixObject.resize4x4() self.center = (0, 0, 0) if self.mesh.hasFaceUV(): print 'Mesh has UV.' self.addVertices() self.addEdges() self.addFaces() self.initCalcCornerNormals() if object.getParent() and object.getParent().getType() == 'Armature': self.addArmature( object ) #else: # print 'Found duplicate object: %s, ignoring...' % object.getData().name if not self.object: alertUser( 'Nothing selected to export!', 'Error!' ) return None self.writeChunk() if not self.armature: print 'No armature found.' self.file.write("end\n") self.file.close() print "Done. Saved to %s\n" % filename class AnimExport ( BaseExport ): def __init__( self ): BaseExport.__init__( self ) self.moves = [] print '\nExporting animation to SMD...' def searchForAct( self ): selfobject = None objects = Blender.Object.GetSelected() for object in objects: if object.getType() == 'Mesh': if not selfobject: selfobject = object if object.getParent() and object.getParent().getType() == 'Armature': return object.getParent().getAction().getName() elif object.getType() == 'Armature': return object.getAction().getName() def getExportFilename( self, filename ): actName = self.searchForAct() return filename[: -( len( filename.split( '.', -1 )[ -1 ] ) + 1 ) ] \ + '_' + actName.replace( '.', '_' ) \ + '.smd' def prepareBones( self ): print 'Preparing bones...' for bone in self.bones: realBone = bone.bone bone.restMat = convertMatrix( realBone.matrix[ 'ARMATURESPACE' ] ) if realBone.hasParent(): parentMat = convertMatrix( realBone.parent.matrix[ 'ARMATURESPACE' ] ) parentMat.invert() bone.restMat = bone.restMat * parentMat matrix = bone.restMat * self.matrixArmature * transformScaleBone bone.pos = vector_by_matrix( matrix, Vector( [ 0, 0, 0 ] ) ) bone.rot = matrixToEuler( bone.restMat ) # write animation def writeAnimation( self ): print 'Writing animations...' action = self.objArmature.getAction() # hack matrix matFixAnim = RotationMatrix( 90.0, 4, 'z' ) # retrieve the poser pose = self.objArmature.getPose() # process move playtime = 0.0 ipos = action.getAllChannelIpos() # determine playtime for boneobj in self.bones: bone = boneobj.bone if bone.name in ipos: boneIpo = ipos[bone.name] ipoCurve = boneIpo.getCurve( 'LocX' ) if ipoCurve: for knot in ipoCurve.bezierPoints: if knot.pt[ 0 ] > playtime: playtime = knot.pt[ 0 ] ipoCurve = boneIpo.getCurve( 'LocY' ) if ipoCurve: for knot in ipoCurve.bezierPoints: if knot.pt[ 0 ] > playtime: playtime = knot.pt[ 0 ] ipoCurve = boneIpo.getCurve( 'LocZ' ) if ipoCurve: for knot in ipoCurve.bezierPoints: if knot.pt[ 0 ] > playtime: playtime = knot.pt[ 0 ] ipoCurve = boneIpo.getCurve( 'QuatX' ) if ipoCurve: for knot in ipoCurve.bezierPoints: if knot.pt[ 0 ] > playtime: playtime = knot.pt[ 0 ] ipoCurve = boneIpo.getCurve( 'QuatY' ) if ipoCurve: for knot in ipoCurve.bezierPoints: if knot.pt[ 0 ] > playtime: playtime = knot.pt[ 0 ] ipoCurve = boneIpo.getCurve( 'QuatZ' ) if ipoCurve: for knot in ipoCurve.bezierPoints: if knot.pt[ 0 ] > playtime: playtime = knot.pt[ 0 ] ipoCurve = boneIpo.getCurve( 'QuatW' ) if ipoCurve: for knot in ipoCurve.bezierPoints: if knot.pt[ 0 ] > playtime: playtime = knot.pt[ 0 ] playtime = int( round( playtime, 0 ) ) if playtime < 1: playtime = 1 # write header self.file.write( 'version 1\n' ) # version one file... don't ask me what else could go here self.writeNodes() self.file.write( 'skeleton\n' ) # switch action self.switchAction( action ) # fetch keyframe values countI = 0 for time in range( 1, playtime + 1 ): countI = countI + 1 self.file.write( 'time %i\n' % ( time - 1 ) ) #self.file.write( '%i %f %f %f %f %f %f\n' % ( 0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 ) ) self.switchTime( time ) # write bones for bone in self.bones: realBone = bone.bone poseBone = pose.bones[ realBone.name ] # calculate bone matrix restMat = convertMatrix( poseBone.poseMatrix * self.matrixArmature ) * matFixAnim if realBone.hasParent(): poseParent = pose.bones[ bone.parent.bone.name ] #parentMat = bone.restMat * convertMatrix( poseParent.poseMatrix ) parentMat = convertMatrix( poseParent.poseMatrix * self.matrixArmature ) * matFixAnim parentMat.invert() restMat = restMat * parentMat pos = vector_by_matrix( restMat * transformScaleBone, Vector( [ 0, 0, 0 ] ) ) rot = matrixToEuler( restMat ) self.file.write( '%i %f %f %f %f %f %f\n' % ( bone.index, self.niceValue( pos.x ), self.niceValue( pos.y ), self.niceValue( pos.z ), self.niceValue( rot.x * ONE_PI ), self.niceValue( rot.y * ONE_PI ), self.niceValue( rot.z * ONE_PI ) ) ) # finsihed self.file.write( 'end\n' ) print '...Wrote %i frames.' % countI def Export( self, filename ): try: self.file = open(filename, 'w') except IOError, (errno, strerror): errmsg = "IOError #%s" % errno errmsg = errmsg + "%t|" + strerror Blender.Draw.PupMenu(errmsg) return None objects = Blender.Object.GetSelected() for object in objects: if object.getType() == 'Armature': self.addArmature( object ) elif object.getType() == 'Mesh': if not self.object: #print 'Object: ' + object.name self.object = object self.mesh = object.getData() self.center = (0, 0, 0) if object.getParent() and object.getParent().getType() == 'Armature': self.addArmature( object ) #else: # print 'Found duplicate object: %s, ignoring...' % object.getData().name if not self.object: alertUser( 'Nothing selected to export!', 'Error!' ) return None if not self.armature: alertUser( 'No armature found, what will be be animating?!', 'Error!' ) return None self.prepareBones() self.writeAnimation() self.file.close() print "Done. Saved to %s\n" % filename args = __script__['arg'] def convertMatrix( 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 Matrix( axisY, axisX, axisZ, pos ) def matrixToEuler( matrix ): euler = matrix.toEuler() 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] + m[3][0], p[0] * m[0][1] + p[1] * m[1][1] + p[2] * m[2][1] + m[3][1], p[0] * m[0][2] + p[1] * m[1][2] + p[2] * m[2][2] + m[3][2] ] ) def axis_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] ] ) def alertUser( message, title ): Blender.Draw.PupMenu( title + '%t|' + message ) print title + ': ' + message def file_callback( filename ): if not filename.endswith( '.smd' ): filename = filename + '.smd' exporter.Export( filename ) exporter = None if args == 'static_sel': exporter = StaticExport( 0 ) elif args == 'anim_sel': exporter = AnimExport() elif args == 'phys_sel': exporter = StaticExport( 1 ) if exporter: Blender.Window.FileSelector( file_callback, "Export SMD", exporter.getExportFilename( Blender.Get( 'filename' ) ) )