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