# ***** 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 3 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, see . # All rights reserved. # ***** GPL LICENSE BLOCK ***** bl_addon_info = { "name": "Export Skeleletal Mesh/Animation Data", "author": "Darknet/Optimus_P-Fat/Active_Trash/Sinsoft", "version": (2,0), "blender": (2, 5, 3), "api": 31847, "location": "File > Export > Skeletal Mesh/Animation Data (.psk/.psa)", "description": "Export Unreal Engine (.psk)", "warning": "", "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.5/Py/"\ "Scripts/File_I-O/Unreal_psk_psa", "tracker_url": "https://projects.blender.org/tracker/index.php?"\ "func=detail&aid=21366&group_id=153&atid=469", "category": "Import/Export"} """ -- Unreal Skeletal Mesh and Animation Export (.psk and .psa) export script v0.0.1 --
- NOTES: - This script Exports To Unreal's PSK and PSA file formats for Skeletal Meshes and Animations.
- This script DOES NOT support vertex animation! These require completely different file formats.
- v0.0.1 - Initial version - v0.0.2 - This version adds support for more than one material index! [ - Edit by: Darknet - v0.0.3 - v0.0.12 - This will work on UT3 and it is a stable version that work with vehicle for testing. - Main Bone fix no dummy needed to be there. - Just bone issues position, rotation, and offset for psk. - The armature bone position, rotation, and the offset of the bone is fix. It was to deal with skeleton mesh export for psk. - Animation is fix for position, offset, rotation bone support one rotation direction when armature build. - It will convert your mesh into triangular when exporting to psk file. - Did not work with psa export yet. - v0.0.13 - The animatoin will support different bone rotations when export the animation. - v0.0.14 - Fixed Action set keys frames when there is no pose keys and it will ignore it. - v0.0.15 - Fixed multiple objects when exporting to psk. Select one mesh to export to psk. - ] - v0.1.1 - Blender 2.50 svn (Support) Credit to: - export_cal3d.py (Position of the Bones Format) - blender2md5.py (Animation Translation Format) - export_obj.py (Blender 2.5/Pyhton 3.x Format) - freenode #blendercoder -> user -> ideasman42 - Give Credit to those who work on this script. - http://sinsoft.com """ import os import time import datetime import bpy import mathutils import operator from struct import pack, calcsize # REFERENCE MATERIAL JUST IN CASE: # # U = x / sqrt(x^2 + y^2 + z^2) # V = y / sqrt(x^2 + y^2 + z^2) # # Triangles specifed counter clockwise for front face # #defines for sizeofs SIZE_FQUAT = 16 SIZE_FVECTOR = 12 SIZE_VJOINTPOS = 44 SIZE_ANIMINFOBINARY = 168 SIZE_VCHUNKHEADER = 32 SIZE_VMATERIAL = 88 SIZE_VBONE = 120 SIZE_FNAMEDBONEBINARY = 120 SIZE_VRAWBONEINFLUENCE = 12 SIZE_VQUATANIMKEY = 32 SIZE_VVERTEX = 16 SIZE_VPOINT = 12 SIZE_VTRIANGLE = 12 ######################################################################## # Generic Object->Integer mapping # the object must be usable as a dictionary key class ObjMap: def __init__(self): self.dict = {} self.next = 0 def get(self, obj): if obj in self.dict: return self.dict[obj] else: id = self.next self.next = self.next + 1 self.dict[obj] = id return id def items(self): getval = operator.itemgetter(0) getkey = operator.itemgetter(1) return map(getval, sorted(self.dict.items(), key=getkey)) ######################################################################## # RG - UNREAL DATA STRUCTS - CONVERTED FROM C STRUCTS GIVEN ON UDN SITE # provided here: http://udn.epicgames.com/Two/BinaryFormatSpecifications.html # updated UDK (Unreal Engine 3): http://udn.epicgames.com/Three/BinaryFormatSpecifications.html class FQuat: def __init__(self): self.X = 0.0 self.Y = 0.0 self.Z = 0.0 self.W = 1.0 def dump(self): data = pack('ffff', self.X, self.Y, self.Z, self.W) return data def __cmp__(self, other): return cmp(self.X, other.X) \ or cmp(self.Y, other.Y) \ or cmp(self.Z, other.Z) \ or cmp(self.W, other.W) def __hash__(self): return hash(self.X) ^ hash(self.Y) ^ hash(self.Z) ^ hash(self.W) def __str__(self): return "[%f,%f,%f,%f](FQuat)" % (self.X, self.Y, self.Z, self.W) class FVector(object): def __init__(self, X=0.0, Y=0.0, Z=0.0): self.X = X self.Y = Y self.Z = Z def dump(self): data = pack('fff', self.X, self.Y, self.Z) return data def __cmp__(self, other): return cmp(self.X, other.X) \ or cmp(self.Y, other.Y) \ or cmp(self.Z, other.Z) def _key(self): return (type(self).__name__, self.X, self.Y, self.Z) def __hash__(self): return hash(self._key()) def __eq__(self, other): if not hasattr(other, '_key'): return False return self._key() == other._key() def dot(self, other): return self.X * other.X + self.Y * other.Y + self.Z * other.Z def cross(self, other): return FVector(self.Y * other.Z - self.Z * other.Y, self.Z * other.X - self.X * other.Z, self.X * other.Y - self.Y * other.X) def sub(self, other): return FVector(self.X - other.X, self.Y - other.Y, self.Z - other.Z) class VJointPos: def __init__(self): self.Orientation = FQuat() self.Position = FVector() self.Length = 0.0 self.XSize = 0.0 self.YSize = 0.0 self.ZSize = 0.0 def dump(self): data = self.Orientation.dump() + self.Position.dump() + pack('4f', self.Length, self.XSize, self.YSize, self.ZSize) return data class AnimInfoBinary: def __init__(self): self.Name = "" # length=64 self.Group = "" # length=64 self.TotalBones = 0 self.RootInclude = 0 self.KeyCompressionStyle = 0 self.KeyQuotum = 0 self.KeyPrediction = 0.0 self.TrackTime = 0.0 self.AnimRate = 0.0 self.StartBone = 0 self.FirstRawFrame = 0 self.NumRawFrames = 0 def dump(self): data = pack('64s64siiiifffiii', self.Name, self.Group, self.TotalBones, self.RootInclude, self.KeyCompressionStyle, self.KeyQuotum, self.KeyPrediction, self.TrackTime, self.AnimRate, self.StartBone, self.FirstRawFrame, self.NumRawFrames) return data class VChunkHeader: def __init__(self, name, type_size): self.ChunkID = name # length=20 self.TypeFlag = 1999801 # special value self.DataSize = type_size self.DataCount = 0 def dump(self): data = pack('20siii', self.ChunkID, self.TypeFlag, self.DataSize, self.DataCount) return data class VMaterial: def __init__(self): self.MaterialName = "" # length=64 self.TextureIndex = 0 self.PolyFlags = 0 # DWORD self.AuxMaterial = 0 self.AuxFlags = 0 # DWORD self.LodBias = 0 self.LodStyle = 0 def dump(self): data = pack('64siLiLii', self.MaterialName, self.TextureIndex, self.PolyFlags, self.AuxMaterial, self.AuxFlags, self.LodBias, self.LodStyle) return data class VBone: def __init__(self): self.Name = "" # length = 64 self.Flags = 0 # DWORD self.NumChildren = 0 self.ParentIndex = 0 self.BonePos = VJointPos() def dump(self): data = pack('64sLii', self.Name, self.Flags, self.NumChildren, self.ParentIndex) + self.BonePos.dump() return data #same as above - whatever - this is how Epic does it... class FNamedBoneBinary: def __init__(self): self.Name = "" # length = 64 self.Flags = 0 # DWORD self.NumChildren = 0 self.ParentIndex = 0 self.BonePos = VJointPos() self.IsRealBone = 0 # this is set to 1 when the bone is actually a bone in the mesh and not a dummy def dump(self): data = pack('64sLii', self.Name, self.Flags, self.NumChildren, self.ParentIndex) + self.BonePos.dump() return data class VRawBoneInfluence: def __init__(self): self.Weight = 0.0 self.PointIndex = 0 self.BoneIndex = 0 def dump(self): data = pack('fii', self.Weight, self.PointIndex, self.BoneIndex) return data class VQuatAnimKey: def __init__(self): self.Position = FVector() self.Orientation = FQuat() self.Time = 0.0 def dump(self): data = self.Position.dump() + self.Orientation.dump() + pack('f', self.Time) return data class VVertex(object): def __init__(self): self.PointIndex = 0 # WORD self.U = 0.0 self.V = 0.0 self.MatIndex = 0 #BYTE self.Reserved = 0 #BYTE def dump(self): data = pack('HHffBBH', self.PointIndex, 0, self.U, self.V, self.MatIndex, self.Reserved, 0) return data def __cmp__(self, other): return cmp(self.PointIndex, other.PointIndex) \ or cmp(self.U, other.U) \ or cmp(self.V, other.V) \ or cmp(self.MatIndex, other.MatIndex) \ or cmp(self.Reserved, other.Reserved) def _key(self): return (type(self).__name__,self.PointIndex, self.U, self.V,self.MatIndex,self.Reserved) def __hash__(self): return hash(self._key()) def __eq__(self, other): if not hasattr(other, '_key'): return False return self._key() == other._key() class VPoint(object): def __init__(self): self.Point = FVector() def dump(self): return self.Point.dump() def __cmp__(self, other): return cmp(self.Point, other.Point) def _key(self): return (type(self).__name__, self.Point) def __hash__(self): return hash(self._key()) def __eq__(self, other): if not hasattr(other, '_key'): return False return self._key() == other._key() class VTriangle: def __init__(self): self.WedgeIndex0 = 0 # WORD self.WedgeIndex1 = 0 # WORD self.WedgeIndex2 = 0 # WORD self.MatIndex = 0 # BYTE self.AuxMatIndex = 0 # BYTE self.SmoothingGroups = 0 # DWORD def dump(self): data = pack('HHHBBL', self.WedgeIndex0, self.WedgeIndex1, self.WedgeIndex2, self.MatIndex, self.AuxMatIndex, self.SmoothingGroups) return data # END UNREAL DATA STRUCTS ######################################################################## ######################################################################## #RG - helper class to handle the normal way the UT files are stored #as sections consisting of a header and then a list of data structures class FileSection: def __init__(self, name, type_size): self.Header = VChunkHeader(name, type_size) self.Data = [] # list of datatypes def dump(self): data = self.Header.dump() for i in range(len(self.Data)): data = data + self.Data[i].dump() return data def UpdateHeader(self): self.Header.DataCount = len(self.Data) class PSKFile: def __init__(self): self.GeneralHeader = VChunkHeader("ACTRHEAD", 0) self.Points = FileSection("PNTS0000", SIZE_VPOINT) #VPoint self.Wedges = FileSection("VTXW0000", SIZE_VVERTEX) #VVertex self.Faces = FileSection("FACE0000", SIZE_VTRIANGLE) #VTriangle self.Materials = FileSection("MATT0000", SIZE_VMATERIAL) #VMaterial self.Bones = FileSection("REFSKELT", SIZE_VBONE) #VBone self.Influences = FileSection("RAWWEIGHTS", SIZE_VRAWBONEINFLUENCE) #VRawBoneInfluence #RG - this mapping is not dumped, but is used internally to store the new point indices # for vertex groups calculated during the mesh dump, so they can be used again # to dump bone influences during the armature dump # # the key in this dictionary is the VertexGroup/Bone Name, and the value # is a list of tuples containing the new point index and the weight, in that order # # Layout: # { groupname : [ (index, weight), ... ], ... } # # example: # { 'MyVertexGroup' : [ (0, 1.0), (5, 1.0), (3, 0.5) ] , 'OtherGroup' : [(2, 1.0)] } self.VertexGroups = {} def AddPoint(self, p): #print ('AddPoint') self.Points.Data.append(p) def AddWedge(self, w): #print ('AddWedge') self.Wedges.Data.append(w) def AddFace(self, f): #print ('AddFace') self.Faces.Data.append(f) def AddMaterial(self, m): #print ('AddMaterial') self.Materials.Data.append(m) def AddBone(self, b): #print ('AddBone [%s]: Position: (x=%f, y=%f, z=%f) Rotation=(%f,%f,%f,%f)' % (b.Name, b.BonePos.Position.X, b.BonePos.Position.Y, b.BonePos.Position.Z, b.BonePos.Orientation.X,b.BonePos.Orientation.Y,b.BonePos.Orientation.Z,b.BonePos.Orientation.W)) self.Bones.Data.append(b) def AddInfluence(self, i): #print ('AddInfluence') self.Influences.Data.append(i) def UpdateHeaders(self): self.Points.UpdateHeader() self.Wedges.UpdateHeader() self.Faces.UpdateHeader() self.Materials.UpdateHeader() self.Bones.UpdateHeader() self.Influences.UpdateHeader() def dump(self): self.UpdateHeaders() data = self.GeneralHeader.dump() + self.Points.dump() + self.Wedges.dump() + self.Faces.dump() + self.Materials.dump() + self.Bones.dump() + self.Influences.dump() return data def GetMatByIndex(self, mat_index): if mat_index >= 0 and len(self.Materials.Data) > mat_index: return self.Materials.Data[mat_index] else: m = VMaterial() m.MaterialName = "Mat%i" % mat_index self.AddMaterial(m) return m def PrintOut(self): print ("--- PSK FILE EXPORTED ---") print ('point count: %i' % len(self.Points.Data)) print ('wedge count: %i' % len(self.Wedges.Data)) print ('face count: %i' % len(self.Faces.Data)) print ('material count: %i' % len(self.Materials.Data)) print ('bone count: %i' % len(self.Bones.Data)) print ('inlfuence count: %i' % len(self.Influences.Data)) print ('-------------------------') # PSA FILE NOTES FROM UDN: # # The raw key array holds all the keys for all the bones in all the specified sequences, # organized as follows: # For each AnimInfoBinary's sequence there are [Number of bones] times [Number of frames keys] # in the VQuatAnimKeys, laid out as tracks of [numframes] keys for each bone in the order of # the bones as defined in the array of FnamedBoneBinary in the PSA. # # Once the data from the PSK (now digested into native skeletal mesh) and PSA (digested into # a native animation object containing one or more sequences) are associated together at runtime, # bones are linked up by name. Any bone in a skeleton (from the PSK) that finds no partner in # the animation sequence (from the PSA) will assume its reference pose stance ( as defined in # the offsets & rotations that are in the VBones making up the reference skeleton from the PSK) class PSAFile: def __init__(self): self.GeneralHeader = VChunkHeader("ANIMHEAD", 0) self.Bones = FileSection("BONENAMES", SIZE_FNAMEDBONEBINARY) #FNamedBoneBinary self.Animations = FileSection("ANIMINFO", SIZE_ANIMINFOBINARY) #AnimInfoBinary self.RawKeys = FileSection("ANIMKEYS", SIZE_VQUATANIMKEY) #VQuatAnimKey # this will take the format of key=Bone Name, value = (BoneIndex, Bone Object) # THIS IS NOT DUMPED self.BoneLookup = {} def dump(self): data = self.Generalheader.dump() + self.Bones.dump() + self.Animations.dump() + self.RawKeys.dump() return data def AddBone(self, b): #LOUD #print "AddBone: " + b.Name self.Bones.Data.append(b) def AddAnimation(self, a): #LOUD #print "AddAnimation: %s, TotalBones: %i, AnimRate: %f, NumRawFrames: %i, TrackTime: %f" % (a.Name, a.TotalBones, a.AnimRate, a.NumRawFrames, a.TrackTime) self.Animations.Data.append(a) def AddRawKey(self, k): #LOUD #print "AddRawKey [%i]: Time: %f, Quat: x=%f, y=%f, z=%f, w=%f, Position: x=%f, y=%f, z=%f" % (len(self.RawKeys.Data), k.Time, k.Orientation.X, k.Orientation.Y, k.Orientation.Z, k.Orientation.W, k.Position.X, k.Position.Y, k.Position.Z) self.RawKeys.Data.append(k) def UpdateHeaders(self): self.Bones.UpdateHeader() self.Animations.UpdateHeader() self.RawKeys.UpdateHeader() def GetBoneByIndex(self, bone_index): if bone_index >= 0 and len(self.Bones.Data) > bone_index: return self.Bones.Data[bone_index] def IsEmpty(self): return (len(self.Bones.Data) == 0 or len(self.Animations.Data) == 0) def StoreBone(self, b): self.BoneLookup[b.Name] = [-1, b] def UseBone(self, bone_name): if bone_name in self.BoneLookup: bone_data = self.BoneLookup[bone_name] if bone_data[0] == -1: bone_data[0] = len(self.Bones.Data) self.AddBone(bone_data[1]) #self.Bones.Data.append(bone_data[1]) return bone_data[0] def GetBoneByName(self, bone_name): if bone_name in self.BoneLookup: bone_data = self.BoneLookup[bone_name] return bone_data[1] def GetBoneIndex(self, bone_name): if bone_name in self.BoneLookup: bone_data = self.BoneLookup[bone_name] return bone_data[0] def dump(self): self.UpdateHeaders() data = self.GeneralHeader.dump() + self.Bones.dump() + self.Animations.dump() + self.RawKeys.dump() return data def PrintOut(self): print ('--- PSA FILE EXPORTED ---') print ('bone count: %i' % len(self.Bones.Data)) print ('animation count: %i' % len(self.Animations.Data)) print ('rawkey count: %i' % len(self.RawKeys.Data)) print ('-------------------------') #################################### # helpers to create bone structs def make_vbone(name, parent_index, child_count, orientation_quat, position_vect): bone = VBone() bone.Name = name bone.ParentIndex = parent_index bone.NumChildren = child_count bone.BonePos.Orientation = orientation_quat bone.BonePos.Position.X = position_vect.x bone.BonePos.Position.Y = position_vect.y bone.BonePos.Position.Z = position_vect.z #these values seem to be ignored? #bone.BonePos.Length = tail.length #bone.BonePos.XSize = tail.x #bone.BonePos.YSize = tail.y #bone.BonePos.ZSize = tail.z return bone def make_namedbonebinary(name, parent_index, child_count, orientation_quat, position_vect, is_real): bone = FNamedBoneBinary() bone.Name = name bone.ParentIndex = parent_index bone.NumChildren = child_count bone.BonePos.Orientation = orientation_quat bone.BonePos.Position.X = position_vect.x bone.BonePos.Position.Y = position_vect.y bone.BonePos.Position.Z = position_vect.z bone.IsRealBone = is_real return bone ################################################## #RG - check to make sure face isnt a line #The face has to be triangle not a line def is_1d_face(blender_face,mesh): #ID Vertex of id point v0 = blender_face.vertices[0] v1 = blender_face.vertices[1] v2 = blender_face.vertices[2] return (mesh.vertices[v0].co == mesh.vertices[v1].co or \ mesh.vertices[v1].co == mesh.vertices[v2].co or \ mesh.vertices[v2].co == mesh.vertices[v0].co) return False ################################################## # http://en.wikibooks.org/wiki/Blender_3D:_Blending_Into_Python/Cookbook#Triangulate_NMesh #blender 2.50 format using the Operators/command convert the mesh to tri mesh def triangulateNMesh(object): bneedtri = False scene = bpy.context.scene bpy.ops.object.mode_set(mode='OBJECT') for i in scene.objects: i.select = False #deselect all objects object.select = True scene.objects.active = object #set the mesh object to current bpy.ops.object.mode_set(mode='OBJECT') print("Checking mesh if needs to convert quad to Tri...") for face in object.data.faces: if (len(face.vertices) > 3): bneedtri = True break bpy.ops.object.mode_set(mode='OBJECT') if bneedtri == True: print("Converting quad to tri mesh...") me_da = object.data.copy() #copy data me_ob = object.copy() #copy object #note two copy two types else it will use the current data or mesh me_ob.data = me_da bpy.context.scene.objects.link(me_ob)#link the object to the scene #current object location for i in scene.objects: i.select = False #deselect all objects me_ob.select = True scene.objects.active = me_ob #set the mesh object to current bpy.ops.object.mode_set(mode='EDIT') #Operators bpy.ops.mesh.select_all(action='SELECT')#select all the face/vertex/edge bpy.ops.mesh.quads_convert_to_tris() #Operators bpy.context.scene.update() bpy.ops.object.mode_set(mode='OBJECT') # set it in object bpy.context.scene.unrealtriangulatebool = True print("Triangulate Mesh Done!") else: print("No need to convert tri mesh.") me_ob = object return me_ob #Blender Bone Index class BBone: def __init__(self): self.bone = "" self.index = 0 bonedata = [] BBCount = 0 #deal with mesh bones groups vertex point def BoneIndex(bone): global BBCount, bonedata #print("//==============") #print(bone.name , "ID:",BBCount) BB = BBone() BB.bone = bone.name BB.index = BBCount bonedata.append(BB) BBCount += 1 for current_child_bone in bone.children: BoneIndex(current_child_bone) def BoneIndexArmature(blender_armature): global BBCount #print("\n Buildng bone before mesh \n") #objectbone = blender_armature.pose #Armature bone #print(blender_armature) objectbone = blender_armature[0].pose #print(dir(ArmatureData)) for bone in objectbone.bones: if(bone.parent is None): BoneIndex(bone) #BBCount += 1 break # Actual object parsing functions def parse_meshes(blender_meshes, psk_file): #this is use to call the bone name and the index array for group index matches global bonedata #print("BONE DATA",len(bonedata)) print ("----- parsing meshes -----") print("Number of Object Meshes:",len(blender_meshes)) for current_obj in blender_meshes: #number of mesh that should be one mesh here current_obj = triangulateNMesh(current_obj) #print(dir(current_obj)) print("Mesh Name:",current_obj.name) current_mesh = current_obj.data #if len(current_obj.materials) > 0: # object_mat = current_obj.materials[0] object_material_index = current_obj.active_material_index points = ObjMap() wedges = ObjMap() discarded_face_count = 0 print (" -- Dumping Mesh Faces -- LEN:", len(current_mesh.faces)) for current_face in current_mesh.faces: #print ' -- Dumping UVs -- ' #print current_face.uv_textures if len(current_face.vertices) != 3: raise RuntimeError("Non-triangular face (%i)" % len(current_face.vertices)) #No Triangulate Yet # if len(current_face.vertices) != 3: # raise RuntimeError("Non-triangular face (%i)" % len(current_face.vertices)) # #TODO: add two fake faces made of triangles? #RG - apparently blender sometimes has problems when you do quad to triangle # conversion, and ends up creating faces that have only TWO points - # one of the points is simply in the vertex list for the face twice. # This is bad, since we can't get a real face normal for a LINE, we need # a plane for this. So, before we add the face to the list of real faces, # ensure that the face is actually a plane, and not a line. If it is not # planar, just discard it and notify the user in the console after we're # done dumping the rest of the faces if not is_1d_face(current_face,current_mesh): #print("faces") wedge_list = [] vect_list = [] #get or create the current material m = psk_file.GetMatByIndex(object_material_index) face_index = current_face.index has_UV = False faceUV = None if len(current_mesh.uv_textures) > 0: has_UV = True #print("face index: ",face_index) #faceUV = current_mesh.uv_textures.active.data[face_index]#UVs for current face #faceUV = current_mesh.uv_textures.active.data[0]#UVs for current face #print(face_index,"<[FACE NUMBER") uv_layer = current_mesh.uv_textures.active faceUV = uv_layer.data[face_index] #print("============================") #size(data) is number of texture faces. Each face has UVs #print("DATA face uv: ",len(faceUV.uv), " >> ",(faceUV.uv[0][0])) for i in range(3): vert_index = current_face.vertices[i] vert = current_mesh.vertices[vert_index] uv = [] #assumes 3 UVs Per face (for now). if (has_UV): if len(faceUV.uv) != 3: print ("WARNING: Current face is missing UV coordinates - writing 0,0...") print ("WARNING: Face has more than 3 UVs - writing 0,0...") uv = [0.0, 0.0] else: #uv.append(faceUV.uv[i][0]) #uv.append(faceUV.uv[i][1]) uv = [faceUV.uv[i][0],faceUV.uv[i][1]] #OR bottom works better # 24 for cube #uv = list(faceUV.uv[i]) #30 just cube else: print ("No UVs?") uv = [0.0, 0.0] #print("UV >",uv) #uv = [0.0, 0.0] #over ride uv that is not fixed #print(uv) #flip V coordinate because UEd requires it and DOESN'T flip it on its own like it #does with the mesh Y coordinates. #this is otherwise known as MAGIC-2 uv[1] = 1.0 - uv[1] #deal with the min and max value #if value is over the set limit it will null the uv texture if (uv[0] > 1): uv[0] = 1 if (uv[0] < 0): uv[0] = 0 if (uv[1] > 1): uv[1] = 1 if (uv[1] < 0): uv[1] = 0 # RE - Append untransformed vector (for normal calc below) # TODO: convert to Blender.Mathutils vect_list.append(FVector(vert.co.x, vert.co.y, vert.co.z)) # Transform position for export #vpos = vert.co * object_material_index vpos = vert.co * current_obj.matrix_local # Create the point p = VPoint() p.Point.X = vpos.x p.Point.Y = vpos.y p.Point.Z = vpos.z # Create the wedge w = VVertex() w.MatIndex = object_material_index w.PointIndex = points.get(p) # get index from map #Set UV TEXTURE w.U = uv[0] w.V = uv[1] index_wedge = wedges.get(w) wedge_list.append(index_wedge) #print results #print 'result PointIndex=%i, U=%f, V=%f, wedge_index=%i' % ( # w.PointIndex, # w.U, # w.V, # wedge_index) # Determine face vertex order # get normal from blender no = current_face.normal # TODO: convert to Blender.Mathutils # convert to FVector norm = FVector(no[0], no[1], no[2]) # Calculate the normal of the face in blender order tnorm = vect_list[1].sub(vect_list[0]).cross(vect_list[2].sub(vect_list[1])) # RE - dot the normal from blender order against the blender normal # this gives the product of the two vectors' lengths along the blender normal axis # all that matters is the sign dot = norm.dot(tnorm) # print results #print 'face norm: (%f,%f,%f), tnorm=(%f,%f,%f), dot=%f' % ( # norm.X, norm.Y, norm.Z, # tnorm.X, tnorm.Y, tnorm.Z, # dot) tri = VTriangle() # RE - magic: if the dot product above > 0, order the vertices 2, 1, 0 # if the dot product above < 0, order the vertices 0, 1, 2 # if the dot product is 0, then blender's normal is coplanar with the face # and we cannot deduce which side of the face is the outside of the mesh if (dot > 0): (tri.WedgeIndex2, tri.WedgeIndex1, tri.WedgeIndex0) = wedge_list elif (dot < 0): (tri.WedgeIndex0, tri.WedgeIndex1, tri.WedgeIndex2) = wedge_list else: dindex0 = current_face.vertices[0]; dindex1 = current_face.vertices[1]; dindex2 = current_face.vertices[2]; raise RuntimeError("normal vector coplanar with face! points:", current_mesh.vertices[dindex0].co, current_mesh.vertices[dindex1].co, current_mesh.vertices[dindex2].co) tri.MatIndex = object_material_index #print(tri) psk_file.AddFace(tri) else: discarded_face_count = discarded_face_count + 1 print (" -- Dumping Mesh Points -- LEN:",len(points.dict)) for point in points.items(): psk_file.AddPoint(point) print (" -- Dumping Mesh Wedge -- LEN:",len(wedges.dict)) for wedge in wedges.items(): psk_file.AddWedge(wedge) #RG - if we happend upon any non-planar faces above that we've discarded, # just let the user know we discarded them here in case they want # to investigate if discarded_face_count > 0: print ("INFO: Discarded %i non-planar faces." % (discarded_face_count)) #RG - walk through the vertex groups and find the indexes into the PSK points array #for them, then store that index and the weight as a tuple in a new list of #verts for the group that we can look up later by bone name, since Blender matches #verts to bones for influences by having the VertexGroup named the same thing as #the bone #vertex group. for bonegroup in bonedata: #print("bone gourp build:",bonegroup.bone) vert_list = [] for current_vert in current_mesh.vertices: #print("INDEX V:",current_vert.index) vert_index = current_vert.index for vgroup in current_vert.groups:#vertex groupd id vert_weight = vgroup.weight if(bonegroup.index == vgroup.group): p = VPoint() vpos = current_vert.co * current_obj.matrix_local p.Point.X = vpos.x p.Point.Y = vpos.y p.Point.Z = vpos.z #print(current_vert.co) point_index = points.get(p) #point index v_item = (point_index, vert_weight) vert_list.append(v_item) #bone name, [point id and wieght] #print("Add Vertex Group:",bonegroup.bone, " No. Points:",len(vert_list)) psk_file.VertexGroups[bonegroup.bone] = vert_list #unrealtriangulatebool #this will remove the mesh from the scene if (bpy.context.scene.unrealtriangulatebool == True): print("Remove tmp Mesh [ " ,current_obj.name, " ] from scene >" ,(bpy.context.scene.unrealtriangulatebool )) bpy.ops.object.mode_set(mode='OBJECT') # set it in object bpy.context.scene.objects.unlink(current_obj) def make_fquat(bquat): quat = FQuat() #flip handedness for UT = set x,y,z to negative (rotate in other direction) quat.X = -bquat.x quat.Y = -bquat.y quat.Z = -bquat.z quat.W = bquat.w return quat def make_fquat_default(bquat): quat = FQuat() quat.X = bquat.x quat.Y = bquat.y quat.Z = bquat.z quat.W = bquat.w return quat # ================================================================================================= # TODO: remove this 1am hack nbone = 0 def parse_bone(blender_bone, psk_file, psa_file, parent_id, is_root_bone, parent_matrix, parent_root): global nbone # look it's evil! #print '-------------------- Dumping Bone ---------------------- ' #If bone does not have parent that mean it the root bone if blender_bone.parent is None: parent_root = blender_bone child_count = len(blender_bone.children) #child of parent child_parent = blender_bone.parent if child_parent != None: print ("--Bone Name:",blender_bone.name ," parent:" , blender_bone.parent.name, "ID:", nbone) else: print ("--Bone Name:",blender_bone.name ," parent: None" , "ID:", nbone) if child_parent != None: quat_root = blender_bone.matrix quat = make_fquat(quat_root.to_quat()) quat_parent = child_parent.matrix.to_quat().inverse() parent_head = child_parent.head * quat_parent parent_tail = child_parent.tail * quat_parent set_position = (parent_tail - parent_head) + blender_bone.head else: # ROOT BONE #This for root set_position = blender_bone.head * parent_matrix #ARMATURE OBJECT Locction rot_mat = blender_bone.matrix * parent_matrix.rotation_part() #ARMATURE OBJECT Rotation #print(dir(rot_mat)) quat = make_fquat_default(rot_mat.to_quat()) print ("[[======= FINAL POSITION:", set_position) final_parent_id = parent_id #RG/RE - #if we are not seperated by a small distance, create a dummy bone for the displacement #this is only needed for root bones, since UT assumes a connected skeleton, and from here #down the chain we just use "tail" as an endpoint #if(head.length > 0.001 and is_root_bone == 1): if(0): pb = make_vbone("dummy_" + blender_bone.name, parent_id, 1, FQuat(), tail) psk_file.AddBone(pb) pbb = make_namedbonebinary("dummy_" + blender_bone.name, parent_id, 1, FQuat(), tail, 0) psa_file.StoreBone(pbb) final_parent_id = nbone nbone = nbone + 1 #tail = tail-head my_id = nbone pb = make_vbone(blender_bone.name, final_parent_id, child_count, quat, set_position) psk_file.AddBone(pb) pbb = make_namedbonebinary(blender_bone.name, final_parent_id, child_count, quat, set_position, 1) psa_file.StoreBone(pbb) nbone = nbone + 1 #RG - dump influences for this bone - use the data we collected in the mesh dump phase # to map our bones to vertex groups #print("///////////////////////") #print("set influence") if blender_bone.name in psk_file.VertexGroups: vertex_list = psk_file.VertexGroups[blender_bone.name] #print("vertex list:", len(vertex_list), " of >" ,blender_bone.name ) for vertex_data in vertex_list: #print("set influence vettex") point_index = vertex_data[0] vertex_weight = vertex_data[1] influence = VRawBoneInfluence() influence.Weight = vertex_weight #influence.BoneIndex = my_id influence.BoneIndex = my_id influence.PointIndex = point_index #print(influence) #print ('Adding Bone Influence for [%s] = Point Index=%i, Weight=%f' % (blender_bone.name, point_index, vertex_weight)) #print("adding influence") psk_file.AddInfluence(influence) #blender_bone.matrix_local #recursively dump child bones mainparent = parent_matrix #if len(blender_bone.children) > 0: for current_child_bone in blender_bone.children: parse_bone(current_child_bone, psk_file, psa_file, my_id, 0, mainparent, parent_root) def parse_armature(blender_armature, psk_file, psa_file): print ("----- parsing armature -----") print ('blender_armature length: %i' % (len(blender_armature))) #magic 0 sized root bone for UT - this is where all armature dummy bones will attach #dont increment nbone here because we initialize it to 1 (hackity hackity hack) #count top level bones first. NOT EFFICIENT. child_count = 0 for current_obj in blender_armature: current_armature = current_obj.data bones = [x for x in current_armature.bones if not x.parent is None] child_count += len(bones) for current_obj in blender_armature: print ("Current Armature Name: " + current_obj.name) current_armature = current_obj.data #armature_id = make_armature_bone(current_obj, psk_file, psa_file) #we dont want children here - only the top level bones of the armature itself #we will recursively dump the child bones as we dump these bones """ bones = [x for x in current_armature.bones if not x.parent is None] #will ingore this part of the ocde """ for current_bone in current_armature.bones: #list the bone. #note this will list all the bones. if(current_bone.parent is None): parse_bone(current_bone, psk_file, psa_file, 0, 0, current_obj.matrix_local, None) break # get blender objects by type def get_blender_objects(objects, intype): return [x for x in objects if x.type == intype] #strips current extension (if any) from filename and replaces it with extension passed in def make_filename_ext(filename, extension): new_filename = '' extension_index = filename.find('.') if extension_index == -1: new_filename = filename + extension else: new_filename = filename[0:extension_index] + extension return new_filename # returns the quaternion Grassman product a*b # this is the same as the rotation a(b(x)) # (ie. the same as B*A if A and B are matrices representing # the rotations described by quaternions a and b) def grassman(a, b): return mathutils.Quaternion( a.w*b.w - a.x*b.x - a.y*b.y - a.z*b.z, a.w*b.x + a.x*b.w + a.y*b.z - a.z*b.y, a.w*b.y - a.x*b.z + a.y*b.w + a.z*b.x, a.w*b.z + a.x*b.y - a.y*b.x + a.z*b.w) def parse_animation(blender_scene, blender_armatures, psa_file): #to do list: #need to list the action sets #need to check if there animation #need to check if animation is has one frame then exit it print ('\n----- parsing animation -----') ##print(dir(blender_scene)) #print(dir(blender_armatures)) render_data = blender_scene.render bHaveAction = True anim_rate = render_data.fps #print("dir:",dir(blender_scene)) #print(dir(bpy.data.actions)) #print("dir:",dir(bpy.data.actions[0])) print("==== Blender Settings ====") print ('Scene: %s Start Frame: %i, End Frame: %i' % (blender_scene.name, blender_scene.frame_start, blender_scene.frame_end)) print ('Frames Per Sec: %i' % anim_rate) print ("Default FPS: 24" ) cur_frame_index = 0 #print(dir(bpy.data.actions)) #print(dir(bpy.context.scene.background_set)) #list of armature objects for arm in blender_armatures: #check if there animation data from armature or something #print(dir(arm.animation_data)) #print("[["+dir(arm.animation_data.action)) if not arm.animation_data: print("======================================") print("Check Animation Data: None") print("Armature has no animation, skipping...") print("======================================") break if not arm.animation_data.action: print("======================================") print("Check Action: None") print("Armature has no animation, skipping...") print("======================================") break act = arm.animation_data.action #print(dir(act)) action_name = act.name if not len(act.fcurves): print("//===========================================================") print("// None bone pose set keys for this action set... skipping...") print("//===========================================================") bHaveAction = False #this deal with action export control if bHaveAction == True: print("") print("==== Action Set ====") print("Action Name:",action_name) #look for min and max frame that current set keys framemin, framemax = act.frame_range #print("max frame:",framemax) start_frame = int(framemin) end_frame = int(framemax) scene_frames = range(start_frame, end_frame+1) frame_count = len(scene_frames) #=================================================== anim = AnimInfoBinary() anim.Name = action_name anim.Group = "" #what is group? anim.NumRawFrames = frame_count anim.AnimRate = anim_rate anim.FirstRawFrame = cur_frame_index #=================================================== count_previous_keys = len(psa_file.RawKeys.Data) print("Frame Key Set Count:",frame_count, "Total Frame:",frame_count) #print("init action bones...") unique_bone_indexes = {} # bone lookup table bones_lookup = {} #build bone node for animation keys needed to be set for bone in arm.data.bones: bones_lookup[bone.name] = bone #print("bone name:",bone.name) frame_count = len(scene_frames) #print ('Frame Count: %i' % frame_count) pose_data = arm.pose #these must be ordered in the order the bones will show up in the PSA file! ordered_bones = {} ordered_bones = sorted([(psa_file.UseBone(x.name), x) for x in pose_data.bones], key=operator.itemgetter(0)) ############################# # ORDERED FRAME, BONE #for frame in scene_frames: for i in range(frame_count): frame = scene_frames[i] #LOUD #print ("==== outputting frame %i ===" % frame) if frame_count > i+1: next_frame = scene_frames[i+1] #print "This Frame: %i, Next Frame: %i" % (frame, next_frame) else: next_frame = -1 #print "This Frame: %i, Next Frame: NONE" % frame #frame start from 1 as number one from blender blender_scene.frame_set(frame) cur_frame_index = cur_frame_index + 1 for bone_data in ordered_bones: bone_index = bone_data[0] pose_bone = bone_data[1] #print("[=====POSE NAME:",pose_bone.name) #print("LENG >>.",len(bones_lookup)) blender_bone = bones_lookup[pose_bone.name] #just need the total unique bones used, later for this AnimInfoBinary unique_bone_indexes[bone_index] = bone_index #LOUD #print ("-------------------", pose_bone.name) head = pose_bone.head posebonemat = mathutils.Matrix(pose_bone.matrix) parent_pose = pose_bone.parent if parent_pose != None: parentposemat = mathutils.Matrix(parent_pose.matrix) #blender 2.4X it been flip around with new 2.50 (mat1 * mat2) should now be (mat2 * mat1) posebonemat = parentposemat.invert() * posebonemat head = posebonemat.translation_part() quat = posebonemat.to_quat().normalize() vkey = VQuatAnimKey() vkey.Position.X = head.x vkey.Position.Y = head.y vkey.Position.Z = head.z if parent_pose != None: quat = make_fquat(quat) else: quat = make_fquat_default(quat) vkey.Orientation = quat #print("Head:",head) #print("Orientation",quat) #time from now till next frame = diff / framesPerSec if next_frame >= 0: diff = next_frame - frame else: diff = 1.0 #print ("Diff = ", diff) vkey.Time = float(diff)/float(anim_rate) psa_file.AddRawKey(vkey) #done looping frames #done looping armatures #continue adding animInfoBinary counts here anim.TotalBones = len(unique_bone_indexes) print("Bones Count:",anim.TotalBones) anim.TrackTime = float(frame_count) / anim.AnimRate print("Time Track Frame:",anim.TrackTime) psa_file.AddAnimation(anim) print("==== Finish Action Build(s) ====") exportmessage = "Export Finish" def fs_callback(filename, context, user_setting): #this deal with repeat export and the reset settings global bonedata, BBCount, nbone, exportmessage bonedata = []#clear array BBCount = 0 nbone = 0 start_time = time.clock() print ("========EXPORTING TO UNREAL SKELETAL MESH FORMATS========\r\n") print("Blender Version:", bpy.app.version_string) psk = PSKFile() psa = PSAFile() #sanity check - this should already have the extension, but just in case, we'll give it one if it doesn't psk_filename = make_filename_ext(filename, '.psk') #make the psa filename psa_filename = make_filename_ext(filename, '.psa') print ('PSK File: ' + psk_filename) print ('PSA File: ' + psa_filename) barmature = True bmesh = True blender_meshes = [] blender_armature = [] selectmesh = [] selectarmature = [] current_scene = context.scene cur_frame = current_scene.frame_current #store current frame before we start walking them during animation parse objects = current_scene.objects print("Checking object count...") for next_obj in objects: if next_obj.type == 'MESH': blender_meshes.append(next_obj) if (next_obj.select): #print("mesh object select") selectmesh.append(next_obj) if next_obj.type == 'ARMATURE': blender_armature.append(next_obj) if (next_obj.select): #print("armature object select") selectarmature.append(next_obj) print("Mesh Count:",len(blender_meshes)," Armature Count:",len(blender_armature)) print("====================================") print("Checking Mesh Condtion(s):") if len(blender_meshes) == 1: print(" - One Mesh Scene") elif (len(blender_meshes) > 1) and (len(selectmesh) == 1): print(" - One Mesh [Select]") else: print(" - Too Many Meshes!") print(" - Select One Mesh Object!") bmesh = False print("====================================") print("Checking Armature Condtion(s):") if len(blender_armature) == 1: print(" - One Armature Scene") elif (len(blender_armature) > 1) and (len(selectarmature) == 1): print(" - One Armature [Select]") else: print(" - Too Armature Meshes!") print(" - Select One Armature Object Only!") barmature = False if (bmesh == False) or (barmature == False): exportmessage = "Export Fail! Check Log." print("=================================") print("= Export Fail! =") print("=================================") else: exportmessage = "Export Finish!" #need to build a temp bone index for mesh group vertex BoneIndexArmature(blender_armature) try: ####################### # STEP 1: MESH DUMP # we build the vertexes, wedges, and faces in here, as well as a vertexgroup lookup table # for the armature parse print("//===============================") print("// STEP 1") print("//===============================") parse_meshes(blender_meshes, psk) except: context.scene.frame_set(cur_frame) #set frame back to original frame print ("Exception during Mesh Parse") raise try: ####################### # STEP 2: ARMATURE DUMP # IMPORTANT: do this AFTER parsing meshes - we need to use the vertex group data from # the mesh parse in here to generate bone influences print("//===============================") print("// STEP 2") print("//===============================") parse_armature(blender_armature, psk, psa) except: context.scene.frame_set(cur_frame) #set frame back to original frame print ("Exception during Armature Parse") raise try: ####################### # STEP 3: ANIMATION DUMP # IMPORTANT: do AFTER parsing bones - we need to do bone lookups in here during animation frames print("//===============================") print("// STEP 3") print("//===============================") parse_animation(current_scene, blender_armature, psa) except: context.scene.frame_set(cur_frame) #set frame back to original frame print ("Exception during Animation Parse") raise # reset current frame context.scene.frame_set(cur_frame) #set frame back to original frame ########################## # FILE WRITE print("//===========================================") print("// bExportPsk:",bpy.context.scene.unrealexportpsk," bExportPsa:",bpy.context.scene.unrealexportpsa) print("//===========================================") if bpy.context.scene.unrealexportpsk == True: print("Writing Skeleton Mesh Data...") #RG - dump psk file psk.PrintOut() file = open(psk_filename, "wb") file.write(psk.dump()) file.close() print ("Successfully Exported File: " + psk_filename) if bpy.context.scene.unrealexportpsa == True: print("Writing Animaiton Data...") #RG - dump psa file if not psa.IsEmpty(): psa.PrintOut() file = open(psa_filename, "wb") file.write(psa.dump()) file.close() print ("Successfully Exported File: " + psa_filename) else: print ("No Animations (.psa file) to Export") print ('PSK/PSA Export Script finished in %.2f seconds' % (time.clock() - start_time)) #MSG BOX EXPORT COMPLETE #... #DONE print ("PSK/PSA Export Complete") def write_data(path, context, user_setting): print("//============================") print("// running psk/psa export...") print("//============================") fs_callback(path, context, user_setting) pass from bpy.props import * exporttypedata = [] # [index,text field,0] #or something like that exporttypedata.append(("0","PSK","Export PSK")) exporttypedata.append(("1","PSA","Export PSA")) exporttypedata.append(("2","ALL","Export ALL")) bpy.types.Scene.unrealfpsrate = IntProperty( name="fps rate", description="Set the frame per second (fps) for unreal.", default=24,min=1,max=100) bpy.types.Scene.unrealexport_settings = EnumProperty( name="Export:", description="Select a export settings (psk/psa/all)...", items = exporttypedata, default = '0') bpy.types.Scene.unrealtriangulatebool = BoolProperty( name="Triangulate Mesh", description="Convert Quad to Tri Mesh Boolean...", default=False) bpy.types.Scene.unrealactionexportall = BoolProperty( name="All Actions", description="This let you export all actions from current armature.[Not Build Yet]", default=False) bpy.types.Scene.unrealexportpsk = BoolProperty( name="bool export psa", description="bool for exporting this psk format", default=False) bpy.types.Scene.unrealexportpsa = BoolProperty( name="bool export psa", description="bool for exporting this psa format", default=False) class ExportUDKAnimData(bpy.types.Operator): global exportmessage '''Export Skeleton Mesh / Animation Data file(s)''' bl_idname = "export.udk_anim_data" # this is important since its how bpy.ops.export.udk_anim_data is constructed bl_label = "Export PSK/PSA" __doc__ = "One mesh and one armature else select one mesh or armature to be exported." # List of operator properties, the attributes will be assigned # to the class instance from the operator settings before calling. filepath = StringProperty(name="File Path", description="Filepath used for exporting the PSA file", maxlen= 1024, default= "") use_setting = BoolProperty(name="No Options Yet", description="No Options Yet", default= True) pskexportbool = BoolProperty(name="Export PSK", description="Export Skeletal Mesh", default= True) psaexportbool = BoolProperty(name="Export PSA", description="Export Action Set (Animation Data)", default= True) actionexportall = BoolProperty(name="All Actions", description="This will export all the actions that matches the current armature.", default=False) @classmethod def poll(cls, context): return context.active_object != None def execute(self, context): #check if skeleton mesh is needed to be exported if (self.pskexportbool): bpy.context.scene.unrealexportpsk = True else: bpy.context.scene.unrealexportpsk = False #check if animation data is needed to be exported if (self.psaexportbool): bpy.context.scene.unrealexportpsa = True else: bpy.context.scene.unrealexportpsa = False write_data(self.filepath, context, self.use_setting) self.report({'WARNING', 'INFO'}, exportmessage) return {'FINISHED'} def invoke(self, context, event): wm = context.window_manager wm.add_fileselect(self) return {'RUNNING_MODAL'} class VIEW3D_PT_unrealtools_objectmode(bpy.types.Panel): bl_space_type = "VIEW_3D" bl_region_type = "TOOLS" bl_label = "Unreal Tools" @classmethod def poll(cls, context): return context.active_object def draw(self, context): layout = self.layout #layout.label(text="Unreal Tools") rd = context.scene #drop box layout.prop(rd, "unrealexport_settings",expand=True) #layout.prop(rd, "unrealexport_settings") #button layout.operator("object.UnrealExport") #FPS #it use the real data from your scene layout.prop(rd.render, "fps") layout.prop(rd, "unrealactionexportall") #row = layout.row() #row.label(text="Action Set(s)(not build)") #for action in bpy.data.actions: #print(dir( action)) #print(action.frame_range) #row = layout.row() #row.prop(action, "name") #print(dir(action.groups[0])) #for g in action.groups:#those are bones #print("group...") #print(dir(g)) #print("////////////") #print((g.name)) #print("////////////") #row.label(text="Active:" + action.select) btrimesh = False class OBJECT_OT_UnrealExport(bpy.types.Operator): global exportmessage bl_idname = "OBJECT_OT_UnrealExport" bl_label = "Unreal Export" __doc__ = "Select export setting for .psk/.psa or both." def invoke(self, context, event): #path = StringProperty(name="File Path", description="File path used for exporting the PSA file", maxlen= 1024, default= "") print("Init Export Script:") if(int(bpy.context.scene.unrealexport_settings) == 0): bpy.context.scene.unrealexportpsk = True bpy.context.scene.unrealexportpsa = False print("Exporting PSK...") if(int(bpy.context.scene.unrealexport_settings) == 1): bpy.context.scene.unrealexportpsk = False bpy.context.scene.unrealexportpsa = True print("Exporting PSA...") if(int(bpy.context.scene.unrealexport_settings) == 2): bpy.context.scene.unrealexportpsk = True bpy.context.scene.unrealexportpsa = True print("Exporting ALL...") default_path = os.path.splitext(bpy.data.filepath)[0] + ".psk" fs_callback(default_path, bpy.context, False) #self.report({'WARNING', 'INFO'}, exportmessage) self.report({'INFO'}, exportmessage) return{'FINISHED'} def menu_func(self, context): bpy.context.scene.unrealexportpsk = True bpy.context.scene.unrealexportpsa = True default_path = os.path.splitext(bpy.data.filepath)[0] + ".psk" self.layout.operator("export.udk_anim_data", text="Skeleton Mesh / Animation Data (.psk/.psa)").filepath = default_path def register(): bpy.types.INFO_MT_file_export.append(menu_func) def unregister(): bpy.types.INFO_MT_file_export.remove(menu_func) if __name__ == "__main__": register()