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