clean-up to simplify maintenance: remove non optimized export. Clean to minimize...
[blender-addons-contrib.git] / io_export_marmalade.py
1 # ***** GPL LICENSE BLOCK *****
2 #
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.
7 #
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.
12 #
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 *****
17
18 # Marmalade SDK is not responsible in any case of the following code.
19 # This Blender add-on is freely shared for the Blender and Marmalade user communities.
20
21
22 bl_info = {
23     "name": "Marmalade Cross-platform Apps (.group)",
24     "author": "Benoit Muller",
25     "version": (0, 5, 4),
26     "blender": (2, 6, 0),
27     "location": "File > Export > Marmalade cross-platform Apps (.group)",
28     "description": "Export Marmalade Format files (.group)",
29     "warning": "",
30     "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/"\
31         "Scripts/Import-Export/Marmalade_Exporter",
32     "tracker_url": "https://projects.blender.org/tracker/index.php?"\
33         "",
34     "category": "Import-Export"}
35
36 import os
37 import shutil
38 from math import radians
39
40 import bpy
41 from mathutils import Matrix
42
43 import mathutils
44 import math
45
46 import datetime
47
48 import subprocess
49
50
51 #Container for the exporter settings
52 class MarmaladeExporterSettings:
53
54     def __init__(self,
55                  context,
56                  FilePath,
57                  CoordinateSystem=1,
58                  FlipNormals=False,
59                  ApplyModifiers=False,
60                  Scale=100,
61                  AnimFPS=30,
62                  ExportVertexColors=True,
63                  ExportMaterialColors=True,
64                  ExportTextures=True,
65                  CopyTextureFiles=True,
66                  ExportArmatures=False,
67                  ExportAnimation=0,
68                  ExportMode=1,
69                  MergeModes=0,
70                  Verbose=False):
71         self.context = context
72         self.FilePath = FilePath
73         self.CoordinateSystem = int(CoordinateSystem)
74         self.FlipNormals = FlipNormals
75         self.ApplyModifiers = ApplyModifiers
76         self.Scale = Scale
77         self.AnimFPS = AnimFPS
78         self.ExportVertexColors = ExportVertexColors
79         self.ExportMaterialColors = ExportMaterialColors
80         self.ExportTextures = ExportTextures
81         self.CopyTextureFiles = CopyTextureFiles
82         self.ExportArmatures = ExportArmatures
83         self.ExportAnimation = int(ExportAnimation)
84         self.ExportMode = int(ExportMode)
85         self.MergeModes = int(MergeModes)
86         self.Verbose = Verbose
87         self.WarningList = []
88
89
90 def ExportMadeWithMarmaladeGroup(Config):
91     print("----------\nExporting to {}".format(Config.FilePath))
92     if Config.Verbose:
93         print("Opening File...")
94     Config.File = open(Config.FilePath, "w")
95
96     if Config.Verbose:
97         print("Done")
98
99     if Config.Verbose:
100         print("writing group header")
101
102     Config.File.write('// Marmalade group file exported from : %s\n' % bpy.data.filepath)
103     Config.File.write('// Exported %s\n' % str(datetime.datetime.now()))
104     Config.File.write("CIwResGroup\n{\n\tname \"%s\"\n" % bpy.path.display_name_from_filepath(Config.FilePath))
105
106     if Config.Verbose:
107         print("Generating Object list for export... (Root parents only)")
108     if Config.ExportMode == 1:
109         Config.ExportList = [Object for Object in Config.context.scene.objects
110                              if Object.type in {'ARMATURE', 'EMPTY', 'MESH'}
111                              and Object.parent is None]
112     else:
113         ExportList = [Object for Object in Config.context.selected_objects
114                       if Object.type in {'ARMATURE', 'EMPTY', 'MESH'}]
115         Config.ExportList = [Object for Object in ExportList
116                              if Object.parent not in ExportList]
117     if Config.Verbose:
118         print("  List: {}\nDone".format(Config.ExportList))
119
120     if Config.Verbose:
121         print("Setting up...")
122
123     if Config.ExportAnimation:
124         if Config.Verbose:
125             print(bpy.context.scene)
126             print(bpy.context.scene.frame_current)
127         CurrentFrame = bpy.context.scene.frame_current
128         #comment because it crashes Blender on some old blend file: bpy.context.scene.frame_current = bpy.context.scene.frame_current
129     if Config.Verbose:
130         print("Done")
131     
132     Config.ObjectList = []
133     if Config.Verbose:
134         print("Writing Objects...")
135     WriteObjects(Config, Config.ExportList)
136     if Config.Verbose:
137         print("Done")
138
139     if Config.Verbose:
140         print("Objects Exported: {}".format(Config.ExportList))
141
142     if Config.ExportAnimation:
143         if Config.Verbose:
144             print("Writing Animation...")
145         WriteKeyedAnimationSet(Config, bpy.context.scene)
146         bpy.context.scene.frame_current = CurrentFrame
147         if Config.Verbose:
148             print("Done")
149     Config.File.write("}\n")
150     CloseFile(Config)
151     print("Finished")
152
153
154 def GetObjectChildren(Parent):
155     return [Object for Object in Parent.children
156             if Object.type in {'ARMATURE', 'EMPTY', 'MESH'}]
157
158
159 #Returns the file path of first image texture from Material.
160 def GetMaterialTextureFullPath(Config, Material):
161     if Material:
162         #Create a list of Textures that have type "IMAGE"
163         ImageTextures = [Material.texture_slots[TextureSlot].texture for TextureSlot in Material.texture_slots.keys() if Material.texture_slots[TextureSlot].texture.type == "IMAGE"]
164         #Refine a new list with only image textures that have a file source
165         TexImages = [Texture.image for Texture in ImageTextures if getattr(Texture.image, "source", "") == "FILE"]
166         ImageFiles = [Texture.image.filepath for Texture in ImageTextures if getattr(Texture.image, "source", "") == "FILE"]
167         if TexImages:
168             filepath = TexImages[0].filepath
169             if TexImages[0].packed_file:
170                 TexImages[0].unpack()
171             if not os.path.exists(filepath):
172                 #try relative path to the blend file
173                 filepath = os.path.dirname(bpy.data.filepath) + filepath
174             #Marmalade doesn't like jpeg/tif so try to convert in png on the fly
175             if (TexImages[0].file_format == 'JPEG' or TexImages[0].file_format == 'TIFF') and os.path.exists(filepath):
176                 marmaladeConvert = os.path.expandvars("%S3E_DIR%\\..\\tools\\ImageMagick\\win32\\convert.exe")
177                 if (os.path.exists(marmaladeConvert)):
178                     srcImagefilepath = filepath
179                     filepath = os.path.splitext(filepath)[0] + '.png'
180                     if Config.Verbose:
181                         print("  /!\\ Converting Texture %s in PNG: %s{}..." % (TexImages[0].file_format, filepath))
182                         print('"%s" "%s" "%s"' % (marmaladeConvert, srcImagefilepath, filepath))
183                     subprocess.call([marmaladeConvert, srcImagefilepath, filepath])
184             return filepath
185     return None
186
187
188 def WriteObjects(Config, ObjectList, geoFile=None, mtlFile=None, GeoModel=None,  bChildObjects=False):
189     Config.ObjectList += ObjectList
190
191     if bChildObjects == False and Config.MergeModes > 0:
192         if geoFile == None:
193             #we merge objects, so use name of group file for the name of Geo
194             geoFile, mtlFile = CreateGeoMtlFiles(Config, bpy.path.display_name_from_filepath(Config.FilePath))
195             GeoModel = CGeoModel(bpy.path.display_name_from_filepath(Config.FilePath))
196
197     for Object in ObjectList:
198         if Config.Verbose:
199             print("  Writing Object: {}...".format(Object.name))
200         
201         if Config.ExportArmatures and Object.type == "ARMATURE":           
202             Armature = Object.data
203             ParentList = [Bone for Bone in Armature.bones if Bone.parent is None]
204             if Config.Verbose:
205                 print("    Writing Armature Bones...")
206             #Create the skel file
207             skelfullname = os.path.dirname(Config.FilePath) + "\models\%s.skel" % (StripName(Object.name))
208             ensure_dir(skelfullname)
209             if Config.Verbose:
210                 print("      Creating skel file %s" % (skelfullname))
211
212             skelFile = open(skelfullname, "w")
213             skelFile.write('// skel file exported from : %r\n' % os.path.basename(bpy.data.filepath))   
214             skelFile.write("CIwAnimSkel\n")
215             skelFile.write("{\n")
216             skelFile.write("\tnumBones %d\n" % (len(Armature.bones)))
217             Config.File.write("\t\".\models\%s.skel\"\n" % (StripName(Object.name)))
218
219             WriteArmatureParentRootBones(Config, Object, ParentList, skelFile)
220
221             skelFile.write("}\n")
222             skelFile.close()
223             if Config.Verbose:
224                 print("    Done")
225
226         ChildList = GetObjectChildren(Object)
227         if Config.ExportMode == 2:  # Selected Objects Only
228             ChildList = [Child for Child in ChildList
229                          if Child in Config.context.selected_objects]
230         if Config.Verbose:
231             print("    Writing Children...")
232         WriteObjects(Config, ChildList, geoFile, mtlFile, GeoModel, True)
233         if Config.Verbose:
234             print("    Done Writing Children")
235
236         if Object.type == "MESH":
237             if Config.Verbose:
238                 print("    Generating Mesh...")
239             if Config.ApplyModifiers:
240                 if Config.ExportArmatures:
241                     #Create a copy of the object and remove all armature modifiers so an unshaped
242                     #mesh can be created from it.
243                     Object2 = Object.copy()
244                     for Modifier in [Modifier for Modifier in Object2.modifiers if Modifier.type == "ARMATURE"]:
245                         Object2.modifiers.remove(Modifier)
246                     Mesh = Object2.to_mesh(bpy.context.scene, True, "PREVIEW")
247                 else:
248                     Mesh = Object.to_mesh(bpy.context.scene, True, "PREVIEW")
249             else:
250                 Mesh = Object.to_mesh(bpy.context.scene, False, "PREVIEW")
251             if Config.Verbose:
252                 print("    Done")
253                 print("    Writing Mesh...")
254
255             # Flip ZY axis (Blender Z up: Marmalade: Y up) ans Scale appropriately
256             X_ROT = mathutils.Matrix.Rotation(-math.pi / 2, 4, 'X')
257
258             if Config.MergeModes == 0:
259                 # No merge, so all objects are exported in MODEL SPACE and not in world space
260                 # Calculate Scale of the Export
261                 meshScale = Object.matrix_world.to_scale()  # Export is working, even if user doesn't have use apply scale in Edit mode.
262
263                 scalematrix = Matrix()
264                 scalematrix[0][0] = meshScale.x * Config.Scale
265                 scalematrix[1][1] = meshScale.y * Config.Scale
266                 scalematrix[2][2] = meshScale.z * Config.Scale
267
268                 Mesh.transform(scalematrix * X_ROT)
269             else:
270                 # In Merge mode, we need to keep relative postion of each objects, so we export in WORLD SPACE
271                 SCALE_MAT = mathutils.Matrix.Scale(Config.Scale, 4)
272                 Mesh.transform(SCALE_MAT * X_ROT * Object.matrix_world)
273
274              # manage merge options
275    
276             if Config.MergeModes == 0:
277                 #one geo per Object, so use name of Object for the Geo file
278                 geoFile, mtlFile = CreateGeoMtlFiles(Config, StripName(Object.name))
279                 GeoModel = CGeoModel(StripName(Object.name))  
280                 
281             # Write the Mesh in the Geo file   
282             WriteMesh(Config, Object, Mesh, geoFile, mtlFile, GeoModel)
283
284             if Config.MergeModes == 0:
285                 # no merge so finalize the file, and discard the file and geo class
286                 FinalizeGeoMtlFiles(Config, geoFile, mtlFile)
287                 geoFile = None
288                 mtlFile = None
289                 GeoModel = None
290             elif Config.MergeModes == 1:
291                 # merge in one Mesh, so keep the Geo class and prepare to change object
292                 GeoModel.NewObject() 
293             elif Config.MergeModes == 2:
294                 # merge several Meshes in one file: so clear the mesh data that we just written in the file,
295                 # but keep Materials info that need to be merged across objects
296                 GeoModel.ClearAllExceptMaterials()
297
298             if Config.Verbose:
299                 print("    Done")
300
301             if Config.ApplyModifiers and Config.ExportArmatures:
302                 bpy.data.objects.remove(Object2)
303             bpy.data.meshes.remove(Mesh)
304
305         if Config.Verbose:
306             print("  Done Writing Object: {}".format(Object.name))
307
308     if bChildObjects == False:
309         # we have finish to do all objects
310         if GeoModel:
311             if Config.MergeModes == 1:
312                 # we have Merges all objects in one Mesh, so time to write this big mesh in the file
313                 GeoModel.PrintGeoMesh(geoFile)
314                 # time to write skinfile if any
315                 if len(GeoModel.useBonesDict) > 0:
316                     # some mesh was not modified by the armature. so we must skinned the merged mesh.
317                     # So unskinned vertices from unarmatured meshes, are assigned to the root bone of the armature
318                     for i in range(0, len(GeoModel.vList)):
319                         if not i in GeoModel.skinnedVertices:
320                             GeoModel.skinnedVertices.append(i)
321                             useBonesKey = pow(2, GeoModel.armatureRootBoneIndex)
322                             vertexGroupIndices = list((GeoModel.armatureRootBoneIndex,))
323                             if useBonesKey not in GeoModel.useBonesDict:                          
324                                 GeoModel.mapVertexGroupNames[GeoModel.armatureRootBoneIndex] = StripBoneName(GeoModel.armatureRootBone.name)
325                                 VertexList = []
326                                 VertexList.append("\t\tvertWeights { %d, 1.0}" % i)
327                                 GeoModel.useBonesDict[useBonesKey] = (vertexGroupIndices, VertexList)
328                             else:
329                                 pair_ListGroupIndices_ListAssignedVertices = GeoModel.useBonesDict[useBonesKey]
330                                 pair_ListGroupIndices_ListAssignedVertices[1].append("\t\tvertWeights { %d, 1.0}" % i)
331                                 GeoModel.useBonesDict[useBonesKey] = pair_ListGroupIndices_ListAssignedVertices
332                     # now generates the skin file
333                     PrintSkinWeights(Config, GeoModel.armatureObjectName, GeoModel.useBonesDict, GeoModel.mapVertexGroupNames, GeoModel.name)
334             if Config.MergeModes > 0:
335                 WriteMeshMaterialsForGeoModel(Config, mtlFile, GeoModel)
336                 FinalizeGeoMtlFiles(Config, geoFile, mtlFile)
337         geoFile = None
338         mtlFile = None
339         GeoModel = None
340
341
342 def CreateGeoMtlFiles(Config, Name):
343     #Create the geo file
344     geofullname = os.path.dirname(Config.FilePath) + ("\models\%s.geo" % Name)
345     ensure_dir(geofullname)
346     if Config.Verbose:
347         print("      Creating geo file %s" % (geofullname))  
348     geoFile = open(geofullname, "w")
349     geoFile.write('// geo file exported from : %r\n' % os.path.basename(bpy.data.filepath))
350     geoFile.write("CIwModel\n")
351     geoFile.write("{\n")
352     geoFile.write("\tname \"%s\"\n" % Name)
353     # add it to the group
354     Config.File.write("\t\".\models\%s.geo\"\n" % Name)
355
356     # Create the mtl file
357     mtlfullname = os.path.dirname(Config.FilePath) + "\models\%s.mtl" % (Name)
358     ensure_dir(mtlfullname)
359     if Config.Verbose:
360         print("      Creating mtl file %s" % (mtlfullname))
361     mtlFile = open(mtlfullname, "w")
362     mtlFile.write('// mtl file exported from : %r\n' % os.path.basename(bpy.data.filepath))   
363     return geoFile, mtlFile
364
365
366 def FinalizeGeoMtlFiles(Config, geoFile, mtlFile):
367     if Config.Verbose:
368         print("      Closing geo file")  
369     geoFile.write("}\n")
370     geoFile.close()
371     if Config.Verbose:
372         print("      Closing mtl file")  
373     mtlFile.close()
374
375
376 def WriteMesh(Config, Object, Mesh,  geoFile=None, mtlFile=None, GeoModel=None):
377     if geoFile == None or mtlFile == None:
378         print (" ERROR not geo file arguments in WriteMesh method")
379         return
380
381     if GeoModel == None:
382         print (" ERROR not GeoModel arguments in WriteMesh method")
383         return
384
385     BuildOptimizedGeo(Config, Object, Mesh, GeoModel)
386     if Config.MergeModes == 0 or Config.MergeModes == 2:
387         #if we don't merge, or if we write several meshes into one file ... write the mesh everytime we do an object
388         GeoModel.PrintGeoMesh(geoFile)
389  
390     if Config.Verbose:
391         print("      Done\n      Writing Mesh Materials...")
392
393     if Config.MergeModes == 0:
394         #No merge, so we can diretly write the Mtl file associated to this object
395         WriteMeshMaterialsForGeoModel(Config, mtlFile, GeoModel)
396
397     if Config.Verbose:
398         print("      Done")
399   
400     if Config.ExportArmatures:
401         if Config.Verbose:
402             print("      Writing Mesh Weights...")
403         WriteMeshSkinWeightsForGeoModel(Config, Object, Mesh, GeoModel)
404         if Config.Verbose:
405             print("      Done")
406
407
408 ###### optimized version fo Export, can be used also to merge several object in one single geo File ######
409
410 # CGeoModel
411 #  -> List Vertices
412 #  -> List Normales
413 #  -> List uv 0
414 #  -> List uv 1
415 #  -> List Vertex Colors
416 #  -> List Materials
417 #       -> Material name
418 #       -> Blender Material Object
419 #       -> List Tris -> Stream Indices v,vn,uv0,uv1,vc
420 #       -> List Quads -> Stream Indices v,vn,uv0,uv1,vc
421
422
423 #############
424 #Store one Point of a Quad or Tri in marmalade geo format: //index-list is: { <int> <int> <int> <int> <int> }   //v,vn,uv0,uv1,vc
425 #############                           
426 class CGeoIndexList:
427     __slots__ = "v", "vn", "uv0", "uv1", "vc"
428     
429     def __init__(self, v, vn, uv0, uv1, vc):
430         self.v = v
431         self.vn = vn
432         self.uv0 = uv0
433         self.uv1 = uv1
434         self.vc = vc
435
436         
437 #############
438 #Store a Quad or a Tri in marmalade geo format : 3 or 4 CIndexList depending it is a Tri or a Quad
439 #############                        
440 class CGeoPoly:
441     __slots__ = "pointsList",
442     
443     def __init__(self):
444         self.pointsList = []
445
446     def AddPoint(self, v, vn, uv0, uv1, vc):
447         self.pointsList.append( CGeoIndexList(v, vn, uv0, uv1, vc))
448
449     def PointsCount(self):
450         return len(self.pointsList)
451
452     def PrintPoly(self, geoFile):
453         if len(self.pointsList) == 3:
454             geoFile.write("\t\t\t\tt ")
455         if len(self.pointsList) == 4:
456             geoFile.write("\t\t\t\tq ")
457         for point in self.pointsList:
458             geoFile.write(" {%d, %d, %d, %d, %d}" % (point.v, point.vn, point.uv0, point.uv1, point.vc))
459         geoFile.write("\n")
460
461
462 #############
463 #Store all the poly (tri or quad) assigned to a Material in marmalade geo format
464 #############                        
465 class CGeoMaterialPolys:
466     __slots__ = "name", "material", "quadList", "triList", "currentPoly"
467     
468     def __init__(self, name, material=None):
469         self.name = name
470         self.material = material
471         self.quadList = []
472         self.triList = []
473         self.currentPoly = None
474
475     def BeginPoly(self):
476         self.currentPoly = CGeoPoly()
477
478     def AddPoint(self, v, vn, uv0, uv1, vc):
479         self.currentPoly.AddPoint(v, vn, uv0, uv1, vc)       
480              
481     def EndPoly(self):
482         if (self.currentPoly.PointsCount() == 3):
483             self.triList.append(self.currentPoly)
484         if (self.currentPoly.PointsCount() == 4):
485             self.quadList.append(self.currentPoly)
486         self.currentPoly = None
487
488     def ClearPolys(self):
489         self.quadList = []
490         self.triList = []
491         self.currentPoly = None
492
493     def PrintMaterialPolys(self, geoFile):
494         geoFile.write("\t\tCSurface\n")
495         geoFile.write("\t\t{\n")
496         geoFile.write("\t\t\tmaterial \"%s\"\n" % self.name)
497         if self.triList:
498             geoFile.write("\t\t\tCTris\n")
499             geoFile.write("\t\t\t{\n")
500             geoFile.write("\t\t\t\tnumTris %d\n" % (len(self.triList)))
501             for poly in self.triList:
502                 poly.PrintPoly(geoFile)
503             geoFile.write("\t\t\t}\n")
504
505         if self.quadList:
506             geoFile.write("\t\t\tCQuads\n")
507             geoFile.write("\t\t\t{\n")
508             geoFile.write("\t\t\t\tnumQuads %d\n" % (len(self.quadList)))
509             for poly in self.quadList:
510                 poly.PrintPoly(geoFile)
511             geoFile.write("\t\t\t}\n")
512         geoFile.write("\t\t}\n")
513
514
515 #############
516 #Store all the information on a Model/Mesh (vertices, normal, certcies color, uv0, uv1, TRI, QUAD) in marmalade geo format
517 #############  
518 class CGeoModel:
519     __slots__ = ("name", "MaterialsDict", "vList", "vnList", "vcList", "uv0List", "uv1List",
520                 "currentMaterialPolys", "vbaseIndex","vnbaseIndex", "uv0baseIndex", "uv1baseIndex",
521                 "armatureObjectName", "useBonesDict", "mapVertexGroupNames", "armatureRootBone", "armatureRootBoneIndex", "skinnedVertices")
522                 
523     def __init__(self, name):
524         self.name = name
525         self.MaterialsDict = {}
526         self.vList = []
527         self.vnList = []
528         self.vcList = []
529         self.uv0List = []
530         self.uv1List = []
531         self.currentMaterialPolys = None
532         #used xx baseIndex are used when merging several blender objects into one Mesh in the geo file (internal offset)
533         self.vbaseIndex = 0
534         self.vnbaseIndex = 0
535         self.uv0baseIndex = 0
536         self.uv1baseIndex = 0
537
538         # Store some information for skin management , when we merge several object in one big mesh (MergeModes 1)
539         # can only work if in the object list only one is rigged with an armature... and if it is located in 0,0,0
540         self.armatureObjectName = ""
541         #useBonesKey : bit field, where each bit is a VertexGroup.Index): Sum(2^VertGroupIndex).
542         #useBonesDict[useBonesKey] = tuple(VertexGroups.group, list(Vertex))
543         self.useBonesDict = {}
544         self.mapVertexGroupNames = {}
545         self.armatureRootBone = None
546         self.armatureRootBoneIndex = 0
547         self.skinnedVertices = []
548
549
550
551     def AddVertex(self, vertex):
552         self.vList.append(vertex.copy())
553
554     def AddVertexNormal(self, vertexN):
555         self.vnList.append(vertexN.copy())
556
557     # add a uv coordiantes and return the current Index in the stream (index is local to the object, when we merge several object into a one Mesh)
558     def AddVertexUV0(self, u, v):
559         self.uv0List.append((u, v))
560         return len(self.uv0List) - 1 - self.uv0baseIndex 
561
562     def AddVertexUV1(self, u, v):
563         self.uv1List.append((u, v))
564         return len(self.uv1List) - 1 - self.uv1baseIndex 
565
566     # add a vertexcolor if it doesn't already exist and return the current Index in the stream (index is global to all objects, when we merge several object into a one Mesh)
567     def AddVertexColor(self, r, g, b, a):
568         for i in range(0, len(self.vcList)):
569             col = self.vcList[i]
570             if col[0] == r and col[1] == g and col[2] == b and col[3] == a:
571                 return i
572
573         self.vcList.append((r, g, b, a))
574         return len(self.vcList)-1
575
576     def BeginPoly(self, MaterialName, material=None):
577         if MaterialName not in self.MaterialsDict:
578             self.currentMaterialPolys = CGeoMaterialPolys(MaterialName, material)
579         else:
580             self.currentMaterialPolys = self.MaterialsDict[MaterialName]
581         self.currentMaterialPolys.BeginPoly()
582
583     def AddPoint(self, v, vn, uv0, uv1, vc):
584         if v != -1:
585             v += self.vbaseIndex
586         if vn != -1:
587             vn += self.vnbaseIndex
588         if uv0 != -1:
589             uv0 += self.uv0baseIndex
590         if uv1 != -1:
591             uv1 += self.uv1baseIndex
592                 
593         self.currentMaterialPolys.AddPoint(v, vn, uv0, uv1, vc)       
594                               
595     def EndPoly(self):
596         self.currentMaterialPolys.EndPoly()
597         self.MaterialsDict[self.currentMaterialPolys.name] = self.currentMaterialPolys
598         self.currentMaterialPolys = None
599
600     def NewObject(self):
601         #used in Merge mode 1: allows to merge several blender objects into one Mesh.
602         self.vbaseIndex = len(self.vList)
603         self.vnbaseIndex = len(self.vnList)
604         self.uv0baseIndex = len(self.uv0List)
605         self.uv1baseIndex = len(self.uv1List)
606
607     def ClearAllExceptMaterials(self):
608         #used in Merge mode 2: one geo with several mesh
609         self.vList = []
610         self.vnList = []
611         self.vcList = []
612         self.uv0List = []
613         self.uv1List = []
614         self.currentMaterialPolys = None
615         self.vbaseIndex = 0
616         self.vnbaseIndex = 0
617         self.uv0baseIndex = 0
618         self.uv1baseIndex = 0
619         for GeoMaterialPolys in self.MaterialsDict.values():
620             GeoMaterialPolys.ClearPolys()
621         self.useBonesDict = {}
622         self.mapVertexGroupNames = {}
623         self.armatureObjectName = ""
624         self.armatureRootBone = None
625         self.armatureRootBoneIndex = 0
626         self.skinnedVertices = []
627
628     def PrintGeoMesh(self, geoFile):
629         geoFile.write("\tCMesh\n")
630         geoFile.write("\t{\n")
631         geoFile.write("\t\tname \"%s\"\n" % (StripName(self.name)))
632
633         if self.vList:
634             geoFile.write("\t\tCVerts\n")
635             geoFile.write("\t\t{\n")
636             geoFile.write("\t\t\tnumVerts %d\n" % len(self.vList))
637             for vertex in self.vList:
638                 geoFile.write("\t\t\tv { %.9f, %.9f, %.9f }\n" % (vertex[0], vertex[1], vertex[2]))                      
639             geoFile.write("\t\t}\n")
640
641         if self.vnList:
642             geoFile.write("\t\tCVertNorms\n")
643             geoFile.write("\t\t{\n")
644             geoFile.write("\t\t\tnumVertNorms  %d\n" % len(self.vnList))
645             for vertexn in self.vnList:
646                 geoFile.write("\t\t\tvn { %.9f, %.9f, %.9f }\n" % (vertexn[0], vertexn[1], vertexn[2]))                      
647             geoFile.write("\t\t}\n")
648
649         if self.vcList:
650             geoFile.write("\t\tCVertCols\n")
651             geoFile.write("\t\t{\n")
652             geoFile.write("\t\t\tnumVertCols %d\n" % len(self.vcList))
653             for color in self.vcList:
654                 geoFile.write("\t\t\tcol { %.6f, %.6f, %.6f, %.6f }\n" % (color[0], color[1], color[2], color[3])) #alpha is not supported on blender for vertex colors           
655             geoFile.write("\t\t}\n")
656
657         if self.uv0List:
658             geoFile.write("\t\tCUVs\n")
659             geoFile.write("\t\t{\n")
660             geoFile.write("\t\t\tsetID 0\n")
661             geoFile.write("\t\t\tnumUVs %d\n" % len(self.uv0List))
662             for uv in self.uv0List:
663                  geoFile.write("\t\t\tuv { %.9f, %.9f }\n" % (uv[0], uv[1]))                       
664             geoFile.write("\t\t}\n")
665
666         if self.uv1List:
667             geoFile.write("\t\tCUVs\n")
668             geoFile.write("\t\t{\n")
669             geoFile.write("\t\t\tsetID 1\n")
670             geoFile.write("\t\t\tnumUVs %d\n" % len(self.uv1List))
671             for uv in self.uv1List:
672                  geoFile.write("\t\t\tuv { %.9f, %.9f }\n" % (uv[0], uv[1]))                       
673             geoFile.write("\t\t}\n")
674
675         for GeoMaterialPolys in self.MaterialsDict.values():
676             GeoMaterialPolys.PrintMaterialPolys(geoFile)
677         geoFile.write("\t}\n")
678
679     def GetMaterialList(self):
680         return list(self.MaterialsDict.keys())
681
682     def GetMaterialByName(self, name):
683         if name in self.MaterialsDict:
684             return self.MaterialsDict[name].material
685         else:
686             return None       
687
688
689
690 #############
691 # iterates faces, vertices ... and store the information in the GeoModel container
692 def BuildOptimizedGeo(Config, Object, Mesh, GeoModel):
693     if GeoModel == None:
694         GeoModel = CGeoModel(filename, Object.name)
695
696     #Store Vertex stream, and Normal stream (use directly the order from blender collection
697     for Vertex in Mesh.vertices:
698         GeoModel.AddVertex(Vertex.co)
699         Normal = Vertex.normal
700         if Config.FlipNormals:
701             Normal = -Normal
702         GeoModel.AddVertexNormal(Normal)
703     #Check if some colors have been defined
704     vertexColours = None
705     if Config.ExportVertexColors and (len(Mesh.vertex_colors) > 0):
706         vertexColours = Mesh.vertex_colors[0].data
707
708     #Check if some uv coordinates have been defined
709     UVCoordinates = None
710     if Config.ExportTextures and (len(Mesh.uv_textures) > 0):
711         for UV in Mesh.uv_textures:
712             if UV.active_render:
713                 UVCoordinates = UV.data
714                 break
715
716     #Iterate on Faces and Store the poly (quad or tri) and the associate colors,UVs
717     for Face in Mesh.faces:
718         # stream for vertex (we use the same for normal)
719         Vertices = list(Face.vertices)
720         if Config.CoordinateSystem == 1:
721             Vertices = Vertices[::-1]
722         # stream for vertex colors
723         if vertexColours:
724             MeshColor = vertexColours[Face.index]
725             if len(Vertices) == 3:
726                 FaceColors = list((MeshColor.color1, MeshColor.color2, MeshColor.color3))
727             else:
728                 FaceColors = list((MeshColor.color1, MeshColor.color2, MeshColor.color3, MeshColor.color4))
729             if Config.CoordinateSystem == 1:
730                 FaceColors = FaceColors[::-1]
731             colorIndex = []
732             for color in FaceColors:
733                 index = GeoModel.AddVertexColor(color[0], color[1], color[2], 1)  #rgba => no alpha on vertex color in Blender so use 1
734                 colorIndex.append(index)
735         else:
736             colorIndex = list((-1,-1,-1,-1))
737
738         # stream for UV0 coordinates
739         if UVCoordinates:
740             uvFace = UVCoordinates[Face.index]
741             uvVertices = []
742             for uvVertex in uvFace.uv:
743                 uvVertices.append(tuple(uvVertex))
744             if Config.CoordinateSystem == 1:
745                 uvVertices = uvVertices[::-1]
746             uv0Index = []
747             for uvVertex in uvVertices:
748                 index = GeoModel.AddVertexUV0(uvVertex[0], 1 - uvVertex[1]) 
749                 uv0Index.append(index)
750         else:
751             uv0Index = list((-1, -1, -1, -1))
752
753         # stream for UV1 coordinates
754         uv1Index = list((-1, -1, -1, -1))
755
756         mat = None
757         # find the associated material
758         if Face.material_index < len(Mesh.materials):
759             mat = Mesh.materials[Face.material_index]
760         if mat:
761             matName =  mat.name
762         else:
763             matName = "NoMaterialAssigned"  # There is no material assigned in blender !!!, exporter have generated a default one          
764             
765         # now on the material, generates the tri/quad in v,vn,uv0,uv1,vc stream index
766         GeoModel.BeginPoly(matName, mat)
767
768         for i in range(0, len(Vertices)):
769             GeoModel.AddPoint(Vertices[i], Vertices[i], uv0Index[i], uv1Index[i], colorIndex[i])
770
771         GeoModel.EndPoly()
772
773
774                               
775 #############
776 # Get the list of Material in use by the CGeoModel
777 def WriteMeshMaterialsForGeoModel(Config, mtlFile, GeoModel):
778     for matName in GeoModel.GetMaterialList():
779         Material = GeoModel.GetMaterialByName(matName)
780         WriteMaterial(Config, mtlFile, Material)
781
782
783 def WriteMaterial(Config, mtlFile, Material=None):
784     mtlFile.write("CIwMaterial\n")
785     mtlFile.write("{\n")
786     if Material:
787         mtlFile.write("\tname \"%s\"\n" % Material.name)
788
789         if Config.ExportMaterialColors:
790             #if bpy.context.scene.world:
791             #    MatAmbientColor = Material.ambient * bpy.context.scene.world.ambient_color
792             MatAmbientColor = Material.ambient * Material.diffuse_color
793             mtlFile.write("\tcolAmbient {%.2f,%.2f,%.2f,%.2f} \n" % (MatAmbientColor[0] * 255, MatAmbientColor[1] * 255, MatAmbientColor[2] * 255, Material.alpha * 255))
794             MatDiffuseColor = Material.diffuse_intensity * Material.diffuse_color
795             mtlFile.write("\tcolDiffuse  {%.2f,%.2f,%.2f} \n" % (MatDiffuseColor * 255)[:])
796             MatSpecularColor = Material.specular_intensity * Material.specular_color
797             mtlFile.write("\tcolSpecular  {%.2f,%.2f,%.2f} \n" % (MatSpecularColor * 255)[:])
798             # EmitColor = Material.emit * Material.diffuse_color
799             # mtlFile.write("\tcolEmissive {%.2f,%.2f,%.2f} \n" % (EmitColor* 255)[:])    
800     else:
801         mtlFile.write("\tname \"NoMaterialAssigned\" // There is no material assigned in blender !!!, exporter have generated a default one\n")
802
803     #Copy texture
804     if Config.ExportTextures:
805         Texture = GetMaterialTextureFullPath(Config, Material)
806         if Texture:
807             mtlFile.write("\ttexture0 .\\textures\\%s\n" % (bpy.path.basename(Texture)))
808             
809             if Config.CopyTextureFiles:
810                 if not os.path.exists(Texture):
811                     #try relative path to the blend file
812                     Texture = os.path.dirname(bpy.data.filepath) + Texture
813                 if os.path.exists(Texture):
814                     textureDest = os.path.dirname(Config.FilePath) + "\\models\\textures\\%s" % (bpy.path.basename(Texture))
815                     ensure_dir(textureDest)
816                     if Config.Verbose:
817                         print("      Copying the texture file %s ---> %s" % (Texture, textureDest))
818                     shutil.copy(Texture, textureDest)
819                 else:
820                     if Config.Verbose:
821                         print("      CANNOT Copy texture file (not found) %s" % (Texture))
822     mtlFile.write("}\n")
823
824 def GetFirstRootBone(ArmatureObject):
825     ArmatureBones = ArmatureObject.data.bones
826     ParentBoneList = [Bone for Bone in ArmatureBones if Bone.parent is None]
827     if ParentBoneList:
828         return ParentBoneList[0]
829     return None
830
831
832 def GetVertexGroupFromBone(Object, Bone):
833     if Bone:
834         rootBoneList = [VertexGroup for VertexGroup in Object.vertex_groups  if VertexGroup.name == Bone.name]
835         if rootBoneList:
836             return rootBoneList[0]
837     return None
838
839
840 def FindUniqueIndexForRootBone(Object, RootVertexGroup):
841     if RootVertexGroup:
842         return RootVertexGroup.index
843     else:
844         #If there is not VertexGroup associated to the root bone name, we don't have a vertex index.
845         #so use the next available free index
846         return len(Object.vertex_groups)
847
848          
849 def WriteMeshSkinWeightsForGeoModel(Config, Object, Mesh, GeoModel):
850     ArmatureList = [Modifier for Modifier in Object.modifiers if Modifier.type == "ARMATURE"]
851     if ArmatureList:
852         ArmatureObject = ArmatureList[0].object
853         if ArmatureObject is None:
854             return
855         RootBone = GetFirstRootBone(ArmatureObject)
856         RootVertexGroup = GetVertexGroupFromBone(Object, RootBone)
857
858         GeoModel.armatureObjectName = StripName(ArmatureObject.name)
859         if RootBone:
860             GeoModel.armatureRootBone = RootBone
861             GeoModel.armatureRootBoneIndex = FindUniqueIndexForRootBone(Object, RootVertexGroup)
862
863         # Marmalade need to declare a vertex per list of affected bones
864         # so first we have to get all the combinations of affected bones that exist in the mesh
865         # to build thoses groups, we build a unique key (like a bit field, where each bit is a VertexGroup.Index): Sum(2^VertGroupIndex)... so we have a unique Number per combinations
866         
867         for Vertex in Mesh.vertices:
868             VertexIndex = Vertex.index + GeoModel.vbaseIndex
869             AddVertexToDicionarySkinWeights(Config, Object, Mesh, Vertex, GeoModel.useBonesDict, GeoModel.mapVertexGroupNames, VertexIndex, RootBone, RootVertexGroup)
870             GeoModel.skinnedVertices.append(VertexIndex)
871
872         if Config.MergeModes != 1:
873             # write skin file directly
874             PrintSkinWeights(Config, GeoModel.armatureObjectName, GeoModel.useBonesDict, GeoModel.mapVertexGroupNames, StripName(Object.name))
875
876
877 def PrintSkinWeights(Config, ArmatureObjectName, useBonesDict, mapVertexGroupNames, GeoName):        
878         #Create the skin file
879         skinfullname = os.path.dirname(Config.FilePath) + "\models\%s.skin" % GeoName
880         ensure_dir(skinfullname)
881         if Config.Verbose:
882             print("      Creating skin file %s" % (skinfullname))
883         skinFile = open(skinfullname, "w")
884         skinFile.write('// skin file exported from : %r\n' % os.path.basename(bpy.data.filepath))   
885         skinFile.write("CIwAnimSkin\n")
886         skinFile.write("{\n")
887         skinFile.write("\tskeleton \"%s\"\n" % ArmatureObjectName)
888         skinFile.write("\tmodel \"%s\"\n" % GeoName)
889
890         # now we have Bones grouped in the dictionary , along with the associated influenced vertex weighting
891         # So simply iterate the dictionary
892         Config.File.write("\t\".\models\%s.skin\"\n" % GeoName)
893         for pair_ListGroupIndices_ListAssignedVertices in useBonesDict.values():
894             skinFile.write("\tCIwAnimSkinSet\n")
895             skinFile.write("\t{\n")
896             skinFile.write("\t\tuseBones {")
897             for vertexGroupIndex in pair_ListGroupIndices_ListAssignedVertices[0]:
898                 skinFile.write(" %s" % mapVertexGroupNames[vertexGroupIndex])
899             skinFile.write(" }\n")
900             skinFile.write("\t\tnumVerts %d\n" % len(pair_ListGroupIndices_ListAssignedVertices[1]))
901             for VertexWeightString in pair_ListGroupIndices_ListAssignedVertices[1]:
902                 skinFile.write(VertexWeightString)
903             skinFile.write("\t}\n")
904
905         skinFile.write("}\n")
906         skinFile.close()
907
908
909 def AddVertexToDicionarySkinWeights(Config, Object, Mesh, Vertex, useBonesDict, mapVertexGroupNames, VertexIndex, RootBone, RootVertexGroup):
910     #build useBones
911     useBonesKey = 0
912     vertexGroupIndices = []
913     weightTotal = 0.0
914     if (len(Vertex.groups)) > 4:
915         print ("ERROR Vertex %d is influenced by more than 4 bones\n" % (VertexIndex))
916     for VertexGroup in Vertex.groups:
917         if (VertexGroup.weight > 0):
918             mapVertexGroupNames[VertexGroup.group] = StripBoneName(Object.vertex_groups[VertexGroup.group].name)
919             if (len(vertexGroupIndices))<4:  #ignore if more 4 bones are influencing the vertex
920                 useBonesKey = useBonesKey + pow(2, VertexGroup.group)
921                 vertexGroupIndices.append(VertexGroup.group)
922                 weightTotal = weightTotal + VertexGroup.weight
923     if (weightTotal == 0):
924         bWeightTotZero = True  #avoid divide by zero later on
925         if (RootBone):
926             if Config.Verbose:
927                 print(" Warning Weight is ZERO for vertex %d => Add it to the root bone" % (VertexIndex))
928             RootBoneGroupIndex = FindUniqueIndexForRootBone(Object, RootVertexGroup)
929             mapVertexGroupNames[RootBoneGroupIndex] = StripBoneName(RootBone.name)
930             useBonesKey = pow(2, RootBoneGroupIndex)
931             vertexGroupIndices = list((RootBoneGroupIndex,))
932
933             weightTotal = 1
934     else:
935         bWeightTotZero = False
936     
937     if len(vertexGroupIndices) > 0:
938         vertexGroupIndices.sort();
939            
940         #build the vertex weight string: vertex indices, followed by influence weight for each bone
941         VertexWeightString = "\t\tvertWeights { %d" % (VertexIndex)
942         for vertexGroupIndex in vertexGroupIndices:
943             #get the weight of this specific VertexGroup (aka bone)
944             boneWeight = 1
945             for VertexGroup in Vertex.groups:
946                 if VertexGroup.group == vertexGroupIndex:
947                     boneWeight = VertexGroup.weight
948             #calculate the influence of this bone compared to the total of weighting applied to this Vertex
949             if not bWeightTotZero:
950                 VertexWeightString += ", %.7f" % (boneWeight / weightTotal)
951             else:
952                 VertexWeightString += ", %.7f" % (1.0 / len(vertexGroupIndices))
953         VertexWeightString += "}"
954         if bWeightTotZero:
955             VertexWeightString += " // total weight was zero in blender , export assign it to the RootBone with weight 1." 
956         if (len(Vertex.groups)) > 4:
957             VertexWeightString += " // vertex is associated to more than 4 bones in blender !! skip some bone association (was associated to %d bones)." % (len(Vertex.groups))
958         VertexWeightString += "\n"
959            
960         #store in dictionnary information
961         if useBonesKey not in useBonesDict:
962             VertexList = []
963             VertexList.append(VertexWeightString)
964             useBonesDict[useBonesKey] = (vertexGroupIndices, VertexList)
965         else:
966             pair_ListGroupIndices_ListAssignedVertices = useBonesDict[useBonesKey]
967             pair_ListGroupIndices_ListAssignedVertices[1].append(VertexWeightString)
968             useBonesDict[useBonesKey] = pair_ListGroupIndices_ListAssignedVertices
969     else:
970         print ("ERROR Vertex %d is not skinned (it doesn't belong to any vertex group\n" % (VertexIndex)) 
971
972
973
974 ############# ARMATURE: Bone export, and Bone animation export 
975
976          
977 def WriteArmatureParentRootBones(Config, Object, RootBonesList, skelFile):
978
979     if len(RootBonesList) > 1:
980         print(" /!\\  WARNING ,Marmelade need only one ROOT bone per armature, there is %d root bones " % len(RootBonesList))
981         print(RootBonesList)
982         
983     PoseBones = Object.pose.bones
984     for Bone in RootBonesList:
985         if Config.Verbose:
986             print("      Writing Root Bone: {}...".format(Bone.name))
987
988         PoseBone = PoseBones[Bone.name]
989         WriteBonePosition(Config, Object, Bone, PoseBones, PoseBone, skelFile, True)
990         #WriteOneBoneRestPosition(Config, Object, Bone, PoseBones, PoseBone, skelFile, True, Vector(),Quaternion())
991         if Config.Verbose:
992             print("      Done")
993         WriteArmatureChildBones(Config, Object, Bone.children, skelFile)
994
995             
996 def WriteArmatureChildBones(Config, Object, BonesList, skelFile):
997     PoseBones = Object.pose.bones
998     for Bone in BonesList:
999         if Config.Verbose:
1000             print("      Writing Child Bone: {}...".format(Bone.name))
1001         PoseBone = PoseBones[Bone.name]
1002         WriteBonePosition(Config, Object, Bone, PoseBones, PoseBone, skelFile, True)
1003         #WriteOneBoneRestPosition(Config, Object, Bone, PoseBones, PoseBone, skelFile, True, Vector(),Quaternion())
1004         if Config.Verbose:
1005             print("      Done")
1006             
1007         WriteArmatureChildBones(Config, Object, Bone.children, skelFile)
1008
1009
1010 def WriteBonePosition(Config, Object, Bone, PoseBones, PoseBone, File, isSkelFileNotAnimFile):
1011     # Compute armature scale : 
1012     # Many others exporter require sthe user to do Apply Scale in Object Mode to have 1,1,1 scale and so that anim data are correctly scaled
1013     # Here we retreive the Scale of the Armture Object.matrix_world.to_scale() and we use it to scale the bones :-)
1014     # So new Blender user should not complain about bad animation export if they forgot to apply the Scale to 1,1,1
1015
1016     armScale = Object.matrix_world.to_scale()
1017     ## scalematrix = Matrix()
1018     ## scalematrix[0][0] = armScale.x * Config.Scale
1019     ## scalematrix[1][1] = armScale.y * Config.Scale
1020     ## scalematrix[2][2] = armScale.z * Config.Scale
1021
1022     if isSkelFileNotAnimFile:
1023         #skel file, bone header
1024         File.write("\tCIwAnimBone\n")
1025         File.write("\t{\n")
1026         File.write("\t\tname \"%s\"\n" % StripBoneName(Bone.name))
1027         if Bone.parent:
1028             File.write("\t\tparent \"%s\"\n" % StripBoneName(Bone.parent.name))
1029     else:
1030         #anim file, bone header
1031         File.write("\t\t\n")
1032         File.write("\t\tbone \"%s\" \n" % StripBoneName(Bone.name))
1033
1034     if Bone.parent:
1035         ParentPoseBone = PoseBones[Bone.parent.name]
1036         locmat = ParentPoseBone.matrix.inverted() * PoseBone.matrix
1037     else:
1038         locmat = PoseBone.matrix
1039         if Config.MergeModes > 0:
1040             # Merge mode is in world coordinates .. anyway merge mesh doesn't work really with armature that should be local to one mesh
1041             locmat = Object.matrix_world * PoseBone.matrix
1042             armScale.x =  armScale.y = armScale.z = 1  
1043         
1044     loc = locmat.to_translation()
1045     quat = locmat.to_quaternion()
1046   
1047     if not Bone.parent:  # and Config.MergeModes == 0:
1048         #flip Y Z axes (only on root bone, other bones are local to root bones, so no need to rotate)
1049         X_ROT = mathutils.Matrix.Rotation(-math.pi / 2, 4, 'X')
1050         quat.rotate(X_ROT)
1051         loc.rotate(X_ROT)
1052
1053         
1054     #Scale the bone
1055     loc.x *= (armScale.x * Config.Scale)
1056     loc.y *= (armScale.y * Config.Scale)
1057     loc.z *= (armScale.z * Config.Scale)
1058     
1059     File.write("\t\tpos { %.9f, %.9f, %.9f }\n" % (loc[0], loc[1], loc[2]))
1060     File.write("\t\trot { %.9f, %.9f, %.9f, %.9f }\n" % (quat.w, quat.x, quat.y, quat.z))
1061
1062     if isSkelFileNotAnimFile:
1063         File.write("\t}\n")
1064
1065       
1066 def WriteKeyedAnimationSet(Config, Scene):  
1067     for Object in [Object for Object in Config.ObjectList if Object.animation_data]:
1068         if Config.Verbose:
1069             print("  Writing Animation Data for Object: {}".format(Object.name))
1070         Action = Object.animation_data.action
1071         if Action:
1072             #Object animated (aka single bone object)
1073             #build key frame time list
1074             keyframeTimes = set()
1075             if Config.ExportAnimation == 1:
1076                 # Exports only key frames
1077                 for FCurve in Action.fcurves:
1078                     for Keyframe in FCurve.keyframe_points:
1079                         if Keyframe.co[0] < Scene.frame_start:
1080                             keyframeTimes.add(Scene.frame_start)
1081                         elif Keyframe.co[0] > Scene.frame_end:
1082                             keyframeTimes.add(Scene.frame_end)
1083                         else:
1084                             keyframeTimes.add(int(Keyframe.co[0]))
1085             else:
1086                 # Exports all frames
1087                 keyframeTimes.update(range(Scene.frame_start, Scene.frame_end + 1, 1))
1088             keyframeTimes = list(keyframeTimes)
1089             keyframeTimes.sort()
1090             if len(keyframeTimes):
1091                 #Create the anim file for offset animation (or single bone animation
1092                 animfullname = os.path.dirname(Config.FilePath) + "\\anims\\%s_offset.anim" % (StripName(Object.name))
1093                 #not yet supported
1094                 """
1095                 ##    ensure_dir(animfullname)
1096                 ##    if Config.Verbose:
1097                 ##        print("      Creating anim file (single bone animation) %s" % (animfullname))
1098                 ##    animFile = open(animfullname, "w")
1099                 ##    animFile.write('// anim file exported from : %r\n' % os.path.basename(bpy.data.filepath))   
1100                 ##    animFile.write("CIwAnim\n")
1101                 ##    animFile.write("{\n")
1102                 ##    animFile.write("\tent \"%s\"\n" % (StripName(Object.name)))
1103                 ##    animFile.write("\tskeleton \"SingleBone\"\n")
1104                 ##    animFile.write("\t\t\n")
1105                 ##
1106                 ##    Config.File.write("\t\".\\anims\\%s_offset.anim\"\n" % (StripName(Object.name)))
1107                 ##
1108                 ##    for KeyframeTime in keyframeTimes:
1109                 ##        #Scene.frame_set(KeyframeTime)    
1110                 ##        animFile.write("\tCIwAnimKeyFrame\n")
1111                 ##        animFile.write("\t{\n")
1112                 ##        animFile.write("\t\ttime %.2f // frame num %d \n" % (KeyframeTime/Config.AnimFPS, KeyframeTime))
1113                 ##        animFile.write("\t\t\n")
1114                 ##        animFile.write("\t\tbone \"SingleBone\" \n")
1115                 ##        #postion
1116                 ##        posx = 0
1117                 ##        for FCurve in Action.fcurves:
1118                 ##            if FCurve.data_path == "location" and FCurve.array_index == 0: posx = FCurve.evaluate(KeyframeTime)
1119                 ##        posy = 0
1120                 ##        for FCurve in Action.fcurves:
1121                 ##            if FCurve.data_path == "location" and FCurve.array_index == 1: posy = FCurve.evaluate(KeyframeTime)
1122                 ##        posz = 0
1123                 ##        for FCurve in Action.fcurves:
1124                 ##            if FCurve.data_path == "location" and FCurve.array_index == 2: posz = FCurve.evaluate(KeyframeTime)
1125                 ##        animFile.write("\t\tpos {%.9f,%.9f,%.9f}\n" % (posx, posy, posz))
1126                 ##        #rotation
1127                 ##        rot = Euler()
1128                 ##        rot[0] = 0
1129                 ##        for FCurve in Action.fcurves:
1130                 ##            if FCurve.data_path == "rotation_euler" and FCurve.array_index == 1: rot[0] = FCurve.evaluate(KeyframeTime)
1131                 ##        rot[1] = 0
1132                 ##        for FCurve in Action.fcurves:
1133                 ##            if FCurve.data_path == "rotation_euler" and FCurve.array_index == 2: rot[1] = FCurve.evaluate(KeyframeTime)
1134                 ##        rot[2] = 0
1135                 ##        for FCurve in Action.fcurves:
1136                 ##            if FCurve.data_path == "rotation_euler" and FCurve.array_index == 3: rot[2] = FCurve.evaluate(KeyframeTime)
1137                 ##        rot = rot.to_quaternion()
1138                 ##        animFile.write("\t\trot {%.9f,%.9f,%.9f,%.9f}\n" % (rot[0], rot[1], rot[2], rot[3]))
1139                 ##        #scale
1140                 ##        scalex = 0
1141                 ##        for FCurve in Action.fcurves:
1142                 ##            if FCurve.data_path == "scale" and FCurve.array_index == 0: scalex = FCurve.evaluate(KeyframeTime)
1143                 ##        scaley = 0
1144                 ##        for FCurve in Action.fcurves:
1145                 ##            if FCurve.data_path == "scale" and FCurve.array_index == 1: scaley = FCurve.evaluate(KeyframeTime)
1146                 ##        scalez = 0
1147                 ##        for FCurve in Action.fcurves:
1148                 ##            if FCurve.data_path == "scale" and FCurve.array_index == 2: scalez = FCurve.evaluate(KeyframeTime)
1149                 ##        animFile.write("\t\t//scale {%.9f,%.9f,%.9f}\n" % (scalex, scaley, scalez))
1150                 ##        #keyframe done
1151                 ##        animFile.write("\t}\n")
1152                 ##    animFile.write("}\n")
1153                 ##    animFile.close()
1154                 """
1155             else:
1156                 if Config.Verbose:
1157                     print("    Object %s has no useable animation data." % (StripName(Object.name)))
1158
1159             if Config.ExportArmatures and Object.type == "ARMATURE":
1160                 if Config.Verbose:
1161                     print("    Writing Armature Bone Animation Data...\n")
1162                 PoseBones = Object.pose.bones
1163                 Bones = Object.data.bones
1164                 #riged bones animated 
1165                 #build key frame time list
1166                 keyframeTimes = set()
1167                 if Config.ExportAnimation==1:
1168                     # Exports only key frames
1169                     for FCurve in Action.fcurves:
1170                         for Keyframe in FCurve.keyframe_points:
1171                             if Keyframe.co[0] < Scene.frame_start:
1172                                 keyframeTimes.add(Scene.frame_start)
1173                             elif Keyframe.co[0] > Scene.frame_end:
1174                                 keyframeTimes.add(Scene.frame_end)
1175                             else:
1176                                 keyframeTimes.add(int(Keyframe.co[0]))
1177                 else:
1178                     # Exports all frame
1179                     keyframeTimes.update(range(Scene.frame_start, Scene.frame_end + 1, 1))
1180                    
1181                 keyframeTimes = list(keyframeTimes)
1182                 keyframeTimes.sort()
1183                 if Config.Verbose:
1184                     print("Exporting frames: ")
1185                     print(keyframeTimes)
1186                     if (Scene.frame_preview_end > Scene.frame_end):
1187                         print(" WARNING: END Frame of animation in UI preview is Higher than the Scene Frame end:\n Scene.frame_end %d versus Scene.frame_preview_end %d.\n"
1188                               % (Scene.frame_end, Scene.frame_preview_end))
1189                         print(" => You might need to change the Scene End Frame, to match the current UI preview frame end...\n=> if you don't want to miss end of animation.\n")
1190
1191                 if len(keyframeTimes):
1192                     #Create the anim file
1193                     animfullname = os.path.dirname(Config.FilePath) + "\\anims\\%s.anim" % (StripName(Object.name))
1194                     ensure_dir(animfullname)
1195                     if Config.Verbose:
1196                         print("      Creating anim file (bones animation) %s\n" % (animfullname))
1197                         print("      Frame count %d \n" % (len(keyframeTimes)))
1198                     animFile = open(animfullname, "w")
1199                     animFile.write('// anim file exported from : %r\n' % os.path.basename(bpy.data.filepath))   
1200                     animFile.write("CIwAnim\n")
1201                     animFile.write("{\n")
1202                     animFile.write("\tskeleton \"%s\"\n" % (StripName(Object.name)))
1203                     animFile.write("\t\t\n")
1204
1205                     Config.File.write("\t\".\\anims\\%s.anim\"\n" % (StripName(Object.name)))
1206
1207                     for KeyframeTime in keyframeTimes:
1208                         if Config.Verbose:
1209                             print("     Writing Frame %d:" % KeyframeTime)
1210                         animFile.write("\tCIwAnimKeyFrame\n")
1211                         animFile.write("\t{\n")
1212                         animFile.write("\t\ttime %.2f // frame num %d \n" % (KeyframeTime / Config.AnimFPS, KeyframeTime))
1213                         #for every frame write bones positions
1214                         Scene.frame_set(KeyframeTime)
1215                         for PoseBone in PoseBones:
1216                             if Config.Verbose:
1217                                 print("      Writing Bone: {}...".format(PoseBone.name))
1218                             animFile.write("\t\t\n")
1219
1220                             Bone = Bones[PoseBone.name]
1221                             WriteBonePosition(Config, Object, Bone, PoseBones, PoseBone, animFile, False)
1222                         #keyframe done
1223                         animFile.write("\t}\n")
1224                     animFile.write("}\n")
1225                     animFile.close()
1226             else:
1227                 if Config.Verbose:
1228                     print("    Object %s has no useable animation data." % (StripName(Object.name)))
1229         if Config.Verbose:
1230             print("  Done") #Done with Object
1231  
1232
1233                                 
1234  
1235 ################## Utilities
1236             
1237 def StripBoneName(name):
1238     return name.replace(" ", "")
1239
1240
1241 def StripName(Name):
1242     
1243     def ReplaceSet(String, OldSet, NewChar):
1244         for OldChar in OldSet:
1245             String = String.replace(OldChar, NewChar)
1246         return String
1247     
1248     import string
1249     
1250     NewName = ReplaceSet(Name, string.punctuation + " ", "_")
1251     return NewName
1252
1253
1254 def ensure_dir(f):
1255     d = os.path.dirname(f)
1256     if not os.path.exists(d):
1257         os.makedirs(d)
1258         
1259
1260 def CloseFile(Config):
1261     if Config.Verbose:
1262         print("Closing File...")
1263     Config.File.close()
1264     if Config.Verbose:
1265         print("Done")
1266
1267
1268 CoordinateSystems = (
1269     ("1", "Left-Handed", ""),
1270     ("2", "Right-Handed", ""),
1271     )
1272
1273
1274 AnimationModes = (
1275     ("0", "None", ""),
1276     ("1", "Keyframes Only", ""),
1277     ("2", "Full Animation", ""),
1278     )
1279
1280 ExportModes = (
1281     ("1", "All Objects", ""),
1282     ("2", "Selected Objects", ""),
1283     )
1284
1285 MergeModes = (
1286     ("0", "None", ""),
1287     ("1", "Merge in one big Mesh", ""),
1288     ("2", "Merge in unique Geo File containing several meshes", ""),
1289     )
1290
1291
1292 from bpy.props import StringProperty, EnumProperty, BoolProperty, IntProperty
1293
1294
1295 class MarmaladeExporter(bpy.types.Operator):
1296     """Export to the Marmalade model format (.group)"""
1297
1298     bl_idname = "export.marmalade"
1299     bl_label = "Export Marmalade"
1300
1301     filepath = StringProperty(subtype='FILE_PATH')
1302      #Export Mode
1303     ExportMode = EnumProperty(
1304         name="Export",
1305         description="Select which objects to export. Only Mesh, Empty, " \
1306                     "and Armature objects will be exported",
1307         items=ExportModes,
1308         default="1")
1309
1310     MergeModes = EnumProperty(
1311         name="Merge",
1312         description="Select if objects should be merged in one Geo File (it can be usefull if a scene is done by several cube/forms)." \
1313                     "Do not merge rigged character that have an armature.",
1314         items=MergeModes,
1315         default="0")
1316     
1317     #General Options
1318     Scale = IntProperty(
1319         name="Scale Percent",
1320         description="Scale percentage applied for export",
1321         default=100, min=1, max=1000)
1322     
1323     FlipNormals = BoolProperty(
1324         name="Flip Normals",
1325         description="",
1326         default=False)
1327     ApplyModifiers = BoolProperty(
1328         name="Apply Modifiers",
1329         description="Apply object modifiers before export",
1330         default=False)
1331     ExportVertexColors = BoolProperty(
1332         name="Export Vertices Colors",
1333         description="Export colors set on vertices, if any",
1334         default=True)
1335     ExportMaterialColors = BoolProperty(
1336         name="Export Material Colors",
1337         description="Ambient color is exported on the Material",
1338         default=True)
1339     ExportTextures = BoolProperty(
1340         name="Export Textures and UVs",
1341         description="Exports UVs and Reference external image files to be used by the model",
1342         default=True)
1343     CopyTextureFiles = BoolProperty(
1344         name="Copy Textures Files",
1345         description="Copy referenced Textures files in the models\\textures directory",
1346         default=True)
1347     ExportArmatures = BoolProperty(
1348         name="Export Armatures",
1349         description="Export the bones of any armatures to deform meshes",
1350         default=True)
1351     ExportAnimation = EnumProperty(
1352         name="Animations",
1353         description="Select the type of animations to export. Only object " \
1354                     "and armature bone animations can be exported. Full " \
1355                     "Animation exports every frame",
1356         items=AnimationModes,
1357         default="1")
1358     if bpy.context.scene:
1359         defFPS = bpy.context.scene.render.fps
1360     else:
1361         defFPS = 30                 
1362     AnimFPS = IntProperty(
1363         name="Animation FPS",
1364         description="Frame rate used to export animation in seconds (can be used to artficially slow down the exported animation, or to speed up it",
1365         default=defFPS, min=1, max=300)
1366
1367     #Advance Options
1368     CoordinateSystem = EnumProperty(
1369         name="System",
1370         description="Select a coordinate system to export to",
1371         items=CoordinateSystems,
1372         default="1")
1373     
1374     Verbose = BoolProperty(
1375         name="Verbose",
1376         description="Run the exporter in debug mode. Check the console for output",
1377         default=True)
1378
1379     def execute(self, context):
1380         #Append .group
1381         FilePath = bpy.path.ensure_ext(self.filepath, ".group")
1382
1383         Config = MarmaladeExporterSettings(context,
1384                                          FilePath,
1385                                          CoordinateSystem=self.CoordinateSystem,
1386                                          FlipNormals=self.FlipNormals,
1387                                          ApplyModifiers=self.ApplyModifiers,
1388                                          Scale=self.Scale,
1389                                          AnimFPS=self.AnimFPS,
1390                                          ExportVertexColors=self.ExportVertexColors,
1391                                          ExportMaterialColors=self.ExportMaterialColors,
1392                                          ExportTextures=self.ExportTextures,
1393                                          CopyTextureFiles=self.CopyTextureFiles,
1394                                          ExportArmatures=self.ExportArmatures,
1395                                          ExportAnimation=self.ExportAnimation,
1396                                          ExportMode=self.ExportMode,
1397                                          MergeModes=self.MergeModes,
1398                                          Verbose=self.Verbose)
1399
1400         # Exit edit mode before exporting, so current object states are exported properly.
1401         if bpy.ops.object.mode_set.poll():
1402             bpy.ops.object.mode_set(mode='OBJECT')
1403
1404         ExportMadeWithMarmaladeGroup(Config)
1405         return {"FINISHED"}
1406
1407     def invoke(self, context, event):
1408         if not self.filepath:
1409             self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".group")
1410         WindowManager = context.window_manager
1411         WindowManager.fileselect_add(self)
1412         return {"RUNNING_MODAL"}
1413
1414
1415 def menu_func(self, context):
1416     self.layout.operator(MarmaladeExporter.bl_idname, text="Marmalade cross-platform Apps (.group)")
1417
1418
1419 def register():
1420     bpy.utils.register_module(__name__)
1421
1422     bpy.types.INFO_MT_file_export.append(menu_func)
1423
1424
1425 def unregister():
1426     bpy.utils.unregister_module(__name__)
1427
1428     bpy.types.INFO_MT_file_export.remove(menu_func)
1429
1430
1431 if __name__ == "__main__":
1432     register()