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