Skip to content
Snippets Groups Projects
io_export_unreal_psk_psa.py 101 KiB
Newer Older
#  ***** 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 <http://www.gnu.org/licenses/>.
#  All rights reserved.
#  ***** GPL LICENSE BLOCK *****
bl_info = {
    "name": "Export Unreal Engine Format(.psk/.psa)",
John Phan's avatar
John Phan committed
    "author": "Darknet/Optimus_P-Fat/Active_Trash/Sinsoft/VendorX",
    "version": (2, 4),
    "blender": (2, 6, 0),
Luca Bonavita's avatar
Luca Bonavita committed
    "location": "File > Export > Skeletal Mesh/Animation Data (.psk/.psa)",
    "description": "Export Skeleletal Mesh/Animation Data",
Luca Bonavita's avatar
Luca Bonavita committed
    "warning": "",
    "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.5/Py/"\
        "Scripts/Import-Export/Unreal_psk_psa",
    "tracker_url": "https://projects.blender.org/tracker/index.php?"\
        "func=detail&aid=21366",
Luca Bonavita's avatar
Luca Bonavita committed
    "category": "Import-Export"}
-- Unreal Skeletal Mesh and Animation Export (.psk  and .psa) export script v0.0.1 --<br> 

- NOTES:
- This script Exports To Unreal's PSK and PSA file formats for Skeletal Meshes and Animations. <br>
- This script DOES NOT support vertex animation! These require completely different file formats. <br>

- 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 random
Campbell Barton's avatar
Campbell Barton committed
from struct import pack

# 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
John Phan's avatar
John Phan committed
MaterialName = []
# ======================================================================
# TODO: remove this 1am hack
nbone = 0
bDeleteMergeMesh = False
exportmessage = "Export Finish" 

########################################################################
# Generic Object->Integer mapping
# the object must be usable as a dictionary key
class ObjMap:
Luca Bonavita's avatar
Luca Bonavita committed
    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:
Luca Bonavita's avatar
Luca Bonavita committed
    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)
        
Luca Bonavita's avatar
Luca Bonavita committed
    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)
Luca Bonavita's avatar
Luca Bonavita committed
    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
            
Luca Bonavita's avatar
Luca Bonavita committed
    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):
John Phan's avatar
John Phan committed
        data = pack('64s64siiiifffiii', str.encode(self.Name), str.encode(self.Group), self.TotalBones, self.RootInclude, self.KeyCompressionStyle, self.KeyQuotum, self.KeyPrediction, self.TrackTime, self.AnimRate, self.StartBone, self.FirstRawFrame, self.NumRawFrames)
Luca Bonavita's avatar
Luca Bonavita committed
        return data
Luca Bonavita's avatar
Luca Bonavita committed
    def __init__(self, name, type_size):
John Phan's avatar
John Phan committed
        self.ChunkID = str.encode(name) # length=20
Luca Bonavita's avatar
Luca Bonavita committed
        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
        
Luca Bonavita's avatar
Luca Bonavita committed
    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):
        print("DATA MATERIAL:",self.MaterialName)
John Phan's avatar
John Phan committed
        data = pack('64siLiLii', str.encode(self.MaterialName), self.TextureIndex, self.PolyFlags, self.AuxMaterial, self.AuxFlags, self.LodBias, self.LodStyle)
Luca Bonavita's avatar
Luca Bonavita committed
        return data
Luca Bonavita's avatar
Luca Bonavita committed
    def __init__(self):
        self.Name = "" # length = 64
        self.Flags = 0 # DWORD
        self.NumChildren = 0
        self.ParentIndex = 0
        self.BonePos = VJointPos()
        
    def dump(self):
John Phan's avatar
John Phan committed
        data = pack('64sLii', str.encode(self.Name), self.Flags, self.NumChildren, self.ParentIndex) + self.BonePos.dump()
Luca Bonavita's avatar
Luca Bonavita committed
        return data
Luca Bonavita's avatar
Luca Bonavita committed

#same as above - whatever - this is how Epic does it...        
Luca Bonavita's avatar
Luca Bonavita committed
    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):
John Phan's avatar
John Phan committed
        data = pack('64sLii', str.encode(self.Name), self.Flags, self.NumChildren, self.ParentIndex) + self.BonePos.dump()
Luca Bonavita's avatar
Luca Bonavita committed
        return data
    
Luca Bonavita's avatar
Luca Bonavita committed
    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
        
Luca Bonavita's avatar
Luca Bonavita committed
    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
        
Luca Bonavita's avatar
Luca Bonavita committed
    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()
        
Luca Bonavita's avatar
Luca Bonavita committed
    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() 
        
Luca Bonavita's avatar
Luca Bonavita committed
    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:
Luca Bonavita's avatar
Luca Bonavita committed
    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)
        
Luca Bonavita's avatar
Luca Bonavita committed
    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()
John Phan's avatar
John Phan committed
            # modified by VendorX
            m.MaterialName = MaterialName[mat_index]
Luca Bonavita's avatar
Luca Bonavita committed
            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 ('-------------------------')
Luca Bonavita's avatar
Luca Bonavita committed
#    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. 
Luca Bonavita's avatar
Luca Bonavita committed
#    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)
Luca Bonavita's avatar
Luca Bonavita committed
    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 = {} 
Campbell Barton's avatar
Campbell Barton committed

Luca Bonavita's avatar
Luca Bonavita committed
    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 ('-------------------------')
        
Luca Bonavita's avatar
Luca Bonavita committed
####################################    
# helpers to create bone structs
def make_vbone(name, parent_index, child_count, orientation_quat, position_vect):
Luca Bonavita's avatar
Luca Bonavita committed
    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):
Luca Bonavita's avatar
Luca Bonavita committed
    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):
Luca Bonavita's avatar
Luca Bonavita committed
    #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):
    global bDeleteMergeMesh
Luca Bonavita's avatar
Luca Bonavita committed
    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!")
        if bDeleteMergeMesh == True:
            print("Remove Merge tmp Mesh [ " ,object.name, " ] from scene!" )
            bpy.ops.object.mode_set(mode='OBJECT') # set it in object
            bpy.context.scene.objects.unlink(object)
Luca Bonavita's avatar
Luca Bonavita committed
    else:
        bpy.context.scene.unrealtriangulatebool = False
        print("No need to convert tri mesh.")
        me_ob = object
    return me_ob

# Actual object parsing functions
def parse_meshes(blender_meshes, psk_file):
Luca Bonavita's avatar
Luca Bonavita committed
    #this is use to call the bone name and the index array for group index matches
    global bDeleteMergeMesh
Luca Bonavita's avatar
Luca Bonavita committed
    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
        #bpy.ops.object.mode_set(mode='EDIT')
Luca Bonavita's avatar
Luca Bonavita committed
        current_obj = triangulateNMesh(current_obj)
        #print(dir(current_obj))
        print("Mesh Name:",current_obj.name)
        current_mesh = current_obj.data
        
John Phan's avatar
John Phan committed
        #collect a list of the material names
        print("== MATERIAL EXPORT LIST & INDEX")
John Phan's avatar
John Phan committed
        if len(current_obj.material_slots) > 0:
            counter = 0
John Phan's avatar
John Phan committed
            while counter < len(current_obj.material_slots):
                print("[MATERIAL IDX:",counter,"=]")
John Phan's avatar
John Phan committed
                MaterialName.append(current_obj.material_slots[counter].name)
                #print("Material Name:",current_obj.material_slots[counter].name)
                #print("Material Name:",dir(current_obj.material_slots[counter].material))                
                #print("TEXTURE:",dir(current_obj.material_slots[counter].material.texture_slots[0].texture.image.filepath))
                #print("Imagepath:",(current_obj.material_slots[counter].material.texture_slots[0].texture.image.filepath))
                #print("TEXTURES:",len(current_obj.material_slots[counter].material.texture_slots))
                #while slot in current_obj.material_slots[counter].material.texture_slots:
                    #print(dir(slot))
                    #if slot.texture.image.filepath != None:
                        #print("file path:",slot.texture.image.filepath)
                if current_obj.material_slots[counter].material.texture_slots[0] != None:
                    if current_obj.material_slots[counter].material.texture_slots[0].texture.image.filepath != None:
                        print("TEXTURE PATH:",current_obj.material_slots[counter].material.texture_slots[0].texture.image.filepath)
                #print("Imagepath:",(current_obj.material_slots[counter].material.texture_slots[0].texture.image.filepath_raw))
                #print("Imagepath2:",dir(current_obj.material_slots[counter].material.texture_slots[0].texture.image))				
John Phan's avatar
John Phan committed
                #create the current material
                matdata = psk_file.GetMatByIndex(counter)
                matdata.MaterialName = current_obj.material_slots[counter].name
                matdata.TextureIndex = counter
                matdata.AuxMaterial = counter
John Phan's avatar
John Phan committed
                #print("materials: ",MaterialName[counter])
                counter += 1
                print("PSK INDEX:",matdata.TextureIndex)
                print("=====")
                print("")
Luca Bonavita's avatar
Luca Bonavita committed
        #    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
John Phan's avatar
John Phan committed
            # modified by VendorX
            object_material_index = current_face.material_index
Luca Bonavita's avatar
Luca Bonavita committed
            
            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
Campbell Barton's avatar
Campbell Barton committed
                psk_file.GetMatByIndex(object_material_index)
Luca Bonavita's avatar
Luca Bonavita committed

                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
                    #check if limit boolean
Luca Bonavita's avatar
Luca Bonavita committed
                    #if value is over the set limit it will null the uv texture
                    if bpy.context.scene.limituv:
                        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
                        #print("limited on")
                    #else:
                        #print("limited off")
Luca Bonavita's avatar
Luca Bonavita committed
                    
                    # 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 = current_obj.matrix_local * vert.co
Luca Bonavita's avatar
Luca Bonavita committed
                    # 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];

                    current_mesh.vertices[dindex0].select = True
                    current_mesh.vertices[dindex1].select = True
                    current_mesh.vertices[dindex2].select = True
                    
                    raise RuntimeError("normal vector coplanar with face! points:", current_mesh.vertices[dindex0].co, current_mesh.vertices[dindex1].co, current_mesh.vertices[dindex2].co)
                #print(dir(current_face))
                current_face.select = True
                #print("smooth:",(current_face.use_smooth))
Luca Bonavita's avatar
Luca Bonavita committed
                #not sure if this right
                #tri.SmoothingGroups
                if current_face.use_smooth == True:
                    tri.SmoothingGroups = 1
                else:
                    tri.SmoothingGroups = 0
                #tri.SmoothingGroups = 1
Luca Bonavita's avatar
Luca Bonavita committed
                tri.MatIndex = object_material_index
                #print(tri)
                psk_file.AddFace(tri)                
Luca Bonavita's avatar
Luca Bonavita committed
            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)
John Phan's avatar
John Phan committed
        if len(points.dict) > 32767:
            raise RuntimeError("Vertex point reach max limited 32767 in pack data. Your",len(points.dict))
Luca Bonavita's avatar
Luca Bonavita committed
        print (" -- Dumping Mesh Wedge -- LEN:",len(wedges.dict))
Luca Bonavita's avatar
Luca Bonavita committed
        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 obvgroup in current_obj.vertex_groups:
            #print("bone gourp build:",obvgroup.name)#print bone name
            #print(dir(obvgroup))
Luca Bonavita's avatar
Luca Bonavita committed
            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(obvgroup.index == vgroup.group):
Luca Bonavita's avatar
Luca Bonavita committed
                        p = VPoint()
                        vpos = current_obj.matrix_local * current_vert.co
Luca Bonavita's avatar
Luca Bonavita committed
                        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:",obvgroup.name, " No. Points:",len(vert_list))
            psk_file.VertexGroups[obvgroup.name] = vert_list
Luca Bonavita's avatar
Luca Bonavita committed
        
        #unrealtriangulatebool #this will remove the mesh from the scene
        '''
        if (bpy.context.scene.unrealtriangulatebool == True) and (bDeleteMergeMesh == True):
            #if bDeleteMergeMesh == True:
            #    print("Removing merge mesh.")
Luca Bonavita's avatar
Luca Bonavita committed
            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)
        el
        '''
        if bDeleteMergeMesh == True:
            print("Remove Merge 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)
        elif bpy.context.scene.unrealtriangulatebool == True:
            print("Remove tri 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)
John Phan's avatar
John Phan committed
        #if bDeleteMergeMesh == True:
            #print("Remove merge Mesh [ " ,current_obj.name, " ] from scene")
            #bpy.ops.object.mode_set(mode='OBJECT') # set it in object
            #bpy.context.scene.objects.unlink(current_obj)
Luca Bonavita's avatar
Luca Bonavita committed
        
Luca Bonavita's avatar
Luca Bonavita committed
    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
    
Luca Bonavita's avatar
Luca Bonavita committed
    quat = FQuat()
John Phan's avatar
John Phan committed
    #print(dir(bquat))
Luca Bonavita's avatar
Luca Bonavita committed
    quat.X = bquat.x
    quat.Y = bquat.y
    quat.Z = bquat.z
    
    quat.W = bquat.w
    return quat

def parse_bone(blender_bone, psk_file, psa_file, parent_id, is_root_bone, parent_matrix, parent_root):
Luca Bonavita's avatar
Luca Bonavita committed
    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_quaternion())
John Phan's avatar
John Phan committed
        #print("DIR:",dir(child_parent.matrix.to_quaternion()))
        quat_parent = child_parent.matrix.to_quaternion().inverted()
        parent_head = quat_parent * child_parent.head
        parent_tail = quat_parent * child_parent.tail
Luca Bonavita's avatar
Luca Bonavita committed