== Blender 2.56 tag ==
[blender-addons.git] / io_export_unreal_psk_psa.py
blobb5f3773acb9cf41aaa8cd6e7477590e412313726
1 # ***** GPL LICENSE BLOCK *****
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation, either version 3 of the License, or
6 # (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # All rights reserved.
16 # ***** GPL LICENSE BLOCK *****
18 bl_addon_info = {
19 "name": "Export Skeleletal Mesh/Animation Data",
20 "author": "Darknet/Optimus_P-Fat/Active_Trash/Sinsoft",
21 "version": (2, 0),
22 "blender": (2, 5, 3),
23 "api": 31847,
24 "location": "File > Export > Skeletal Mesh/Animation Data (.psk/.psa)",
25 "description": "Export Unreal Engine (.psk)",
26 "warning": "",
27 "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.5/Py/"\
28 "Scripts/Import-Export/Unreal_psk_psa",
29 "tracker_url": "https://projects.blender.org/tracker/index.php?"\
30 "func=detail&aid=21366&group_id=153&atid=469",
31 "category": "Import-Export"}
33 """
34 -- Unreal Skeletal Mesh and Animation Export (.psk and .psa) export script v0.0.1 --<br>
36 - NOTES:
37 - This script Exports To Unreal's PSK and PSA file formats for Skeletal Meshes and Animations. <br>
38 - This script DOES NOT support vertex animation! These require completely different file formats. <br>
40 - v0.0.1
41 - Initial version
43 - v0.0.2
44 - This version adds support for more than one material index!
46 [ - Edit by: Darknet
47 - v0.0.3 - v0.0.12
48 - This will work on UT3 and it is a stable version that work with vehicle for testing.
49 - Main Bone fix no dummy needed to be there.
50 - Just bone issues position, rotation, and offset for psk.
51 - The armature bone position, rotation, and the offset of the bone is fix. It was to deal with skeleton mesh export for psk.
52 - Animation is fix for position, offset, rotation bone support one rotation direction when armature build.
53 - It will convert your mesh into triangular when exporting to psk file.
54 - Did not work with psa export yet.
56 - v0.0.13
57 - The animatoin will support different bone rotations when export the animation.
59 - v0.0.14
60 - Fixed Action set keys frames when there is no pose keys and it will ignore it.
62 - v0.0.15
63 - Fixed multiple objects when exporting to psk. Select one mesh to export to psk.
64 - ]
66 - v0.1.1
67 - Blender 2.50 svn (Support)
69 Credit to:
70 - export_cal3d.py (Position of the Bones Format)
71 - blender2md5.py (Animation Translation Format)
72 - export_obj.py (Blender 2.5/Pyhton 3.x Format)
74 - freenode #blendercoder -> user -> ideasman42
76 - Give Credit to those who work on this script.
78 - http://sinsoft.com
79 """
81 import os
82 import time
83 import datetime
84 import bpy
85 import mathutils
86 import operator
88 from struct import pack, calcsize
91 # REFERENCE MATERIAL JUST IN CASE:
93 # U = x / sqrt(x^2 + y^2 + z^2)
94 # V = y / sqrt(x^2 + y^2 + z^2)
96 # Triangles specifed counter clockwise for front face
98 #defines for sizeofs
99 SIZE_FQUAT = 16
100 SIZE_FVECTOR = 12
101 SIZE_VJOINTPOS = 44
102 SIZE_ANIMINFOBINARY = 168
103 SIZE_VCHUNKHEADER = 32
104 SIZE_VMATERIAL = 88
105 SIZE_VBONE = 120
106 SIZE_FNAMEDBONEBINARY = 120
107 SIZE_VRAWBONEINFLUENCE = 12
108 SIZE_VQUATANIMKEY = 32
109 SIZE_VVERTEX = 16
110 SIZE_VPOINT = 12
111 SIZE_VTRIANGLE = 12
113 ########################################################################
114 # Generic Object->Integer mapping
115 # the object must be usable as a dictionary key
116 class ObjMap:
117 def __init__(self):
118 self.dict = {}
119 self.next = 0
120 def get(self, obj):
121 if obj in self.dict:
122 return self.dict[obj]
123 else:
124 id = self.next
125 self.next = self.next + 1
126 self.dict[obj] = id
127 return id
129 def items(self):
130 getval = operator.itemgetter(0)
131 getkey = operator.itemgetter(1)
132 return map(getval, sorted(self.dict.items(), key=getkey))
134 ########################################################################
135 # RG - UNREAL DATA STRUCTS - CONVERTED FROM C STRUCTS GIVEN ON UDN SITE
136 # provided here: http://udn.epicgames.com/Two/BinaryFormatSpecifications.html
137 # updated UDK (Unreal Engine 3): http://udn.epicgames.com/Three/BinaryFormatSpecifications.html
138 class FQuat:
139 def __init__(self):
140 self.X = 0.0
141 self.Y = 0.0
142 self.Z = 0.0
143 self.W = 1.0
145 def dump(self):
146 data = pack('ffff', self.X, self.Y, self.Z, self.W)
147 return data
149 def __cmp__(self, other):
150 return cmp(self.X, other.X) \
151 or cmp(self.Y, other.Y) \
152 or cmp(self.Z, other.Z) \
153 or cmp(self.W, other.W)
155 def __hash__(self):
156 return hash(self.X) ^ hash(self.Y) ^ hash(self.Z) ^ hash(self.W)
158 def __str__(self):
159 return "[%f,%f,%f,%f](FQuat)" % (self.X, self.Y, self.Z, self.W)
161 class FVector(object):
162 def __init__(self, X=0.0, Y=0.0, Z=0.0):
163 self.X = X
164 self.Y = Y
165 self.Z = Z
167 def dump(self):
168 data = pack('fff', self.X, self.Y, self.Z)
169 return data
171 def __cmp__(self, other):
172 return cmp(self.X, other.X) \
173 or cmp(self.Y, other.Y) \
174 or cmp(self.Z, other.Z)
176 def _key(self):
177 return (type(self).__name__, self.X, self.Y, self.Z)
179 def __hash__(self):
180 return hash(self._key())
182 def __eq__(self, other):
183 if not hasattr(other, '_key'):
184 return False
185 return self._key() == other._key()
187 def dot(self, other):
188 return self.X * other.X + self.Y * other.Y + self.Z * other.Z
190 def cross(self, other):
191 return FVector(self.Y * other.Z - self.Z * other.Y,
192 self.Z * other.X - self.X * other.Z,
193 self.X * other.Y - self.Y * other.X)
195 def sub(self, other):
196 return FVector(self.X - other.X,
197 self.Y - other.Y,
198 self.Z - other.Z)
200 class VJointPos:
201 def __init__(self):
202 self.Orientation = FQuat()
203 self.Position = FVector()
204 self.Length = 0.0
205 self.XSize = 0.0
206 self.YSize = 0.0
207 self.ZSize = 0.0
209 def dump(self):
210 data = self.Orientation.dump() + self.Position.dump() + pack('4f', self.Length, self.XSize, self.YSize, self.ZSize)
211 return data
213 class AnimInfoBinary:
214 def __init__(self):
215 self.Name = "" # length=64
216 self.Group = "" # length=64
217 self.TotalBones = 0
218 self.RootInclude = 0
219 self.KeyCompressionStyle = 0
220 self.KeyQuotum = 0
221 self.KeyPrediction = 0.0
222 self.TrackTime = 0.0
223 self.AnimRate = 0.0
224 self.StartBone = 0
225 self.FirstRawFrame = 0
226 self.NumRawFrames = 0
228 def dump(self):
229 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)
230 return data
232 class VChunkHeader:
233 def __init__(self, name, type_size):
234 self.ChunkID = name # length=20
235 self.TypeFlag = 1999801 # special value
236 self.DataSize = type_size
237 self.DataCount = 0
239 def dump(self):
240 data = pack('20siii', self.ChunkID, self.TypeFlag, self.DataSize, self.DataCount)
241 return data
243 class VMaterial:
244 def __init__(self):
245 self.MaterialName = "" # length=64
246 self.TextureIndex = 0
247 self.PolyFlags = 0 # DWORD
248 self.AuxMaterial = 0
249 self.AuxFlags = 0 # DWORD
250 self.LodBias = 0
251 self.LodStyle = 0
253 def dump(self):
254 data = pack('64siLiLii', self.MaterialName, self.TextureIndex, self.PolyFlags, self.AuxMaterial, self.AuxFlags, self.LodBias, self.LodStyle)
255 return data
257 class VBone:
258 def __init__(self):
259 self.Name = "" # length = 64
260 self.Flags = 0 # DWORD
261 self.NumChildren = 0
262 self.ParentIndex = 0
263 self.BonePos = VJointPos()
265 def dump(self):
266 data = pack('64sLii', self.Name, self.Flags, self.NumChildren, self.ParentIndex) + self.BonePos.dump()
267 return data
269 #same as above - whatever - this is how Epic does it...
270 class FNamedBoneBinary:
271 def __init__(self):
272 self.Name = "" # length = 64
273 self.Flags = 0 # DWORD
274 self.NumChildren = 0
275 self.ParentIndex = 0
276 self.BonePos = VJointPos()
278 self.IsRealBone = 0 # this is set to 1 when the bone is actually a bone in the mesh and not a dummy
280 def dump(self):
281 data = pack('64sLii', self.Name, self.Flags, self.NumChildren, self.ParentIndex) + self.BonePos.dump()
282 return data
284 class VRawBoneInfluence:
285 def __init__(self):
286 self.Weight = 0.0
287 self.PointIndex = 0
288 self.BoneIndex = 0
290 def dump(self):
291 data = pack('fii', self.Weight, self.PointIndex, self.BoneIndex)
292 return data
294 class VQuatAnimKey:
295 def __init__(self):
296 self.Position = FVector()
297 self.Orientation = FQuat()
298 self.Time = 0.0
300 def dump(self):
301 data = self.Position.dump() + self.Orientation.dump() + pack('f', self.Time)
302 return data
304 class VVertex(object):
305 def __init__(self):
306 self.PointIndex = 0 # WORD
307 self.U = 0.0
308 self.V = 0.0
309 self.MatIndex = 0 #BYTE
310 self.Reserved = 0 #BYTE
312 def dump(self):
313 data = pack('HHffBBH', self.PointIndex, 0, self.U, self.V, self.MatIndex, self.Reserved, 0)
314 return data
316 def __cmp__(self, other):
317 return cmp(self.PointIndex, other.PointIndex) \
318 or cmp(self.U, other.U) \
319 or cmp(self.V, other.V) \
320 or cmp(self.MatIndex, other.MatIndex) \
321 or cmp(self.Reserved, other.Reserved)
323 def _key(self):
324 return (type(self).__name__,self.PointIndex, self.U, self.V,self.MatIndex,self.Reserved)
326 def __hash__(self):
327 return hash(self._key())
329 def __eq__(self, other):
330 if not hasattr(other, '_key'):
331 return False
332 return self._key() == other._key()
334 class VPoint(object):
335 def __init__(self):
336 self.Point = FVector()
338 def dump(self):
339 return self.Point.dump()
341 def __cmp__(self, other):
342 return cmp(self.Point, other.Point)
344 def _key(self):
345 return (type(self).__name__, self.Point)
347 def __hash__(self):
348 return hash(self._key())
350 def __eq__(self, other):
351 if not hasattr(other, '_key'):
352 return False
353 return self._key() == other._key()
355 class VTriangle:
356 def __init__(self):
357 self.WedgeIndex0 = 0 # WORD
358 self.WedgeIndex1 = 0 # WORD
359 self.WedgeIndex2 = 0 # WORD
360 self.MatIndex = 0 # BYTE
361 self.AuxMatIndex = 0 # BYTE
362 self.SmoothingGroups = 0 # DWORD
364 def dump(self):
365 data = pack('HHHBBL', self.WedgeIndex0, self.WedgeIndex1, self.WedgeIndex2, self.MatIndex, self.AuxMatIndex, self.SmoothingGroups)
366 return data
368 # END UNREAL DATA STRUCTS
369 ########################################################################
371 ########################################################################
372 #RG - helper class to handle the normal way the UT files are stored
373 #as sections consisting of a header and then a list of data structures
374 class FileSection:
375 def __init__(self, name, type_size):
376 self.Header = VChunkHeader(name, type_size)
377 self.Data = [] # list of datatypes
379 def dump(self):
380 data = self.Header.dump()
381 for i in range(len(self.Data)):
382 data = data + self.Data[i].dump()
383 return data
385 def UpdateHeader(self):
386 self.Header.DataCount = len(self.Data)
388 class PSKFile:
389 def __init__(self):
390 self.GeneralHeader = VChunkHeader("ACTRHEAD", 0)
391 self.Points = FileSection("PNTS0000", SIZE_VPOINT) #VPoint
392 self.Wedges = FileSection("VTXW0000", SIZE_VVERTEX) #VVertex
393 self.Faces = FileSection("FACE0000", SIZE_VTRIANGLE) #VTriangle
394 self.Materials = FileSection("MATT0000", SIZE_VMATERIAL) #VMaterial
395 self.Bones = FileSection("REFSKELT", SIZE_VBONE) #VBone
396 self.Influences = FileSection("RAWWEIGHTS", SIZE_VRAWBONEINFLUENCE) #VRawBoneInfluence
398 #RG - this mapping is not dumped, but is used internally to store the new point indices
399 # for vertex groups calculated during the mesh dump, so they can be used again
400 # to dump bone influences during the armature dump
402 # the key in this dictionary is the VertexGroup/Bone Name, and the value
403 # is a list of tuples containing the new point index and the weight, in that order
405 # Layout:
406 # { groupname : [ (index, weight), ... ], ... }
408 # example:
409 # { 'MyVertexGroup' : [ (0, 1.0), (5, 1.0), (3, 0.5) ] , 'OtherGroup' : [(2, 1.0)] }
411 self.VertexGroups = {}
413 def AddPoint(self, p):
414 #print ('AddPoint')
415 self.Points.Data.append(p)
417 def AddWedge(self, w):
418 #print ('AddWedge')
419 self.Wedges.Data.append(w)
421 def AddFace(self, f):
422 #print ('AddFace')
423 self.Faces.Data.append(f)
425 def AddMaterial(self, m):
426 #print ('AddMaterial')
427 self.Materials.Data.append(m)
429 def AddBone(self, b):
430 #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))
431 self.Bones.Data.append(b)
433 def AddInfluence(self, i):
434 #print ('AddInfluence')
435 self.Influences.Data.append(i)
437 def UpdateHeaders(self):
438 self.Points.UpdateHeader()
439 self.Wedges.UpdateHeader()
440 self.Faces.UpdateHeader()
441 self.Materials.UpdateHeader()
442 self.Bones.UpdateHeader()
443 self.Influences.UpdateHeader()
445 def dump(self):
446 self.UpdateHeaders()
447 data = self.GeneralHeader.dump() + self.Points.dump() + self.Wedges.dump() + self.Faces.dump() + self.Materials.dump() + self.Bones.dump() + self.Influences.dump()
448 return data
450 def GetMatByIndex(self, mat_index):
451 if mat_index >= 0 and len(self.Materials.Data) > mat_index:
452 return self.Materials.Data[mat_index]
453 else:
454 m = VMaterial()
455 m.MaterialName = "Mat%i" % mat_index
456 self.AddMaterial(m)
457 return m
459 def PrintOut(self):
460 print ("--- PSK FILE EXPORTED ---")
461 print ('point count: %i' % len(self.Points.Data))
462 print ('wedge count: %i' % len(self.Wedges.Data))
463 print ('face count: %i' % len(self.Faces.Data))
464 print ('material count: %i' % len(self.Materials.Data))
465 print ('bone count: %i' % len(self.Bones.Data))
466 print ('inlfuence count: %i' % len(self.Influences.Data))
467 print ('-------------------------')
469 # PSA FILE NOTES FROM UDN:
471 # The raw key array holds all the keys for all the bones in all the specified sequences,
472 # organized as follows:
473 # For each AnimInfoBinary's sequence there are [Number of bones] times [Number of frames keys]
474 # in the VQuatAnimKeys, laid out as tracks of [numframes] keys for each bone in the order of
475 # the bones as defined in the array of FnamedBoneBinary in the PSA.
477 # Once the data from the PSK (now digested into native skeletal mesh) and PSA (digested into
478 # a native animation object containing one or more sequences) are associated together at runtime,
479 # bones are linked up by name. Any bone in a skeleton (from the PSK) that finds no partner in
480 # the animation sequence (from the PSA) will assume its reference pose stance ( as defined in
481 # the offsets & rotations that are in the VBones making up the reference skeleton from the PSK)
483 class PSAFile:
484 def __init__(self):
485 self.GeneralHeader = VChunkHeader("ANIMHEAD", 0)
486 self.Bones = FileSection("BONENAMES", SIZE_FNAMEDBONEBINARY) #FNamedBoneBinary
487 self.Animations = FileSection("ANIMINFO", SIZE_ANIMINFOBINARY) #AnimInfoBinary
488 self.RawKeys = FileSection("ANIMKEYS", SIZE_VQUATANIMKEY) #VQuatAnimKey
490 # this will take the format of key=Bone Name, value = (BoneIndex, Bone Object)
491 # THIS IS NOT DUMPED
492 self.BoneLookup = {}
494 def dump(self):
495 data = self.Generalheader.dump() + self.Bones.dump() + self.Animations.dump() + self.RawKeys.dump()
496 return data
498 def AddBone(self, b):
499 #LOUD
500 #print "AddBone: " + b.Name
501 self.Bones.Data.append(b)
503 def AddAnimation(self, a):
504 #LOUD
505 #print "AddAnimation: %s, TotalBones: %i, AnimRate: %f, NumRawFrames: %i, TrackTime: %f" % (a.Name, a.TotalBones, a.AnimRate, a.NumRawFrames, a.TrackTime)
506 self.Animations.Data.append(a)
508 def AddRawKey(self, k):
509 #LOUD
510 #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)
511 self.RawKeys.Data.append(k)
513 def UpdateHeaders(self):
514 self.Bones.UpdateHeader()
515 self.Animations.UpdateHeader()
516 self.RawKeys.UpdateHeader()
518 def GetBoneByIndex(self, bone_index):
519 if bone_index >= 0 and len(self.Bones.Data) > bone_index:
520 return self.Bones.Data[bone_index]
522 def IsEmpty(self):
523 return (len(self.Bones.Data) == 0 or len(self.Animations.Data) == 0)
525 def StoreBone(self, b):
526 self.BoneLookup[b.Name] = [-1, b]
528 def UseBone(self, bone_name):
529 if bone_name in self.BoneLookup:
530 bone_data = self.BoneLookup[bone_name]
532 if bone_data[0] == -1:
533 bone_data[0] = len(self.Bones.Data)
534 self.AddBone(bone_data[1])
535 #self.Bones.Data.append(bone_data[1])
537 return bone_data[0]
539 def GetBoneByName(self, bone_name):
540 if bone_name in self.BoneLookup:
541 bone_data = self.BoneLookup[bone_name]
542 return bone_data[1]
544 def GetBoneIndex(self, bone_name):
545 if bone_name in self.BoneLookup:
546 bone_data = self.BoneLookup[bone_name]
547 return bone_data[0]
549 def dump(self):
550 self.UpdateHeaders()
551 data = self.GeneralHeader.dump() + self.Bones.dump() + self.Animations.dump() + self.RawKeys.dump()
552 return data
554 def PrintOut(self):
555 print ('--- PSA FILE EXPORTED ---')
556 print ('bone count: %i' % len(self.Bones.Data))
557 print ('animation count: %i' % len(self.Animations.Data))
558 print ('rawkey count: %i' % len(self.RawKeys.Data))
559 print ('-------------------------')
561 ####################################
562 # helpers to create bone structs
563 def make_vbone(name, parent_index, child_count, orientation_quat, position_vect):
564 bone = VBone()
565 bone.Name = name
566 bone.ParentIndex = parent_index
567 bone.NumChildren = child_count
568 bone.BonePos.Orientation = orientation_quat
569 bone.BonePos.Position.X = position_vect.x
570 bone.BonePos.Position.Y = position_vect.y
571 bone.BonePos.Position.Z = position_vect.z
573 #these values seem to be ignored?
574 #bone.BonePos.Length = tail.length
575 #bone.BonePos.XSize = tail.x
576 #bone.BonePos.YSize = tail.y
577 #bone.BonePos.ZSize = tail.z
579 return bone
581 def make_namedbonebinary(name, parent_index, child_count, orientation_quat, position_vect, is_real):
582 bone = FNamedBoneBinary()
583 bone.Name = name
584 bone.ParentIndex = parent_index
585 bone.NumChildren = child_count
586 bone.BonePos.Orientation = orientation_quat
587 bone.BonePos.Position.X = position_vect.x
588 bone.BonePos.Position.Y = position_vect.y
589 bone.BonePos.Position.Z = position_vect.z
590 bone.IsRealBone = is_real
591 return bone
593 ##################################################
594 #RG - check to make sure face isnt a line
595 #The face has to be triangle not a line
596 def is_1d_face(blender_face,mesh):
597 #ID Vertex of id point
598 v0 = blender_face.vertices[0]
599 v1 = blender_face.vertices[1]
600 v2 = blender_face.vertices[2]
602 return (mesh.vertices[v0].co == mesh.vertices[v1].co or \
603 mesh.vertices[v1].co == mesh.vertices[v2].co or \
604 mesh.vertices[v2].co == mesh.vertices[v0].co)
605 return False
607 ##################################################
608 # http://en.wikibooks.org/wiki/Blender_3D:_Blending_Into_Python/Cookbook#Triangulate_NMesh
610 #blender 2.50 format using the Operators/command convert the mesh to tri mesh
611 def triangulateNMesh(object):
612 bneedtri = False
613 scene = bpy.context.scene
614 bpy.ops.object.mode_set(mode='OBJECT')
615 for i in scene.objects: i.select = False #deselect all objects
616 object.select = True
617 scene.objects.active = object #set the mesh object to current
618 bpy.ops.object.mode_set(mode='OBJECT')
619 print("Checking mesh if needs to convert quad to Tri...")
620 for face in object.data.faces:
621 if (len(face.vertices) > 3):
622 bneedtri = True
623 break
625 bpy.ops.object.mode_set(mode='OBJECT')
626 if bneedtri == True:
627 print("Converting quad to tri mesh...")
628 me_da = object.data.copy() #copy data
629 me_ob = object.copy() #copy object
630 #note two copy two types else it will use the current data or mesh
631 me_ob.data = me_da
632 bpy.context.scene.objects.link(me_ob)#link the object to the scene #current object location
633 for i in scene.objects: i.select = False #deselect all objects
634 me_ob.select = True
635 scene.objects.active = me_ob #set the mesh object to current
636 bpy.ops.object.mode_set(mode='EDIT') #Operators
637 bpy.ops.mesh.select_all(action='SELECT')#select all the face/vertex/edge
638 bpy.ops.mesh.quads_convert_to_tris() #Operators
639 bpy.context.scene.update()
640 bpy.ops.object.mode_set(mode='OBJECT') # set it in object
641 bpy.context.scene.unrealtriangulatebool = True
642 print("Triangulate Mesh Done!")
643 else:
644 bpy.context.scene.unrealtriangulatebool = False
645 print("No need to convert tri mesh.")
646 me_ob = object
647 return me_ob
649 #Blender Bone Index
650 class BBone:
651 def __init__(self):
652 self.bone = ""
653 self.index = 0
654 bonedata = []
655 BBCount = 0
656 #deal with mesh bones groups vertex point
657 def BoneIndex(bone):
658 global BBCount, bonedata
659 #print("//==============")
660 #print(bone.name , "ID:",BBCount)
661 BB = BBone()
662 BB.bone = bone.name
663 BB.index = BBCount
664 bonedata.append(BB)
665 BBCount += 1
666 for current_child_bone in bone.children:
667 BoneIndex(current_child_bone)
669 def BoneIndexArmature(blender_armature):
670 global BBCount
671 #print("\n Buildng bone before mesh \n")
672 #objectbone = blender_armature.pose #Armature bone
673 #print(blender_armature)
674 objectbone = blender_armature[0].pose
675 #print(dir(ArmatureData))
677 for bone in objectbone.bones:
678 if(bone.parent is None):
679 BoneIndex(bone)
680 #BBCount += 1
681 break
683 # Actual object parsing functions
684 def parse_meshes(blender_meshes, psk_file):
685 #this is use to call the bone name and the index array for group index matches
686 global bonedata
687 #print("BONE DATA",len(bonedata))
688 print ("----- parsing meshes -----")
689 print("Number of Object Meshes:",len(blender_meshes))
690 for current_obj in blender_meshes: #number of mesh that should be one mesh here
691 bpy.ops.object.mode_set(mode='EDIT')
692 current_obj = triangulateNMesh(current_obj)
693 #print(dir(current_obj))
694 print("Mesh Name:",current_obj.name)
695 current_mesh = current_obj.data
697 #if len(current_obj.materials) > 0:
698 # object_mat = current_obj.materials[0]
699 object_material_index = current_obj.active_material_index
701 points = ObjMap()
702 wedges = ObjMap()
704 discarded_face_count = 0
705 print (" -- Dumping Mesh Faces -- LEN:", len(current_mesh.faces))
706 for current_face in current_mesh.faces:
707 #print ' -- Dumping UVs -- '
708 #print current_face.uv_textures
710 if len(current_face.vertices) != 3:
711 raise RuntimeError("Non-triangular face (%i)" % len(current_face.vertices))
713 #No Triangulate Yet
714 # if len(current_face.vertices) != 3:
715 # raise RuntimeError("Non-triangular face (%i)" % len(current_face.vertices))
716 # #TODO: add two fake faces made of triangles?
718 #RG - apparently blender sometimes has problems when you do quad to triangle
719 # conversion, and ends up creating faces that have only TWO points -
720 # one of the points is simply in the vertex list for the face twice.
721 # This is bad, since we can't get a real face normal for a LINE, we need
722 # a plane for this. So, before we add the face to the list of real faces,
723 # ensure that the face is actually a plane, and not a line. If it is not
724 # planar, just discard it and notify the user in the console after we're
725 # done dumping the rest of the faces
727 if not is_1d_face(current_face,current_mesh):
728 #print("faces")
729 wedge_list = []
730 vect_list = []
732 #get or create the current material
733 m = psk_file.GetMatByIndex(object_material_index)
735 face_index = current_face.index
736 has_UV = False
737 faceUV = None
739 if len(current_mesh.uv_textures) > 0:
740 has_UV = True
741 #print("face index: ",face_index)
742 #faceUV = current_mesh.uv_textures.active.data[face_index]#UVs for current face
743 #faceUV = current_mesh.uv_textures.active.data[0]#UVs for current face
744 #print(face_index,"<[FACE NUMBER")
745 uv_layer = current_mesh.uv_textures.active
746 faceUV = uv_layer.data[face_index]
747 #print("============================")
748 #size(data) is number of texture faces. Each face has UVs
749 #print("DATA face uv: ",len(faceUV.uv), " >> ",(faceUV.uv[0][0]))
751 for i in range(3):
752 vert_index = current_face.vertices[i]
753 vert = current_mesh.vertices[vert_index]
754 uv = []
755 #assumes 3 UVs Per face (for now).
756 if (has_UV):
757 if len(faceUV.uv) != 3:
758 print ("WARNING: Current face is missing UV coordinates - writing 0,0...")
759 print ("WARNING: Face has more than 3 UVs - writing 0,0...")
760 uv = [0.0, 0.0]
761 else:
762 #uv.append(faceUV.uv[i][0])
763 #uv.append(faceUV.uv[i][1])
764 uv = [faceUV.uv[i][0],faceUV.uv[i][1]] #OR bottom works better # 24 for cube
765 #uv = list(faceUV.uv[i]) #30 just cube
766 else:
767 #print ("No UVs?")
768 uv = [0.0, 0.0]
769 #print("UV >",uv)
770 #uv = [0.0, 0.0] #over ride uv that is not fixed
771 #print(uv)
772 #flip V coordinate because UEd requires it and DOESN'T flip it on its own like it
773 #does with the mesh Y coordinates.
774 #this is otherwise known as MAGIC-2
775 uv[1] = 1.0 - uv[1]
777 #deal with the min and max value
778 #if value is over the set limit it will null the uv texture
779 if (uv[0] > 1):
780 uv[0] = 1
781 if (uv[0] < 0):
782 uv[0] = 0
783 if (uv[1] > 1):
784 uv[1] = 1
785 if (uv[1] < 0):
786 uv[1] = 0
789 # RE - Append untransformed vector (for normal calc below)
790 # TODO: convert to Blender.Mathutils
791 vect_list.append(FVector(vert.co.x, vert.co.y, vert.co.z))
793 # Transform position for export
794 #vpos = vert.co * object_material_index
795 vpos = vert.co * current_obj.matrix_local
796 # Create the point
797 p = VPoint()
798 p.Point.X = vpos.x
799 p.Point.Y = vpos.y
800 p.Point.Z = vpos.z
802 # Create the wedge
803 w = VVertex()
804 w.MatIndex = object_material_index
805 w.PointIndex = points.get(p) # get index from map
806 #Set UV TEXTURE
807 w.U = uv[0]
808 w.V = uv[1]
809 index_wedge = wedges.get(w)
810 wedge_list.append(index_wedge)
812 #print results
813 #print 'result PointIndex=%i, U=%f, V=%f, wedge_index=%i' % (
814 # w.PointIndex,
815 # w.U,
816 # w.V,
817 # wedge_index)
819 # Determine face vertex order
820 # get normal from blender
821 no = current_face.normal
823 # TODO: convert to Blender.Mathutils
824 # convert to FVector
825 norm = FVector(no[0], no[1], no[2])
827 # Calculate the normal of the face in blender order
828 tnorm = vect_list[1].sub(vect_list[0]).cross(vect_list[2].sub(vect_list[1]))
830 # RE - dot the normal from blender order against the blender normal
831 # this gives the product of the two vectors' lengths along the blender normal axis
832 # all that matters is the sign
833 dot = norm.dot(tnorm)
835 # print results
836 #print 'face norm: (%f,%f,%f), tnorm=(%f,%f,%f), dot=%f' % (
837 # norm.X, norm.Y, norm.Z,
838 # tnorm.X, tnorm.Y, tnorm.Z,
839 # dot)
841 tri = VTriangle()
842 # RE - magic: if the dot product above > 0, order the vertices 2, 1, 0
843 # if the dot product above < 0, order the vertices 0, 1, 2
844 # if the dot product is 0, then blender's normal is coplanar with the face
845 # and we cannot deduce which side of the face is the outside of the mesh
846 if (dot > 0):
847 (tri.WedgeIndex2, tri.WedgeIndex1, tri.WedgeIndex0) = wedge_list
848 elif (dot < 0):
849 (tri.WedgeIndex0, tri.WedgeIndex1, tri.WedgeIndex2) = wedge_list
850 else:
851 dindex0 = current_face.vertices[0];
852 dindex1 = current_face.vertices[1];
853 dindex2 = current_face.vertices[2];
855 current_mesh.vertices[dindex0].select = True
856 current_mesh.vertices[dindex1].select = True
857 current_mesh.vertices[dindex2].select = True
859 raise RuntimeError("normal vector coplanar with face! points:", current_mesh.vertices[dindex0].co, current_mesh.vertices[dindex1].co, current_mesh.vertices[dindex2].co)
860 #print(dir(current_face))
861 current_face.select = True
862 #print((current_face.use_smooth))
863 #not sure if this right
864 #tri.SmoothingGroups
865 if current_face.use_smooth == True:
866 tri.SmoothingGroups = 1
867 else:
868 tri.SmoothingGroups = 0
870 tri.MatIndex = object_material_index
873 #print(tri)
874 psk_file.AddFace(tri)
876 else:
877 discarded_face_count = discarded_face_count + 1
879 print (" -- Dumping Mesh Points -- LEN:",len(points.dict))
880 for point in points.items():
881 psk_file.AddPoint(point)
882 print (" -- Dumping Mesh Wedge -- LEN:",len(wedges.dict))
883 for wedge in wedges.items():
884 psk_file.AddWedge(wedge)
886 #RG - if we happend upon any non-planar faces above that we've discarded,
887 # just let the user know we discarded them here in case they want
888 # to investigate
890 if discarded_face_count > 0:
891 print ("INFO: Discarded %i non-planar faces." % (discarded_face_count))
893 #RG - walk through the vertex groups and find the indexes into the PSK points array
894 #for them, then store that index and the weight as a tuple in a new list of
895 #verts for the group that we can look up later by bone name, since Blender matches
896 #verts to bones for influences by having the VertexGroup named the same thing as
897 #the bone
899 #vertex group.
900 for bonegroup in bonedata:
901 #print("bone gourp build:",bonegroup.bone)
902 vert_list = []
903 for current_vert in current_mesh.vertices:
904 #print("INDEX V:",current_vert.index)
905 vert_index = current_vert.index
906 for vgroup in current_vert.groups:#vertex groupd id
907 vert_weight = vgroup.weight
908 if(bonegroup.index == vgroup.group):
909 p = VPoint()
910 vpos = current_vert.co * current_obj.matrix_local
911 p.Point.X = vpos.x
912 p.Point.Y = vpos.y
913 p.Point.Z = vpos.z
914 #print(current_vert.co)
915 point_index = points.get(p) #point index
916 v_item = (point_index, vert_weight)
917 vert_list.append(v_item)
918 #bone name, [point id and wieght]
919 #print("Add Vertex Group:",bonegroup.bone, " No. Points:",len(vert_list))
920 psk_file.VertexGroups[bonegroup.bone] = vert_list
922 #unrealtriangulatebool #this will remove the mesh from the scene
923 if (bpy.context.scene.unrealtriangulatebool == True):
924 print("Remove tmp Mesh [ " ,current_obj.name, " ] from scene >" ,(bpy.context.scene.unrealtriangulatebool ))
925 bpy.ops.object.mode_set(mode='OBJECT') # set it in object
926 bpy.context.scene.objects.unlink(current_obj)
928 def make_fquat(bquat):
929 quat = FQuat()
931 #flip handedness for UT = set x,y,z to negative (rotate in other direction)
932 quat.X = -bquat.x
933 quat.Y = -bquat.y
934 quat.Z = -bquat.z
936 quat.W = bquat.w
937 return quat
939 def make_fquat_default(bquat):
940 quat = FQuat()
942 quat.X = bquat.x
943 quat.Y = bquat.y
944 quat.Z = bquat.z
946 quat.W = bquat.w
947 return quat
949 # =================================================================================================
950 # TODO: remove this 1am hack
951 nbone = 0
952 def parse_bone(blender_bone, psk_file, psa_file, parent_id, is_root_bone, parent_matrix, parent_root):
953 global nbone # look it's evil!
954 #print '-------------------- Dumping Bone ---------------------- '
956 #If bone does not have parent that mean it the root bone
957 if blender_bone.parent is None:
958 parent_root = blender_bone
961 child_count = len(blender_bone.children)
962 #child of parent
963 child_parent = blender_bone.parent
965 if child_parent != None:
966 print ("--Bone Name:",blender_bone.name ," parent:" , blender_bone.parent.name, "ID:", nbone)
967 else:
968 print ("--Bone Name:",blender_bone.name ," parent: None" , "ID:", nbone)
970 if child_parent != None:
971 quat_root = blender_bone.matrix
972 quat = make_fquat(quat_root.to_quat())
974 quat_parent = child_parent.matrix.to_quat().inverse()
975 parent_head = child_parent.head * quat_parent
976 parent_tail = child_parent.tail * quat_parent
978 set_position = (parent_tail - parent_head) + blender_bone.head
979 else:
980 # ROOT BONE
981 #This for root
982 set_position = blender_bone.head * parent_matrix #ARMATURE OBJECT Locction
983 rot_mat = blender_bone.matrix * parent_matrix.rotation_part() #ARMATURE OBJECT Rotation
984 #print(dir(rot_mat))
986 quat = make_fquat_default(rot_mat.to_quat())
988 #print ("[[======= FINAL POSITION:", set_position)
989 final_parent_id = parent_id
991 #RG/RE -
992 #if we are not seperated by a small distance, create a dummy bone for the displacement
993 #this is only needed for root bones, since UT assumes a connected skeleton, and from here
994 #down the chain we just use "tail" as an endpoint
995 #if(head.length > 0.001 and is_root_bone == 1):
996 if(0):
997 pb = make_vbone("dummy_" + blender_bone.name, parent_id, 1, FQuat(), tail)
998 psk_file.AddBone(pb)
999 pbb = make_namedbonebinary("dummy_" + blender_bone.name, parent_id, 1, FQuat(), tail, 0)
1000 psa_file.StoreBone(pbb)
1001 final_parent_id = nbone
1002 nbone = nbone + 1
1003 #tail = tail-head
1005 my_id = nbone
1007 pb = make_vbone(blender_bone.name, final_parent_id, child_count, quat, set_position)
1008 psk_file.AddBone(pb)
1009 pbb = make_namedbonebinary(blender_bone.name, final_parent_id, child_count, quat, set_position, 1)
1010 psa_file.StoreBone(pbb)
1012 nbone = nbone + 1
1014 #RG - dump influences for this bone - use the data we collected in the mesh dump phase
1015 # to map our bones to vertex groups
1016 #print("///////////////////////")
1017 #print("set influence")
1018 if blender_bone.name in psk_file.VertexGroups:
1019 vertex_list = psk_file.VertexGroups[blender_bone.name]
1020 #print("vertex list:", len(vertex_list), " of >" ,blender_bone.name )
1021 for vertex_data in vertex_list:
1022 #print("set influence vettex")
1023 point_index = vertex_data[0]
1024 vertex_weight = vertex_data[1]
1025 influence = VRawBoneInfluence()
1026 influence.Weight = vertex_weight
1027 influence.BoneIndex = my_id
1028 influence.PointIndex = point_index
1029 #print ('Adding Bone Influence for [%s] = Point Index=%i, Weight=%f' % (blender_bone.name, point_index, vertex_weight))
1030 #print("adding influence")
1031 psk_file.AddInfluence(influence)
1033 #blender_bone.matrix_local
1034 #recursively dump child bones
1035 mainparent = parent_matrix
1036 #if len(blender_bone.children) > 0:
1037 for current_child_bone in blender_bone.children:
1038 parse_bone(current_child_bone, psk_file, psa_file, my_id, 0, mainparent, parent_root)
1040 def parse_armature(blender_armature, psk_file, psa_file):
1041 print ("----- parsing armature -----")
1042 print ('blender_armature length: %i' % (len(blender_armature)))
1044 #magic 0 sized root bone for UT - this is where all armature dummy bones will attach
1045 #dont increment nbone here because we initialize it to 1 (hackity hackity hack)
1047 #count top level bones first. NOT EFFICIENT.
1048 child_count = 0
1049 for current_obj in blender_armature:
1050 current_armature = current_obj.data
1051 bones = [x for x in current_armature.bones if not x.parent is None]
1052 child_count += len(bones)
1054 for current_obj in blender_armature:
1055 print ("Current Armature Name: " + current_obj.name)
1056 current_armature = current_obj.data
1057 #armature_id = make_armature_bone(current_obj, psk_file, psa_file)
1059 #we dont want children here - only the top level bones of the armature itself
1060 #we will recursively dump the child bones as we dump these bones
1062 bones = [x for x in current_armature.bones if not x.parent is None]
1063 #will ingore this part of the ocde
1065 for current_bone in current_armature.bones: #list the bone. #note this will list all the bones.
1066 if(current_bone.parent is None):
1067 parse_bone(current_bone, psk_file, psa_file, 0, 0, current_obj.matrix_local, None)
1068 break
1070 # get blender objects by type
1071 def get_blender_objects(objects, intype):
1072 return [x for x in objects if x.type == intype]
1074 #strips current extension (if any) from filename and replaces it with extension passed in
1075 def make_filename_ext(filename, extension):
1076 new_filename = ''
1077 extension_index = filename.find('.')
1079 if extension_index == -1:
1080 new_filename = filename + extension
1081 else:
1082 new_filename = filename[0:extension_index] + extension
1084 return new_filename
1086 # returns the quaternion Grassman product a*b
1087 # this is the same as the rotation a(b(x))
1088 # (ie. the same as B*A if A and B are matrices representing
1089 # the rotations described by quaternions a and b)
1090 def grassman(a, b):
1091 return mathutils.Quaternion(
1092 a.w*b.w - a.x*b.x - a.y*b.y - a.z*b.z,
1093 a.w*b.x + a.x*b.w + a.y*b.z - a.z*b.y,
1094 a.w*b.y - a.x*b.z + a.y*b.w + a.z*b.x,
1095 a.w*b.z + a.x*b.y - a.y*b.x + a.z*b.w)
1097 def parse_animation(blender_scene, blender_armatures, psa_file):
1098 #to do list:
1099 #need to list the action sets
1100 #need to check if there animation
1101 #need to check if animation is has one frame then exit it
1102 print ('\n----- parsing animation -----')
1103 render_data = blender_scene.render
1104 bHaveAction = True
1106 anim_rate = render_data.fps
1108 print("==== Blender Settings ====")
1109 print ('Scene: %s Start Frame: %i, End Frame: %i' % (blender_scene.name, blender_scene.frame_start, blender_scene.frame_end))
1110 print ('Frames Per Sec: %i' % anim_rate)
1111 print ("Default FPS: 24" )
1113 cur_frame_index = 0
1115 if bpy.context.scene.unrealactionexportall :#if exporting all actions is ture then go do some work.
1116 print("Exporting all action:",bpy.context.scene.unrealactionexportall)
1117 print("[==== Action list Start====]")
1119 print("Number of Action set(s):",len(bpy.data.actions))
1121 for action in bpy.data.actions:#current number action sets
1122 print("========>>>>>")
1123 print("Action Name:",action.name)
1124 #print("Groups:")
1125 #for bone in action.groups:
1126 #print("> Name: ",bone.name)
1127 #print(dir(bone))
1129 print("[==== Action list End ====]")
1131 amatureobject = None #this is the armature set to none
1132 bonenames = [] #bone name of the armature bones list
1134 for arm in blender_armatures:
1135 amatureobject = arm
1137 print("\n[==== Armature Object ====]")
1138 if amatureobject != None:
1139 print("Name:",amatureobject.name)
1140 print("Number of bones:", len(amatureobject.pose.bones))
1141 for bone in amatureobject.pose.bones:
1142 bonenames.append(bone.name)
1143 print("[=========================]")
1145 for ActionNLA in bpy.data.actions:
1146 print("\n==== Action Set ====")
1147 nobone = 0
1148 baction = True
1149 #print("\nChecking actions matching groups with bone names...")
1150 #Check if the bone names matches the action groups names
1151 for group in ActionNLA.groups:
1152 for abone in bonenames:
1153 #print("name:>>",abone)
1154 if abone == group.name:
1155 nobone += 1
1156 break
1157 #if action groups matches the bones length and names matching the gourps do something
1158 if (len(ActionNLA.groups) == len(bonenames)) and (nobone == len(ActionNLA.groups)):
1159 print("Action Set match: Pass")
1160 baction = True
1161 else:
1162 print("Action Set match: Fail")
1163 print("Action Name:",ActionNLA.name)
1164 baction = False
1166 if baction == True:
1167 arm = amatureobject #set armature object
1168 if not arm.animation_data:
1169 print("======================================")
1170 print("Check Animation Data: None")
1171 print("Armature has no animation, skipping...")
1172 print("======================================")
1173 break
1175 if not arm.animation_data.action:
1176 print("======================================")
1177 print("Check Action: None")
1178 print("Armature has no animation, skipping...")
1179 print("======================================")
1180 break
1181 arm.animation_data.action = ActionNLA
1182 act = arm.animation_data.action
1183 action_name = act.name
1185 if not len(act.fcurves):
1186 print("//===========================================================")
1187 print("// None bone pose set keys for this action set... skipping...")
1188 print("//===========================================================")
1189 bHaveAction = False
1191 #this deal with action export control
1192 if bHaveAction == True:
1194 print("Action Name:",action_name)
1195 #look for min and max frame that current set keys
1196 framemin, framemax = act.frame_range
1197 #print("max frame:",framemax)
1198 start_frame = int(framemin)
1199 end_frame = int(framemax)
1200 scene_frames = range(start_frame, end_frame+1)
1201 frame_count = len(scene_frames)
1202 #===================================================
1203 anim = AnimInfoBinary()
1204 anim.Name = action_name
1205 anim.Group = "" #what is group?
1206 anim.NumRawFrames = frame_count
1207 anim.AnimRate = anim_rate
1208 anim.FirstRawFrame = cur_frame_index
1209 #===================================================
1210 count_previous_keys = len(psa_file.RawKeys.Data)
1211 print("Frame Key Set Count:",frame_count, "Total Frame:",frame_count)
1212 #print("init action bones...")
1213 unique_bone_indexes = {}
1214 # bone lookup table
1215 bones_lookup = {}
1217 #build bone node for animation keys needed to be set
1218 for bone in arm.data.bones:
1219 bones_lookup[bone.name] = bone
1220 #print("bone name:",bone.name)
1221 frame_count = len(scene_frames)
1222 #print ('Frame Count: %i' % frame_count)
1223 pose_data = arm.pose
1225 #these must be ordered in the order the bones will show up in the PSA file!
1226 ordered_bones = {}
1227 ordered_bones = sorted([(psa_file.UseBone(x.name), x) for x in pose_data.bones], key=operator.itemgetter(0))
1229 #############################
1230 # ORDERED FRAME, BONE
1231 #for frame in scene_frames:
1233 for i in range(frame_count):
1234 frame = scene_frames[i]
1235 #LOUD
1236 #print ("==== outputting frame %i ===" % frame)
1238 if frame_count > i+1:
1239 next_frame = scene_frames[i+1]
1240 #print "This Frame: %i, Next Frame: %i" % (frame, next_frame)
1241 else:
1242 next_frame = -1
1243 #print "This Frame: %i, Next Frame: NONE" % frame
1245 #frame start from 1 as number one from blender
1246 blender_scene.frame_set(frame)
1248 cur_frame_index = cur_frame_index + 1
1249 for bone_data in ordered_bones:
1250 bone_index = bone_data[0]
1251 pose_bone = bone_data[1]
1252 #print("[=====POSE NAME:",pose_bone.name)
1254 #print("LENG >>.",len(bones_lookup))
1255 blender_bone = bones_lookup[pose_bone.name]
1257 #just need the total unique bones used, later for this AnimInfoBinary
1258 unique_bone_indexes[bone_index] = bone_index
1259 #LOUD
1260 #print ("-------------------", pose_bone.name)
1261 head = pose_bone.head
1263 posebonemat = mathutils.Matrix(pose_bone.matrix)
1264 parent_pose = pose_bone.parent
1265 if parent_pose != None:
1266 parentposemat = mathutils.Matrix(parent_pose.matrix)
1267 #blender 2.4X it been flip around with new 2.50 (mat1 * mat2) should now be (mat2 * mat1)
1268 posebonemat = parentposemat.invert() * posebonemat
1269 head = posebonemat.translation_part()
1270 quat = posebonemat.to_quat().normalize()
1271 vkey = VQuatAnimKey()
1272 vkey.Position.X = head.x
1273 vkey.Position.Y = head.y
1274 vkey.Position.Z = head.z
1276 if parent_pose != None:
1277 quat = make_fquat(quat)
1278 else:
1279 quat = make_fquat_default(quat)
1281 vkey.Orientation = quat
1282 #print("Head:",head)
1283 #print("Orientation",quat)
1285 #time from now till next frame = diff / framesPerSec
1286 if next_frame >= 0:
1287 diff = next_frame - frame
1288 else:
1289 diff = 1.0
1291 #print ("Diff = ", diff)
1292 vkey.Time = float(diff)/float(anim_rate)
1294 psa_file.AddRawKey(vkey)
1296 #done looping frames
1297 #done looping armatures
1298 #continue adding animInfoBinary counts here
1300 anim.TotalBones = len(unique_bone_indexes)
1301 print("Bones Count:",anim.TotalBones)
1302 anim.TrackTime = float(frame_count) / anim.AnimRate
1303 print("Time Track Frame:",anim.TrackTime)
1304 psa_file.AddAnimation(anim)
1305 print("==== Finish Action Build(s) ====")
1306 else:
1307 print("Exporting one action:",bpy.context.scene.unrealactionexportall)
1308 #list of armature objects
1309 for arm in blender_armatures:
1310 #check if there animation data from armature or something
1312 if not arm.animation_data:
1313 print("======================================")
1314 print("Check Animation Data: None")
1315 print("Armature has no animation, skipping...")
1316 print("======================================")
1317 break
1319 if not arm.animation_data.action:
1320 print("======================================")
1321 print("Check Action: None")
1322 print("Armature has no animation, skipping...")
1323 print("======================================")
1324 break
1325 act = arm.animation_data.action
1326 #print(dir(act))
1327 action_name = act.name
1329 if not len(act.fcurves):
1330 print("//===========================================================")
1331 print("// None bone pose set keys for this action set... skipping...")
1332 print("//===========================================================")
1333 bHaveAction = False
1335 #this deal with action export control
1336 if bHaveAction == True:
1337 print("")
1338 print("==== Action Set ====")
1339 print("Action Name:",action_name)
1340 #look for min and max frame that current set keys
1341 framemin, framemax = act.frame_range
1342 #print("max frame:",framemax)
1343 start_frame = int(framemin)
1344 end_frame = int(framemax)
1345 scene_frames = range(start_frame, end_frame+1)
1346 frame_count = len(scene_frames)
1347 #===================================================
1348 anim = AnimInfoBinary()
1349 anim.Name = action_name
1350 anim.Group = "" #what is group?
1351 anim.NumRawFrames = frame_count
1352 anim.AnimRate = anim_rate
1353 anim.FirstRawFrame = cur_frame_index
1354 #===================================================
1355 count_previous_keys = len(psa_file.RawKeys.Data)
1356 print("Frame Key Set Count:",frame_count, "Total Frame:",frame_count)
1357 #print("init action bones...")
1358 unique_bone_indexes = {}
1359 # bone lookup table
1360 bones_lookup = {}
1362 #build bone node for animation keys needed to be set
1363 for bone in arm.data.bones:
1364 bones_lookup[bone.name] = bone
1365 #print("bone name:",bone.name)
1366 frame_count = len(scene_frames)
1367 #print ('Frame Count: %i' % frame_count)
1368 pose_data = arm.pose
1370 #these must be ordered in the order the bones will show up in the PSA file!
1371 ordered_bones = {}
1372 ordered_bones = sorted([(psa_file.UseBone(x.name), x) for x in pose_data.bones], key=operator.itemgetter(0))
1374 #############################
1375 # ORDERED FRAME, BONE
1376 #for frame in scene_frames:
1378 for i in range(frame_count):
1379 frame = scene_frames[i]
1380 #LOUD
1381 #print ("==== outputting frame %i ===" % frame)
1383 if frame_count > i+1:
1384 next_frame = scene_frames[i+1]
1385 #print "This Frame: %i, Next Frame: %i" % (frame, next_frame)
1386 else:
1387 next_frame = -1
1388 #print "This Frame: %i, Next Frame: NONE" % frame
1390 #frame start from 1 as number one from blender
1391 blender_scene.frame_set(frame)
1393 cur_frame_index = cur_frame_index + 1
1394 for bone_data in ordered_bones:
1395 bone_index = bone_data[0]
1396 pose_bone = bone_data[1]
1397 #print("[=====POSE NAME:",pose_bone.name)
1399 #print("LENG >>.",len(bones_lookup))
1400 blender_bone = bones_lookup[pose_bone.name]
1402 #just need the total unique bones used, later for this AnimInfoBinary
1403 unique_bone_indexes[bone_index] = bone_index
1404 #LOUD
1405 #print ("-------------------", pose_bone.name)
1406 head = pose_bone.head
1408 posebonemat = mathutils.Matrix(pose_bone.matrix)
1409 parent_pose = pose_bone.parent
1410 if parent_pose != None:
1411 parentposemat = mathutils.Matrix(parent_pose.matrix)
1412 #blender 2.4X it been flip around with new 2.50 (mat1 * mat2) should now be (mat2 * mat1)
1413 posebonemat = parentposemat.invert() * posebonemat
1414 head = posebonemat.translation_part()
1415 quat = posebonemat.to_quat().normalize()
1416 vkey = VQuatAnimKey()
1417 vkey.Position.X = head.x
1418 vkey.Position.Y = head.y
1419 vkey.Position.Z = head.z
1421 if parent_pose != None:
1422 quat = make_fquat(quat)
1423 else:
1424 quat = make_fquat_default(quat)
1426 vkey.Orientation = quat
1427 #print("Head:",head)
1428 #print("Orientation",quat)
1430 #time from now till next frame = diff / framesPerSec
1431 if next_frame >= 0:
1432 diff = next_frame - frame
1433 else:
1434 diff = 1.0
1436 #print ("Diff = ", diff)
1437 vkey.Time = float(diff)/float(anim_rate)
1439 psa_file.AddRawKey(vkey)
1441 #done looping frames
1442 #done looping armatures
1443 #continue adding animInfoBinary counts here
1445 anim.TotalBones = len(unique_bone_indexes)
1446 print("Bones Count:",anim.TotalBones)
1447 anim.TrackTime = float(frame_count) / anim.AnimRate
1448 print("Time Track Frame:",anim.TrackTime)
1449 psa_file.AddAnimation(anim)
1450 print("==== Finish Action Build(s) ====")
1452 exportmessage = "Export Finish"
1454 def fs_callback(filename, context):
1455 #this deal with repeat export and the reset settings
1456 global bonedata, BBCount, nbone, exportmessage
1457 bonedata = []#clear array
1458 BBCount = 0
1459 nbone = 0
1461 start_time = time.clock()
1463 print ("========EXPORTING TO UNREAL SKELETAL MESH FORMATS========\r\n")
1464 print("Blender Version:", bpy.app.version_string)
1466 psk = PSKFile()
1467 psa = PSAFile()
1469 #sanity check - this should already have the extension, but just in case, we'll give it one if it doesn't
1470 psk_filename = make_filename_ext(filename, '.psk')
1472 #make the psa filename
1473 psa_filename = make_filename_ext(filename, '.psa')
1475 print ('PSK File: ' + psk_filename)
1476 print ('PSA File: ' + psa_filename)
1478 barmature = True
1479 bmesh = True
1480 blender_meshes = []
1481 blender_armature = []
1482 selectmesh = []
1483 selectarmature = []
1485 current_scene = context.scene
1486 cur_frame = current_scene.frame_current #store current frame before we start walking them during animation parse
1487 objects = current_scene.objects
1489 print("Checking object count...")
1490 for next_obj in objects:
1491 if next_obj.type == 'MESH':
1492 blender_meshes.append(next_obj)
1493 if (next_obj.select):
1494 #print("mesh object select")
1495 selectmesh.append(next_obj)
1496 if next_obj.type == 'ARMATURE':
1497 blender_armature.append(next_obj)
1498 if (next_obj.select):
1499 #print("armature object select")
1500 selectarmature.append(next_obj)
1502 print("Mesh Count:",len(blender_meshes)," Armature Count:",len(blender_armature))
1503 print("====================================")
1504 print("Checking Mesh Condtion(s):")
1505 if len(blender_meshes) == 1:
1506 print(" - One Mesh Scene")
1507 elif (len(blender_meshes) > 1) and (len(selectmesh) == 1):
1508 print(" - One Mesh [Select]")
1509 else:
1510 print(" - Too Many Meshes!")
1511 print(" - Select One Mesh Object!")
1512 bmesh = False
1513 print("====================================")
1514 print("Checking Armature Condtion(s):")
1515 if len(blender_armature) == 1:
1516 print(" - One Armature Scene")
1517 elif (len(blender_armature) > 1) and (len(selectarmature) == 1):
1518 print(" - One Armature [Select]")
1519 else:
1520 print(" - Too Armature Meshes!")
1521 print(" - Select One Armature Object Only!")
1522 barmature = False
1524 if (bmesh == False) or (barmature == False):
1525 exportmessage = "Export Fail! Check Log."
1526 print("=================================")
1527 print("= Export Fail! =")
1528 print("=================================")
1529 else:
1530 exportmessage = "Export Finish!"
1531 #need to build a temp bone index for mesh group vertex
1532 BoneIndexArmature(blender_armature)
1534 try:
1535 #######################
1536 # STEP 1: MESH DUMP
1537 # we build the vertexes, wedges, and faces in here, as well as a vertexgroup lookup table
1538 # for the armature parse
1539 print("//===============================")
1540 print("// STEP 1")
1541 print("//===============================")
1542 parse_meshes(blender_meshes, psk)
1543 except:
1544 context.scene.frame_set(cur_frame) #set frame back to original frame
1545 print ("Exception during Mesh Parse")
1546 raise
1548 try:
1549 #######################
1550 # STEP 2: ARMATURE DUMP
1551 # IMPORTANT: do this AFTER parsing meshes - we need to use the vertex group data from
1552 # the mesh parse in here to generate bone influences
1553 print("//===============================")
1554 print("// STEP 2")
1555 print("//===============================")
1556 parse_armature(blender_armature, psk, psa)
1558 except:
1559 context.scene.frame_set(cur_frame) #set frame back to original frame
1560 print ("Exception during Armature Parse")
1561 raise
1563 try:
1564 #######################
1565 # STEP 3: ANIMATION DUMP
1566 # IMPORTANT: do AFTER parsing bones - we need to do bone lookups in here during animation frames
1567 print("//===============================")
1568 print("// STEP 3")
1569 print("//===============================")
1570 parse_animation(current_scene, blender_armature, psa)
1572 except:
1573 context.scene.frame_set(cur_frame) #set frame back to original frame
1574 print ("Exception during Animation Parse")
1575 raise
1577 # reset current frame
1579 context.scene.frame_set(cur_frame) #set frame back to original frame
1581 ##########################
1582 # FILE WRITE
1583 print("//===========================================")
1584 print("// bExportPsk:",bpy.context.scene.unrealexportpsk," bExportPsa:",bpy.context.scene.unrealexportpsa)
1585 print("//===========================================")
1586 if bpy.context.scene.unrealexportpsk == True:
1587 print("Writing Skeleton Mesh Data...")
1588 #RG - dump psk file
1589 psk.PrintOut()
1590 file = open(psk_filename, "wb")
1591 file.write(psk.dump())
1592 file.close()
1593 print ("Successfully Exported File: " + psk_filename)
1594 if bpy.context.scene.unrealexportpsa == True:
1595 print("Writing Animaiton Data...")
1596 #RG - dump psa file
1597 if not psa.IsEmpty():
1598 psa.PrintOut()
1599 file = open(psa_filename, "wb")
1600 file.write(psa.dump())
1601 file.close()
1602 print ("Successfully Exported File: " + psa_filename)
1603 else:
1604 print ("No Animations (.psa file) to Export")
1606 print ('PSK/PSA Export Script finished in %.2f seconds' % (time.clock() - start_time))
1607 print( "Current Script version: ",bl_addon_info['version'])
1608 #MSG BOX EXPORT COMPLETE
1609 #...
1611 #DONE
1612 print ("PSK/PSA Export Complete")
1614 def write_data(path, context):
1615 print("//============================")
1616 print("// running psk/psa export...")
1617 print("//============================")
1618 fs_callback(path, context)
1619 pass
1621 from bpy.props import *
1623 exporttypedata = []
1625 # [index,text field,0] #or something like that
1626 exporttypedata.append(("0","PSK","Export PSK"))
1627 exporttypedata.append(("1","PSA","Export PSA"))
1628 exporttypedata.append(("2","ALL","Export ALL"))
1630 bpy.types.Scene.unrealfpsrate = IntProperty(
1631 name="fps rate",
1632 description="Set the frame per second (fps) for unreal.",
1633 default=24,min=1,max=100)
1635 bpy.types.Scene.unrealexport_settings = EnumProperty(
1636 name="Export:",
1637 description="Select a export settings (psk/psa/all)...",
1638 items = exporttypedata, default = '0')
1640 bpy.types.Scene.unrealtriangulatebool = BoolProperty(
1641 name="Triangulate Mesh",
1642 description="Convert Quad to Tri Mesh Boolean...",
1643 default=False)
1645 bpy.types.Scene.unrealactionexportall = BoolProperty(
1646 name="All Actions",
1647 description="This let you export all the actions from current armature that matches bone name in action groups names.",
1648 default=False)
1650 bpy.types.Scene.unrealexportpsk = BoolProperty(
1651 name="bool export psa",
1652 description="bool for exporting this psk format",
1653 default=True)
1655 bpy.types.Scene.unrealexportpsa = BoolProperty(
1656 name="bool export psa",
1657 description="bool for exporting this psa format",
1658 default=True)
1660 class ExportUDKAnimData(bpy.types.Operator):
1661 global exportmessage
1662 '''Export Skeleton Mesh / Animation Data file(s)'''
1663 bl_idname = "export.udk_anim_data" # this is important since its how bpy.ops.export.udk_anim_data is constructed
1664 bl_label = "Export PSK/PSA"
1665 __doc__ = "One mesh and one armature else select one mesh or armature to be exported."
1667 # List of operator properties, the attributes will be assigned
1668 # to the class instance from the operator settings before calling.
1670 filepath = StringProperty(name="File Path", description="Filepath used for exporting the PSA file", maxlen= 1024, default= "", subtype='FILE_PATH')
1671 pskexportbool = BoolProperty(name="Export PSK", description="Export Skeletal Mesh", default= True)
1672 psaexportbool = BoolProperty(name="Export PSA", description="Export Action Set (Animation Data)", default= True)
1673 actionexportall = BoolProperty(name="All Actions", description="This will export all the actions that matches the current armature.", default=False)
1675 @classmethod
1676 def poll(cls, context):
1677 return context.active_object != None
1679 def execute(self, context):
1680 #check if skeleton mesh is needed to be exported
1681 if (self.pskexportbool):
1682 bpy.context.scene.unrealexportpsk = True
1683 else:
1684 bpy.context.scene.unrealexportpsk = False
1685 #check if animation data is needed to be exported
1686 if (self.psaexportbool):
1687 bpy.context.scene.unrealexportpsa = True
1688 else:
1689 bpy.context.scene.unrealexportpsa = False
1691 if (self.actionexportall):
1692 bpy.context.scene.unrealactionexportall = True
1693 else:
1694 bpy.context.scene.unrealactionexportall = False
1696 write_data(self.filepath, context)
1698 self.report({'WARNING', 'INFO'}, exportmessage)
1699 return {'FINISHED'}
1701 def invoke(self, context, event):
1702 wm = context.window_manager
1703 wm.fileselect_add(self)
1704 return {'RUNNING_MODAL'}
1706 class VIEW3D_PT_unrealtools_objectmode(bpy.types.Panel):
1707 bl_space_type = "VIEW_3D"
1708 bl_region_type = "TOOLS"
1709 bl_label = "Unreal Tools"
1711 @classmethod
1712 def poll(cls, context):
1713 return context.active_object
1715 def draw(self, context):
1716 layout = self.layout
1717 rd = context.scene
1718 layout.prop(rd, "unrealexport_settings",expand=True)
1719 layout.operator("object.UnrealExport")#button
1720 #FPS #it use the real data from your scene
1721 layout.prop(rd.render, "fps")
1723 layout.prop(rd, "unrealactionexportall")
1724 #row = layout.row()
1725 #row.label(text="Action Set(s)(not build)")
1726 #for action in bpy.data.actions:
1727 #print(dir( action))
1728 #print(action.frame_range)
1729 #row = layout.row()
1730 #row.prop(action, "name")
1732 #print(dir(action.groups[0]))
1733 #for g in action.groups:#those are bones
1734 #print("group...")
1735 #print(dir(g))
1736 #print("////////////")
1737 #print((g.name))
1738 #print("////////////")
1740 #row.label(text="Active:" + action.select)
1741 btrimesh = False
1743 class OBJECT_OT_UnrealExport(bpy.types.Operator):
1744 global exportmessage
1745 bl_idname = "OBJECT_OT_UnrealExport"
1746 bl_label = "Unreal Export"
1747 __doc__ = "Select export setting for .psk/.psa or both."
1749 def invoke(self, context, event):
1750 print("Init Export Script:")
1751 if(int(bpy.context.scene.unrealexport_settings) == 0):
1752 bpy.context.scene.unrealexportpsk = True
1753 bpy.context.scene.unrealexportpsa = False
1754 print("Exporting PSK...")
1755 if(int(bpy.context.scene.unrealexport_settings) == 1):
1756 bpy.context.scene.unrealexportpsk = False
1757 bpy.context.scene.unrealexportpsa = True
1758 print("Exporting PSA...")
1759 if(int(bpy.context.scene.unrealexport_settings) == 2):
1760 bpy.context.scene.unrealexportpsk = True
1761 bpy.context.scene.unrealexportpsa = True
1762 print("Exporting ALL...")
1764 default_path = os.path.splitext(bpy.data.filepath)[0] + ".psk"
1765 fs_callback(default_path, bpy.context)
1767 #self.report({'WARNING', 'INFO'}, exportmessage)
1768 self.report({'INFO'}, exportmessage)
1769 return{'FINISHED'}
1771 def menu_func(self, context):
1772 #bpy.context.scene.unrealexportpsk = True
1773 #bpy.context.scene.unrealexportpsa = True
1774 default_path = os.path.splitext(bpy.data.filepath)[0] + ".psk"
1775 self.layout.operator("export.udk_anim_data", text="Skeleton Mesh / Animation Data (.psk/.psa)").filepath = default_path
1777 def register():
1778 bpy.types.INFO_MT_file_export.append(menu_func)
1780 def unregister():
1781 bpy.types.INFO_MT_file_export.remove(menu_func)
1783 if __name__ == "__main__":
1784 register()