1 # ***** GPL LICENSE BLOCK *****
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation, either version 3 of the License, or
6 # (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # All rights reserved.
16 # ***** GPL LICENSE BLOCK *****
18 # 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.
23 "name": "Marmalade Cross-platform Apps (.group)",
24 "author": "Benoit Muller",
27 "location": "File > Export > Marmalade cross-platform Apps (.group)",
28 "description": "Export Marmalade Format files (.group)",
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?"\
34 "category": "Import-Export"}
38 from math import radians
41 from mathutils import Matrix
51 #Container for the exporter settings
52 class MarmaladeExporterSettings:
62 ExportVertexColors=True,
63 ExportMaterialColors=True,
65 CopyTextureFiles=True,
66 ExportArmatures=False,
67 ExportAnimationFrames=0,
68 ExportAnimationActions=0,
72 self.context = context
73 self.FilePath = FilePath
74 self.CoordinateSystem = int(CoordinateSystem)
75 self.FlipNormals = FlipNormals
76 self.ApplyModifiers = ApplyModifiers
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
92 def ExportMadeWithMarmaladeGroup(Config):
93 print("----------\nExporting to {}".format(Config.FilePath))
95 print("Opening File...")
96 Config.File = open(Config.FilePath, "w")
102 print("writing group header")
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))
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]
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]
120 print(" List: {}\nDone".format(Config.ExportList))
123 print("Setting up...")
125 if Config.ExportAnimationFrames:
127 print(bpy.context.scene)
128 print(bpy.context.scene.frame_current)
129 CurrentFrame = bpy.context.scene.frame_current
130 #comment because it crashes Blender on some old blend file: bpy.context.scene.frame_current = bpy.context.scene.frame_current
134 Config.ObjectList = []
136 print("Writing Objects...")
137 WriteObjects(Config, Config.ExportList)
142 print("Objects Exported: {}".format(Config.ExportList))
144 if Config.ExportAnimationFrames:
146 print("Writing Animation...")
147 WriteKeyedAnimationSet(Config, bpy.context.scene)
148 bpy.context.scene.frame_current = CurrentFrame
151 Config.File.write("}\n")
156 def GetObjectChildren(Parent):
157 return [Object for Object in Parent.children
158 if Object.type in {'ARMATURE', 'EMPTY', 'MESH'}]
161 #Returns the file path of first image texture from Material.
162 def GetMaterialTextureFullPath(Config, Material):
164 #Create a list of Textures that have type "IMAGE"
165 ImageTextures = [Material.texture_slots[TextureSlot].texture for TextureSlot in Material.texture_slots.keys() if Material.texture_slots[TextureSlot].texture.type == "IMAGE"]
166 #Refine a new list with only image textures that have a file source
167 TexImages = [Texture.image for Texture in ImageTextures if getattr(Texture.image, "source", "") == "FILE"]
168 ImageFiles = [Texture.image.filepath for Texture in ImageTextures if getattr(Texture.image, "source", "") == "FILE"]
170 filepath = TexImages[0].filepath
171 if TexImages[0].packed_file:
172 TexImages[0].unpack()
173 if not os.path.exists(filepath):
174 #try relative path to the blend file
175 filepath = os.path.dirname(bpy.data.filepath) + filepath
176 #Marmalade doesn't like jpeg/tif so try to convert in png on the fly
177 if (TexImages[0].file_format == 'JPEG' or TexImages[0].file_format == 'TIFF') and os.path.exists(filepath):
178 marmaladeConvert = os.path.expandvars("%S3E_DIR%\\..\\tools\\ImageMagick\\win32\\convert.exe")
179 if (os.path.exists(marmaladeConvert)):
180 srcImagefilepath = filepath
181 filepath = os.path.splitext(filepath)[0] + '.png'
183 print(" /!\\ Converting Texture %s in PNG: %s{}..." % (TexImages[0].file_format, filepath))
184 print('"%s" "%s" "%s"' % (marmaladeConvert, srcImagefilepath, filepath))
185 subprocess.call([marmaladeConvert, srcImagefilepath, filepath])
190 def WriteObjects(Config, ObjectList, geoFile=None, mtlFile=None, GeoModel=None, bChildObjects=False):
191 Config.ObjectList += ObjectList
193 if bChildObjects == False and Config.MergeModes > 0:
195 #we merge objects, so use name of group file for the name of Geo
196 geoFile, mtlFile = CreateGeoMtlFiles(Config, bpy.path.display_name_from_filepath(Config.FilePath))
197 GeoModel = CGeoModel(bpy.path.display_name_from_filepath(Config.FilePath))
199 for Object in ObjectList:
201 print(" Writing Object: {}...".format(Object.name))
203 if Config.ExportArmatures and Object.type == "ARMATURE":
204 Armature = Object.data
205 ParentList = [Bone for Bone in Armature.bones if Bone.parent is None]
207 print(" Writing Armature Bones...")
208 #Create the skel file
209 skelfullname = os.path.dirname(Config.FilePath) + os.sep + "models" + os.sep + "%s.skel" % (StripName(Object.name))
210 ensure_dir(skelfullname)
212 print(" Creating skel file %s" % (skelfullname))
214 skelFile = open(skelfullname, "w")
215 skelFile.write('// skel file exported from : %r\n' % os.path.basename(bpy.data.filepath))
216 skelFile.write("CIwAnimSkel\n")
217 skelFile.write("{\n")
218 skelFile.write("\tnumBones %d\n" % (len(Armature.bones)))
219 Config.File.write("\t\".\models\%s.skel\"\n" % (StripName(Object.name)))
221 WriteArmatureParentRootBones(Config, Object, ParentList, skelFile)
223 skelFile.write("}\n")
228 ChildList = GetObjectChildren(Object)
229 if Config.ExportMode == 2: # Selected Objects Only
230 ChildList = [Child for Child in ChildList
231 if Child in Config.context.selected_objects]
233 print(" Writing Children...")
234 WriteObjects(Config, ChildList, geoFile, mtlFile, GeoModel, True)
236 print(" Done Writing Children")
238 if Object.type == "MESH":
240 print(" Generating Mesh...")
241 if Config.ApplyModifiers:
242 if Config.ExportArmatures:
243 #Create a copy of the object and remove all armature modifiers so an unshaped
244 #mesh can be created from it.
245 Object2 = Object.copy()
246 for Modifier in [Modifier for Modifier in Object2.modifiers if Modifier.type == "ARMATURE"]:
247 Object2.modifiers.remove(Modifier)
248 Mesh = Object2.to_mesh(bpy.context.scene, True, "PREVIEW")
250 Mesh = Object.to_mesh(bpy.context.scene, True, "PREVIEW")
252 Mesh = Object.to_mesh(bpy.context.scene, False, "PREVIEW")
255 print(" Writing Mesh...")
257 # Flip ZY axis (Blender Z up: Marmalade: Y up) ans Scale appropriately
258 X_ROT = mathutils.Matrix.Rotation(-math.pi / 2, 4, 'X')
260 if Config.MergeModes == 0:
261 # No merge, so all objects are exported in MODEL SPACE and not in world space
262 # Calculate Scale of the Export
263 meshScale = Object.matrix_world.to_scale() # Export is working, even if user doesn't have use apply scale in Edit mode.
265 scalematrix = Matrix()
266 scalematrix[0][0] = meshScale.x * Config.Scale
267 scalematrix[1][1] = meshScale.y * Config.Scale
268 scalematrix[2][2] = meshScale.z * Config.Scale
270 meshRot = Object.matrix_world.to_quaternion() # Export is working, even if user doesn't have use apply Rotation in Edit mode.
271 Mesh.transform(X_ROT * meshRot.to_matrix().to_4x4() * scalematrix)
273 # In Merge mode, we need to keep relative postion of each objects, so we export in WORLD SPACE
274 SCALE_MAT = mathutils.Matrix.Scale(Config.Scale, 4)
275 Mesh.transform(SCALE_MAT * X_ROT * Object.matrix_world)
277 # manage merge options
279 if Config.MergeModes == 0:
280 #one geo per Object, so use name of Object for the Geo file
281 geoFile, mtlFile = CreateGeoMtlFiles(Config, StripName(Object.name))
282 GeoModel = CGeoModel(StripName(Object.name))
284 # Write the Mesh in the Geo file
285 WriteMesh(Config, Object, Mesh, geoFile, mtlFile, GeoModel)
287 if Config.MergeModes == 0:
288 # no merge so finalize the file, and discard the file and geo class
289 FinalizeGeoMtlFiles(Config, geoFile, mtlFile)
293 elif Config.MergeModes == 1:
294 # merge in one Mesh, so keep the Geo class and prepare to change object
296 elif Config.MergeModes == 2:
297 # merge several Meshes in one file: so clear the mesh data that we just written in the file,
298 # but keep Materials info that need to be merged across objects
299 GeoModel.ClearAllExceptMaterials()
304 if Config.ApplyModifiers and Config.ExportArmatures:
305 bpy.data.objects.remove(Object2)
306 bpy.data.meshes.remove(Mesh)
309 print(" Done Writing Object: {}".format(Object.name))
311 if bChildObjects == False:
312 # we have finish to do all objects
314 if Config.MergeModes == 1:
315 # we have Merges all objects in one Mesh, so time to write this big mesh in the file
316 GeoModel.PrintGeoMesh(geoFile)
317 # time to write skinfile if any
318 if len(GeoModel.useBonesDict) > 0:
319 # some mesh was not modified by the armature. so we must skinned the merged mesh.
320 # So unskinned vertices from unarmatured meshes, are assigned to the root bone of the armature
321 for i in range(0, len(GeoModel.vList)):
322 if not i in GeoModel.skinnedVertices:
323 GeoModel.skinnedVertices.append(i)
324 useBonesKey = pow(2, GeoModel.armatureRootBoneIndex)
325 vertexGroupIndices = list((GeoModel.armatureRootBoneIndex,))
326 if useBonesKey not in GeoModel.useBonesDict:
327 GeoModel.mapVertexGroupNames[GeoModel.armatureRootBoneIndex] = StripBoneName(GeoModel.armatureRootBone.name)
329 VertexList.append("\t\tvertWeights { %d, 1.0}" % i)
330 GeoModel.useBonesDict[useBonesKey] = (vertexGroupIndices, VertexList)
332 pair_ListGroupIndices_ListAssignedVertices = GeoModel.useBonesDict[useBonesKey]
333 pair_ListGroupIndices_ListAssignedVertices[1].append("\t\tvertWeights { %d, 1.0}" % i)
334 GeoModel.useBonesDict[useBonesKey] = pair_ListGroupIndices_ListAssignedVertices
335 # now generates the skin file
336 PrintSkinWeights(Config, GeoModel.armatureObjectName, GeoModel.useBonesDict, GeoModel.mapVertexGroupNames, GeoModel.name)
337 if Config.MergeModes > 0:
338 WriteMeshMaterialsForGeoModel(Config, mtlFile, GeoModel)
339 FinalizeGeoMtlFiles(Config, geoFile, mtlFile)
345 def CreateGeoMtlFiles(Config, Name):
347 geofullname = os.path.dirname(Config.FilePath) + os.sep + "models" + os.sep + "%s.geo" % Name
348 ensure_dir(geofullname)
350 print(" Creating geo file %s" % (geofullname))
351 geoFile = open(geofullname, "w")
352 geoFile.write('// geo file exported from : %r\n' % os.path.basename(bpy.data.filepath))
353 geoFile.write("CIwModel\n")
355 geoFile.write("\tname \"%s\"\n" % Name)
356 # add it to the group
357 Config.File.write("\t\".\models\%s.geo\"\n" % Name)
359 # Create the mtl file
360 mtlfullname = os.path.dirname(Config.FilePath) + os.sep + "models" + os.sep + "%s.mtl" % Name
361 ensure_dir(mtlfullname)
363 print(" Creating mtl file %s" % (mtlfullname))
364 mtlFile = open(mtlfullname, "w")
365 mtlFile.write('// mtl file exported from : %r\n' % os.path.basename(bpy.data.filepath))
366 return geoFile, mtlFile
369 def FinalizeGeoMtlFiles(Config, geoFile, mtlFile):
371 print(" Closing geo file")
375 print(" Closing mtl file")
379 def WriteMesh(Config, Object, Mesh, geoFile=None, mtlFile=None, GeoModel=None):
380 if geoFile == None or mtlFile == None:
381 print (" ERROR not geo file arguments in WriteMesh method")
385 print (" ERROR not GeoModel arguments in WriteMesh method")
388 BuildOptimizedGeo(Config, Object, Mesh, GeoModel)
389 if Config.MergeModes == 0 or Config.MergeModes == 2:
390 #if we don't merge, or if we write several meshes into one file ... write the mesh everytime we do an object
391 GeoModel.PrintGeoMesh(geoFile)
394 print(" Done\n Writing Mesh Materials...")
396 if Config.MergeModes == 0:
397 #No merge, so we can diretly write the Mtl file associated to this object
398 WriteMeshMaterialsForGeoModel(Config, mtlFile, GeoModel)
403 if Config.ExportArmatures:
405 print(" Writing Mesh Weights...")
406 WriteMeshSkinWeightsForGeoModel(Config, Object, Mesh, GeoModel)
411 ###### optimized version fo Export, can be used also to merge several object in one single geo File ######
418 # -> List Vertex Colors
421 # -> Blender Material Object
422 # -> List Tris -> Stream Indices v,vn,uv0,uv1,vc
423 # -> List Quads -> Stream Indices v,vn,uv0,uv1,vc
427 #Store one Point of a Quad or Tri in marmalade geo format: //index-list is: { <int> <int> <int> <int> <int> } //v,vn,uv0,uv1,vc
430 __slots__ = "v", "vn", "uv0", "uv1", "vc"
432 def __init__(self, v, vn, uv0, uv1, vc):
441 #Store a Quad or a Tri in marmalade geo format : 3 or 4 CIndexList depending it is a Tri or a Quad
444 __slots__ = "pointsList",
449 def AddPoint(self, v, vn, uv0, uv1, vc):
450 self.pointsList.append( CGeoIndexList(v, vn, uv0, uv1, vc))
452 def PointsCount(self):
453 return len(self.pointsList)
455 def PrintPoly(self, geoFile):
456 if len(self.pointsList) == 3:
457 geoFile.write("\t\t\t\tt ")
458 if len(self.pointsList) == 4:
459 geoFile.write("\t\t\t\tq ")
460 for point in self.pointsList:
461 geoFile.write(" {%d, %d, %d, %d, %d}" % (point.v, point.vn, point.uv0, point.uv1, point.vc))
466 #Store all the poly (tri or quad) assigned to a Material in marmalade geo format
468 class CGeoMaterialPolys:
469 __slots__ = "name", "material", "quadList", "triList", "currentPoly"
471 def __init__(self, name, material=None):
473 self.material = material
476 self.currentPoly = None
479 self.currentPoly = CGeoPoly()
481 def AddPoint(self, v, vn, uv0, uv1, vc):
482 self.currentPoly.AddPoint(v, vn, uv0, uv1, vc)
485 if (self.currentPoly.PointsCount() == 3):
486 self.triList.append(self.currentPoly)
487 if (self.currentPoly.PointsCount() == 4):
488 self.quadList.append(self.currentPoly)
489 self.currentPoly = None
491 def ClearPolys(self):
494 self.currentPoly = None
496 def PrintMaterialPolys(self, geoFile):
497 geoFile.write("\t\tCSurface\n")
498 geoFile.write("\t\t{\n")
499 geoFile.write("\t\t\tmaterial \"%s\"\n" % self.name)
501 geoFile.write("\t\t\tCTris\n")
502 geoFile.write("\t\t\t{\n")
503 geoFile.write("\t\t\t\tnumTris %d\n" % (len(self.triList)))
504 for poly in self.triList:
505 poly.PrintPoly(geoFile)
506 geoFile.write("\t\t\t}\n")
509 geoFile.write("\t\t\tCQuads\n")
510 geoFile.write("\t\t\t{\n")
511 geoFile.write("\t\t\t\tnumQuads %d\n" % (len(self.quadList)))
512 for poly in self.quadList:
513 poly.PrintPoly(geoFile)
514 geoFile.write("\t\t\t}\n")
515 geoFile.write("\t\t}\n")
519 #Store all the information on a Model/Mesh (vertices, normal, certcies color, uv0, uv1, TRI, QUAD) in marmalade geo format
522 __slots__ = ("name", "MaterialsDict", "vList", "vnList", "vcList", "uv0List", "uv1List",
523 "currentMaterialPolys", "vbaseIndex","vnbaseIndex", "uv0baseIndex", "uv1baseIndex",
524 "armatureObjectName", "useBonesDict", "mapVertexGroupNames", "armatureRootBone", "armatureRootBoneIndex", "skinnedVertices")
526 def __init__(self, name):
528 self.MaterialsDict = {}
534 self.currentMaterialPolys = None
535 #used xx baseIndex are used when merging several blender objects into one Mesh in the geo file (internal offset)
538 self.uv0baseIndex = 0
539 self.uv1baseIndex = 0
541 # Store some information for skin management , when we merge several object in one big mesh (MergeModes 1)
542 # can only work if in the object list only one is rigged with an armature... and if it is located in 0,0,0
543 self.armatureObjectName = ""
544 #useBonesKey : bit field, where each bit is a VertexGroup.Index): Sum(2^VertGroupIndex).
545 #useBonesDict[useBonesKey] = tuple(VertexGroups.group, list(Vertex))
546 self.useBonesDict = {}
547 self.mapVertexGroupNames = {}
548 self.armatureRootBone = None
549 self.armatureRootBoneIndex = 0
550 self.skinnedVertices = []
554 def AddVertex(self, vertex):
555 self.vList.append(vertex.copy())
557 def AddVertexNormal(self, vertexN):
558 self.vnList.append(vertexN.copy())
560 # add a uv coordiantes and return the current Index in the stream (index is local to the object, when we merge several object into a one Mesh)
561 def AddVertexUV0(self, u, v):
562 self.uv0List.append((u, v))
563 return len(self.uv0List) - 1 - self.uv0baseIndex
565 def AddVertexUV1(self, u, v):
566 self.uv1List.append((u, v))
567 return len(self.uv1List) - 1 - self.uv1baseIndex
569 # add a vertexcolor if it doesn't already exist and return the current Index in the stream (index is global to all objects, when we merge several object into a one Mesh)
570 def AddVertexColor(self, r, g, b, a):
571 for i in range(0, len(self.vcList)):
573 if col[0] == r and col[1] == g and col[2] == b and col[3] == a:
576 self.vcList.append((r, g, b, a))
577 return len(self.vcList)-1
579 def BeginPoly(self, MaterialName, material=None):
580 if MaterialName not in self.MaterialsDict:
581 self.currentMaterialPolys = CGeoMaterialPolys(MaterialName, material)
583 self.currentMaterialPolys = self.MaterialsDict[MaterialName]
584 self.currentMaterialPolys.BeginPoly()
586 def AddPoint(self, v, vn, uv0, uv1, vc):
590 vn += self.vnbaseIndex
592 uv0 += self.uv0baseIndex
594 uv1 += self.uv1baseIndex
596 self.currentMaterialPolys.AddPoint(v, vn, uv0, uv1, vc)
599 self.currentMaterialPolys.EndPoly()
600 self.MaterialsDict[self.currentMaterialPolys.name] = self.currentMaterialPolys
601 self.currentMaterialPolys = None
604 #used in Merge mode 1: allows to merge several blender objects into one Mesh.
605 self.vbaseIndex = len(self.vList)
606 self.vnbaseIndex = len(self.vnList)
607 self.uv0baseIndex = len(self.uv0List)
608 self.uv1baseIndex = len(self.uv1List)
610 def ClearAllExceptMaterials(self):
611 #used in Merge mode 2: one geo with several mesh
617 self.currentMaterialPolys = None
620 self.uv0baseIndex = 0
621 self.uv1baseIndex = 0
622 for GeoMaterialPolys in self.MaterialsDict.values():
623 GeoMaterialPolys.ClearPolys()
624 self.useBonesDict = {}
625 self.mapVertexGroupNames = {}
626 self.armatureObjectName = ""
627 self.armatureRootBone = None
628 self.armatureRootBoneIndex = 0
629 self.skinnedVertices = []
631 def PrintGeoMesh(self, geoFile):
632 geoFile.write("\tCMesh\n")
633 geoFile.write("\t{\n")
634 geoFile.write("\t\tname \"%s\"\n" % (StripName(self.name)))
637 geoFile.write("\t\tCVerts\n")
638 geoFile.write("\t\t{\n")
639 geoFile.write("\t\t\tnumVerts %d\n" % len(self.vList))
640 for vertex in self.vList:
641 geoFile.write("\t\t\tv { %.9f, %.9f, %.9f }\n" % (vertex[0], vertex[1], vertex[2]))
642 geoFile.write("\t\t}\n")
645 geoFile.write("\t\tCVertNorms\n")
646 geoFile.write("\t\t{\n")
647 geoFile.write("\t\t\tnumVertNorms %d\n" % len(self.vnList))
648 for vertexn in self.vnList:
649 geoFile.write("\t\t\tvn { %.9f, %.9f, %.9f }\n" % (vertexn[0], vertexn[1], vertexn[2]))
650 geoFile.write("\t\t}\n")
653 geoFile.write("\t\tCVertCols\n")
654 geoFile.write("\t\t{\n")
655 geoFile.write("\t\t\tnumVertCols %d\n" % len(self.vcList))
656 for color in self.vcList:
657 geoFile.write("\t\t\tcol { %.6f, %.6f, %.6f, %.6f }\n" % (color[0], color[1], color[2], color[3])) #alpha is not supported on blender for vertex colors
658 geoFile.write("\t\t}\n")
661 geoFile.write("\t\tCUVs\n")
662 geoFile.write("\t\t{\n")
663 geoFile.write("\t\t\tsetID 0\n")
664 geoFile.write("\t\t\tnumUVs %d\n" % len(self.uv0List))
665 for uv in self.uv0List:
666 geoFile.write("\t\t\tuv { %.9f, %.9f }\n" % (uv[0], uv[1]))
667 geoFile.write("\t\t}\n")
670 geoFile.write("\t\tCUVs\n")
671 geoFile.write("\t\t{\n")
672 geoFile.write("\t\t\tsetID 1\n")
673 geoFile.write("\t\t\tnumUVs %d\n" % len(self.uv1List))
674 for uv in self.uv1List:
675 geoFile.write("\t\t\tuv { %.9f, %.9f }\n" % (uv[0], uv[1]))
676 geoFile.write("\t\t}\n")
678 for GeoMaterialPolys in self.MaterialsDict.values():
679 GeoMaterialPolys.PrintMaterialPolys(geoFile)
680 geoFile.write("\t}\n")
682 def GetMaterialList(self):
683 return list(self.MaterialsDict.keys())
685 def GetMaterialByName(self, name):
686 if name in self.MaterialsDict:
687 return self.MaterialsDict[name].material
694 # iterates faces, vertices ... and store the information in the GeoModel container
695 def BuildOptimizedGeo(Config, Object, Mesh, GeoModel):
697 GeoModel = CGeoModel(filename, Object.name)
699 #Ensure tessfaces data are here
700 Mesh.update (calc_tessface=True)
702 #Store Vertex stream, and Normal stream (use directly the order from blender collection
703 for Vertex in Mesh.vertices:
704 GeoModel.AddVertex(Vertex.co)
705 Normal = Vertex.normal
706 if Config.FlipNormals:
708 GeoModel.AddVertexNormal(Normal)
709 #Check if some colors have been defined
711 if Config.ExportVertexColors and (len(Mesh.vertex_colors) > 0):
712 vertexColors = Mesh.tessface_vertex_colors[0].data
714 #Check if some uv coordinates have been defined
716 if Config.ExportTextures and (len(Mesh.uv_textures) > 0):
717 for UV in Mesh.tessface_uv_textures:
719 UVCoordinates = UV.data
722 #Iterate on Faces and Store the poly (quad or tri) and the associate colors,UVs
723 for Face in Mesh.tessfaces:
724 # stream for vertex (we use the same for normal)
725 Vertices = list(Face.vertices)
726 if Config.CoordinateSystem == 1:
727 Vertices = Vertices[::-1]
728 # stream for vertex colors
730 MeshColor = vertexColors[Face.index]
731 if len(Vertices) == 3:
732 FaceColors = list((MeshColor.color1, MeshColor.color2, MeshColor.color3))
734 FaceColors = list((MeshColor.color1, MeshColor.color2, MeshColor.color3, MeshColor.color4))
735 if Config.CoordinateSystem == 1:
736 FaceColors = FaceColors[::-1]
738 for color in FaceColors:
739 index = GeoModel.AddVertexColor(color[0], color[1], color[2], 1) #rgba => no alpha on vertex color in Blender so use 1
740 colorIndex.append(index)
742 colorIndex = list((-1,-1,-1,-1))
744 # stream for UV0 coordinates
746 uvFace = UVCoordinates[Face.index]
748 for uvVertex in uvFace.uv:
749 uvVertices.append(tuple(uvVertex))
750 if Config.CoordinateSystem == 1:
751 uvVertices = uvVertices[::-1]
753 for uvVertex in uvVertices:
754 index = GeoModel.AddVertexUV0(uvVertex[0], 1 - uvVertex[1])
755 uv0Index.append(index)
757 uv0Index = list((-1, -1, -1, -1))
759 # stream for UV1 coordinates
760 uv1Index = list((-1, -1, -1, -1))
763 # find the associated material
764 if Face.material_index < len(Mesh.materials):
765 mat = Mesh.materials[Face.material_index]
769 matName = "NoMaterialAssigned" # There is no material assigned in blender !!!, exporter have generated a default one
771 # now on the material, generates the tri/quad in v,vn,uv0,uv1,vc stream index
772 GeoModel.BeginPoly(matName, mat)
774 for i in range(0, len(Vertices)):
775 GeoModel.AddPoint(Vertices[i], Vertices[i], uv0Index[i], uv1Index[i], colorIndex[i])
782 # Get the list of Material in use by the CGeoModel
783 def WriteMeshMaterialsForGeoModel(Config, mtlFile, GeoModel):
784 for matName in GeoModel.GetMaterialList():
785 Material = GeoModel.GetMaterialByName(matName)
786 WriteMaterial(Config, mtlFile, Material)
789 def WriteMaterial(Config, mtlFile, Material=None):
790 mtlFile.write("CIwMaterial\n")
793 mtlFile.write("\tname \"%s\"\n" % Material.name)
795 if Config.ExportMaterialColors:
796 #if bpy.context.scene.world:
797 # MatAmbientColor = Material.ambient * bpy.context.scene.world.ambient_color
798 MatAmbientColor = Material.ambient * Material.diffuse_color
799 mtlFile.write("\tcolAmbient {%.2f,%.2f,%.2f,%.2f} \n" % (min(255, MatAmbientColor[0] * 255), min(255, MatAmbientColor[1] * 255), min(255, MatAmbientColor[2] * 255), min(255, Material.alpha * 255)))
800 MatDiffuseColor = 255 * Material.diffuse_intensity * Material.diffuse_color
801 MatDiffuseColor = min((255, 255, 255)[:],MatDiffuseColor[:])
802 mtlFile.write("\tcolDiffuse {%.2f,%.2f,%.2f} \n" % (MatDiffuseColor[:]))
803 MatSpecularColor = 255 * Material.specular_intensity * Material.specular_color
804 MatSpecularColor = min((255, 255, 255)[:],MatSpecularColor[:])
805 mtlFile.write("\tcolSpecular {%.2f,%.2f,%.2f} \n" % (MatSpecularColor[:]))
806 # EmitColor = Material.emit * Material.diffuse_color
807 # mtlFile.write("\tcolEmissive {%.2f,%.2f,%.2f} \n" % (EmitColor* 255)[:])
809 mtlFile.write("\tname \"NoMaterialAssigned\" // There is no material assigned in blender !!!, exporter have generated a default one\n")
812 if Config.ExportTextures:
813 Texture = GetMaterialTextureFullPath(Config, Material)
815 mtlFile.write("\ttexture0 .\\textures\\%s\n" % (bpy.path.basename(Texture)))
817 if Config.CopyTextureFiles:
818 if not os.path.exists(Texture):
819 #try relative path to the blend file
820 Texture = os.path.dirname(bpy.data.filepath) + Texture
821 if os.path.exists(Texture):
822 textureDest = os.path.dirname(Config.FilePath) + os.sep + "models" + os.sep + "textures" + os.sep + ("%s" % bpy.path.basename(Texture))
823 ensure_dir(textureDest)
825 print(" Copying the texture file %s ---> %s" % (Texture, textureDest))
826 shutil.copy(Texture, textureDest)
829 print(" CANNOT Copy texture file (not found) %s" % (Texture))
832 def GetFirstRootBone(ArmatureObject):
833 ArmatureBones = ArmatureObject.data.bones
834 ParentBoneList = [Bone for Bone in ArmatureBones if Bone.parent is None]
836 return ParentBoneList[0]
840 def GetVertexGroupFromBone(Object, Bone):
842 vertexGroupList = [VertexGroup for VertexGroup in Object.vertex_groups if VertexGroup.name == Bone.name]
844 return vertexGroupList[0]
848 def GetBoneListNames(Bones):
851 boneList.append(Bone.name)
852 boneList += GetBoneListNames(Bone.children)
856 def FindUniqueIndexForRootBone(Object, RootVertexGroup):
858 return RootVertexGroup.index
860 #If there is not VertexGroup associated to the root bone name, we don't have a vertex index.
861 #so use the next available free index
862 return len(Object.vertex_groups)
865 def WriteMeshSkinWeightsForGeoModel(Config, Object, Mesh, GeoModel):
866 ArmatureList = [Modifier for Modifier in Object.modifiers if Modifier.type == "ARMATURE"]
868 ArmatureObject = ArmatureList[0].object
869 if ArmatureObject is None:
871 RootBone = GetFirstRootBone(ArmatureObject)
872 RootVertexGroup = GetVertexGroupFromBone(Object, RootBone)
873 BoneNames = GetBoneListNames(ArmatureObject.data.bones)
875 GeoModel.armatureObjectName = StripName(ArmatureObject.name)
877 GeoModel.armatureRootBone = RootBone
878 GeoModel.armatureRootBoneIndex = FindUniqueIndexForRootBone(Object, RootVertexGroup)
880 # Marmalade need to declare a vertex per list of affected bones
881 # so first we have to get all the combinations of affected bones that exist in the mesh
882 # to build thoses groups, we build a unique key (like a bit field, where each bit is a VertexGroup.Index): Sum(2^VertGroupIndex)... so we have a unique Number per combinations
884 for Vertex in Mesh.vertices:
885 VertexIndex = Vertex.index + GeoModel.vbaseIndex
886 AddVertexToDicionarySkinWeights(Config, Object, Mesh, Vertex, GeoModel.useBonesDict, GeoModel.mapVertexGroupNames, VertexIndex, RootBone, RootVertexGroup, BoneNames)
887 GeoModel.skinnedVertices.append(VertexIndex)
889 if Config.MergeModes != 1:
890 # write skin file directly
891 PrintSkinWeights(Config, GeoModel.armatureObjectName, GeoModel.useBonesDict, GeoModel.mapVertexGroupNames, StripName(Object.name))
894 def PrintSkinWeights(Config, ArmatureObjectName, useBonesDict, mapVertexGroupNames, GeoName):
895 #Create the skin file
896 skinfullname = os.path.dirname(Config.FilePath) + os.sep + "models" + os.sep + "%s.skin" % GeoName
897 ensure_dir(skinfullname)
899 print(" Creating skin file %s" % (skinfullname))
900 skinFile = open(skinfullname, "w")
901 skinFile.write('// skin file exported from : %r\n' % os.path.basename(bpy.data.filepath))
902 skinFile.write("CIwAnimSkin\n")
903 skinFile.write("{\n")
904 skinFile.write("\tskeleton \"%s\"\n" % ArmatureObjectName)
905 skinFile.write("\tmodel \"%s\"\n" % GeoName)
907 # now we have Bones grouped in the dictionary , along with the associated influenced vertex weighting
908 # So simply iterate the dictionary
909 Config.File.write("\t\".\models\%s.skin\"\n" % GeoName)
910 for pair_ListGroupIndices_ListAssignedVertices in useBonesDict.values():
911 skinFile.write("\tCIwAnimSkinSet\n")
912 skinFile.write("\t{\n")
913 skinFile.write("\t\tuseBones {")
914 for vertexGroupIndex in pair_ListGroupIndices_ListAssignedVertices[0]:
915 skinFile.write(" %s" % mapVertexGroupNames[vertexGroupIndex])
916 skinFile.write(" }\n")
917 skinFile.write("\t\tnumVerts %d\n" % len(pair_ListGroupIndices_ListAssignedVertices[1]))
918 for VertexWeightString in pair_ListGroupIndices_ListAssignedVertices[1]:
919 skinFile.write(VertexWeightString)
920 skinFile.write("\t}\n")
922 skinFile.write("}\n")
926 def AddVertexToDicionarySkinWeights(Config, Object, Mesh, Vertex, useBonesDict, mapVertexGroupNames, VertexIndex, RootBone, RootVertexGroup, BoneNames):
929 vertexGroupIndices = []
931 if (len(Vertex.groups)) > 4:
932 print ("ERROR Vertex %d is influenced by more than 4 bones\n" % (VertexIndex))
933 for VertexGroup in Vertex.groups:
934 if (VertexGroup.weight > 0):
935 groupName = Object.vertex_groups[VertexGroup.group].name
936 if groupName in BoneNames:
937 mapVertexGroupNames[VertexGroup.group] = StripBoneName(groupName)
938 if (len(vertexGroupIndices))<4: #ignore if more 4 bones are influencing the vertex
939 useBonesKey = useBonesKey + pow(2, VertexGroup.group)
940 vertexGroupIndices.append(VertexGroup.group)
941 weightTotal = weightTotal + VertexGroup.weight
942 if (weightTotal == 0):
943 bWeightTotZero = True #avoid divide by zero later on
946 print(" Warning Weight is ZERO for vertex %d => Add it to the root bone" % (VertexIndex))
947 RootBoneGroupIndex = FindUniqueIndexForRootBone(Object, RootVertexGroup)
948 mapVertexGroupNames[RootBoneGroupIndex] = StripBoneName(RootBone.name)
949 useBonesKey = pow(2, RootBoneGroupIndex)
950 vertexGroupIndices = list((RootBoneGroupIndex,))
954 bWeightTotZero = False
956 if len(vertexGroupIndices) > 0:
957 vertexGroupIndices.sort();
959 #build the vertex weight string: vertex indices, followed by influence weight for each bone
960 VertexWeightString = "\t\tvertWeights { %d" % (VertexIndex)
961 for vertexGroupIndex in vertexGroupIndices:
962 #get the weight of this specific VertexGroup (aka bone)
964 for VertexGroup in Vertex.groups:
965 if VertexGroup.group == vertexGroupIndex:
966 boneWeight = VertexGroup.weight
967 #calculate the influence of this bone compared to the total of weighting applied to this Vertex
968 if not bWeightTotZero:
969 VertexWeightString += ", %.7f" % (boneWeight / weightTotal)
971 VertexWeightString += ", %.7f" % (1.0 / len(vertexGroupIndices))
972 VertexWeightString += "}"
974 VertexWeightString += " // total weight was zero in blender , export assign it to the RootBone with weight 1."
975 if (len(Vertex.groups)) > 4:
976 VertexWeightString += " // vertex is associated to more than 4 bones in blender !! skip some bone association (was associated to %d bones)." % (len(Vertex.groups))
977 VertexWeightString += "\n"
979 #store in dictionnary information
980 if useBonesKey not in useBonesDict:
982 VertexList.append(VertexWeightString)
983 useBonesDict[useBonesKey] = (vertexGroupIndices, VertexList)
985 pair_ListGroupIndices_ListAssignedVertices = useBonesDict[useBonesKey]
986 pair_ListGroupIndices_ListAssignedVertices[1].append(VertexWeightString)
987 useBonesDict[useBonesKey] = pair_ListGroupIndices_ListAssignedVertices
989 print ("ERROR Vertex %d is not skinned (it doesn't belong to any vertex group\n" % (VertexIndex))
993 ############# ARMATURE: Bone export, and Bone animation export
996 def WriteArmatureParentRootBones(Config, Object, RootBonesList, skelFile):
998 if len(RootBonesList) > 1:
999 print(" /!\\ WARNING ,Marmelade need only one ROOT bone per armature, there is %d root bones " % len(RootBonesList))
1000 print(RootBonesList)
1002 PoseBones = Object.pose.bones
1003 for Bone in RootBonesList:
1005 print(" Writing Root Bone: {}...".format(Bone.name))
1007 PoseBone = PoseBones[Bone.name]
1008 WriteBonePosition(Config, Object, Bone, PoseBones, PoseBone, skelFile, True)
1011 WriteArmatureChildBones(Config, Object, Bone.children, skelFile)
1014 def WriteArmatureChildBones(Config, Object, BonesList, skelFile):
1015 PoseBones = Object.pose.bones
1016 for Bone in BonesList:
1018 print(" Writing Child Bone: {}...".format(Bone.name))
1019 PoseBone = PoseBones[Bone.name]
1020 WriteBonePosition(Config, Object, Bone, PoseBones, PoseBone, skelFile, True)
1024 WriteArmatureChildBones(Config, Object, Bone.children, skelFile)
1027 def WriteBonePosition(Config, Object, Bone, PoseBones, PoseBone, File, isRestPoseNotAnimPose):
1028 # Compute armature scale :
1029 # Many others exporter require sthe user to do Apply Scale in Object Mode to have 1,1,1 scale and so that anim data are correctly scaled
1030 # Here we retreive the Scale of the Armture Object.matrix_world.to_scale() and we use it to scale the bones :-)
1031 # So new Blender user should not complain about bad animation export if they forgot to apply the Scale to 1,1,1
1033 armScale = Object.matrix_world.to_scale()
1034 armRot = Object.matrix_world.to_quaternion()
1035 if isRestPoseNotAnimPose:
1036 #skel file, bone header
1037 File.write("\tCIwAnimBone\n")
1039 File.write("\t\tname \"%s\"\n" % StripBoneName(Bone.name))
1040 #get bone local matrix for rest pose
1042 File.write("\t\tparent \"%s\"\n" % StripBoneName(Bone.parent.name))
1043 localmat = Bone.parent.matrix_local.inverted() * Bone.matrix_local
1045 localmat = Bone.matrix_local
1047 #anim file, bone header
1048 File.write("\t\t\n")
1049 File.write("\t\tbone \"%s\" \n" % StripBoneName(Bone.name))
1050 localmat = PoseBone.matrix
1051 #get bone local matrix for current anim pose
1053 ParentPoseBone = PoseBones[Bone.parent.name]
1054 localmat = ParentPoseBone.matrix.inverted() * PoseBone.matrix
1056 localmat = PoseBone.matrix
1059 #Flip Y Z axes (only on root bone, other bones are local to root bones, so no need to rotate)
1060 X_ROT = mathutils.Matrix.Rotation(-math.pi / 2, 4, 'X')
1061 if Config.MergeModes > 0:
1062 # Merge mode is in world coordinates and not in model coordinates: so apply the world coordinate on the rootbone
1063 localmat = X_ROT * Object.matrix_world * localmat
1064 armScale.x = armScale.y = armScale.z = 1
1066 localmat= X_ROT * armRot.to_matrix().to_4x4() * localmat #apply the armature rotation on the root bone
1069 loc = localmat.to_translation()
1070 quat = localmat.to_quaternion()
1073 loc.x *= (armScale.x * Config.Scale)
1074 loc.y *= (armScale.y * Config.Scale)
1075 loc.z *= (armScale.z * Config.Scale)
1077 File.write("\t\tpos { %.9f, %.9f, %.9f }\n" % (loc[0], loc[1], loc[2]))
1078 File.write("\t\trot { %.9f, %.9f, %.9f, %.9f }\n" % (quat.w, quat.x, quat.y, quat.z))
1080 if isRestPoseNotAnimPose:
1084 def WriteKeyedAnimationSet(Config, Scene):
1085 for Object in [Object for Object in Config.ObjectList if Object.animation_data]:
1087 print(" Writing Animation Data for Object: {}".format(Object.name))
1089 if Config.ExportAnimationActions == 0 and Object.animation_data.action:
1090 actions.append(Object.animation_data.action)
1092 actions = bpy.data.actions[:]
1093 DefaultAction = Object.animation_data.action
1095 for Action in actions:
1096 if Config.ExportAnimationActions == 0:
1097 animFileName = StripName(Object.name)
1099 Object.animation_data.action = Action
1100 animFileName = "%s_%s" % (StripName(Object.name),StripName(Action.name))
1102 #Object animated (aka single bone object)
1103 #build key frame time list
1105 keyframeTimes = set()
1106 if Config.ExportAnimationFrames == 1:
1107 # Exports only key frames
1108 for FCurve in Action.fcurves:
1109 for Keyframe in FCurve.keyframe_points:
1110 if Keyframe.co[0] < Scene.frame_start:
1111 keyframeTimes.add(Scene.frame_start)
1112 elif Keyframe.co[0] > Scene.frame_end:
1113 keyframeTimes.add(Scene.frame_end)
1115 keyframeTimes.add(int(Keyframe.co[0]))
1117 # Exports all frames
1118 keyframeTimes.update(range(Scene.frame_start, Scene.frame_end + 1, 1))
1119 keyframeTimes = list(keyframeTimes)
1120 keyframeTimes.sort()
1121 if len(keyframeTimes):
1122 #Create the anim file for offset animation (or single bone animation
1123 animfullname = os.path.dirname(Config.FilePath) + os.sep + "anims" + os.sep + "%s_offset.anim" % animFileName
1126 ## ensure_dir(animfullname)
1127 ## if Config.Verbose:
1128 ## print(" Creating anim file (single bone animation) %s" % (animfullname))
1129 ## animFile = open(animfullname, "w")
1130 ## animFile.write('// anim file exported from : %r\n' % os.path.basename(bpy.data.filepath))
1131 ## animFile.write("CIwAnim\n")
1132 ## animFile.write("{\n")
1133 ## animFile.write("\tent \"%s\"\n" % (StripName(Object.name)))
1134 ## animFile.write("\tskeleton \"SingleBone\"\n")
1135 ## animFile.write("\t\t\n")
1137 ## Config.File.write("\t\".\\anims\\%s_offset.anim\"\n" % animFileName))
1139 ## for KeyframeTime in keyframeTimes:
1140 ## #Scene.frame_set(KeyframeTime)
1141 ## animFile.write("\tCIwAnimKeyFrame\n")
1142 ## animFile.write("\t{\n")
1143 ## animFile.write("\t\ttime %.2f // frame num %d \n" % (KeyframeTime/Config.AnimFPS, KeyframeTime))
1144 ## animFile.write("\t\t\n")
1145 ## animFile.write("\t\tbone \"SingleBone\" \n")
1148 ## for FCurve in Action.fcurves:
1149 ## if FCurve.data_path == "location" and FCurve.array_index == 0: posx = FCurve.evaluate(KeyframeTime)
1151 ## for FCurve in Action.fcurves:
1152 ## if FCurve.data_path == "location" and FCurve.array_index == 1: posy = FCurve.evaluate(KeyframeTime)
1154 ## for FCurve in Action.fcurves:
1155 ## if FCurve.data_path == "location" and FCurve.array_index == 2: posz = FCurve.evaluate(KeyframeTime)
1156 ## animFile.write("\t\tpos {%.9f,%.9f,%.9f}\n" % (posx, posy, posz))
1160 ## for FCurve in Action.fcurves:
1161 ## if FCurve.data_path == "rotation_euler" and FCurve.array_index == 1: rot[0] = FCurve.evaluate(KeyframeTime)
1163 ## for FCurve in Action.fcurves:
1164 ## if FCurve.data_path == "rotation_euler" and FCurve.array_index == 2: rot[1] = FCurve.evaluate(KeyframeTime)
1166 ## for FCurve in Action.fcurves:
1167 ## if FCurve.data_path == "rotation_euler" and FCurve.array_index == 3: rot[2] = FCurve.evaluate(KeyframeTime)
1168 ## rot = rot.to_quaternion()
1169 ## animFile.write("\t\trot {%.9f,%.9f,%.9f,%.9f}\n" % (rot[0], rot[1], rot[2], rot[3]))
1172 ## for FCurve in Action.fcurves:
1173 ## if FCurve.data_path == "scale" and FCurve.array_index == 0: scalex = FCurve.evaluate(KeyframeTime)
1175 ## for FCurve in Action.fcurves:
1176 ## if FCurve.data_path == "scale" and FCurve.array_index == 1: scaley = FCurve.evaluate(KeyframeTime)
1178 ## for FCurve in Action.fcurves:
1179 ## if FCurve.data_path == "scale" and FCurve.array_index == 2: scalez = FCurve.evaluate(KeyframeTime)
1180 ## animFile.write("\t\t//scale {%.9f,%.9f,%.9f}\n" % (scalex, scaley, scalez))
1182 ## animFile.write("\t}\n")
1183 ## animFile.write("}\n")
1188 print(" Object %s has no useable animation data." % (StripName(Object.name)))
1190 if Config.ExportArmatures and Object.type == "ARMATURE":
1192 print(" Writing Armature Bone Animation Data...\n")
1193 PoseBones = Object.pose.bones
1194 Bones = Object.data.bones
1195 #riged bones animated
1196 #build key frame time list
1197 keyframeTimes = set()
1198 if Config.ExportAnimationFrames == 1:
1199 # Exports only key frames
1200 for FCurve in Action.fcurves:
1201 for Keyframe in FCurve.keyframe_points:
1202 if Keyframe.co[0] < Scene.frame_start:
1203 keyframeTimes.add(Scene.frame_start)
1204 elif Keyframe.co[0] > Scene.frame_end:
1205 keyframeTimes.add(Scene.frame_end)
1207 keyframeTimes.add(int(Keyframe.co[0]))
1210 keyframeTimes.update(range(Scene.frame_start, Scene.frame_end + 1, 1))
1212 keyframeTimes = list(keyframeTimes)
1213 keyframeTimes.sort()
1215 print("Exporting frames: ")
1216 print(keyframeTimes)
1217 if (Scene.frame_preview_end > Scene.frame_end):
1218 print(" WARNING: END Frame of animation in UI preview is Higher than the Scene Frame end:\n Scene.frame_end %d versus Scene.frame_preview_end %d.\n"
1219 % (Scene.frame_end, Scene.frame_preview_end))
1220 print(" => You might need to change the Scene End Frame, to match the current UI preview frame end...\n=> if you don't want to miss end of animation.\n")
1222 if len(keyframeTimes):
1223 #Create the anim file
1224 animfullname = os.path.dirname(Config.FilePath) + os.sep + "anims" + os.sep + "%s.anim" % animFileName
1225 ensure_dir(animfullname)
1227 print(" Creating anim file (bones animation) %s\n" % (animfullname))
1228 print(" Frame count %d \n" % (len(keyframeTimes)))
1229 animFile = open(animfullname, "w")
1230 animFile.write('// anim file exported from : %r\n' % os.path.basename(bpy.data.filepath))
1231 animFile.write("CIwAnim\n")
1232 animFile.write("{\n")
1233 animFile.write("\tskeleton \"%s\"\n" % (StripName(Object.name)))
1234 animFile.write("\t\t\n")
1236 Config.File.write("\t\".\\anims\\%s.anim\"\n" % animFileName)
1238 for KeyframeTime in keyframeTimes:
1240 print(" Writing Frame %d:" % KeyframeTime)
1241 animFile.write("\tCIwAnimKeyFrame\n")
1242 animFile.write("\t{\n")
1243 animFile.write("\t\ttime %.2f // frame num %d \n" % (KeyframeTime / Config.AnimFPS, KeyframeTime))
1244 #for every frame write bones positions
1245 Scene.frame_set(KeyframeTime)
1246 for PoseBone in PoseBones:
1248 print(" Writing Bone: {}...".format(PoseBone.name))
1249 animFile.write("\t\t\n")
1251 Bone = Bones[PoseBone.name]
1252 WriteBonePosition(Config, Object, Bone, PoseBones, PoseBone, animFile, False)
1254 animFile.write("\t}\n")
1255 animFile.write("}\n")
1259 print(" Object %s has no useable animation data." % (StripName(Object.name)))
1260 if Config.ExportAnimationActions == 1:
1261 #set back the original default animation
1262 Object.animation_data.action = DefaultAction
1264 print(" Done") #Done with Object
1269 ################## Utilities
1271 def StripBoneName(name):
1272 return name.replace(" ", "")
1275 def StripName(Name):
1277 def ReplaceSet(String, OldSet, NewChar):
1278 for OldChar in OldSet:
1279 String = String.replace(OldChar, NewChar)
1284 NewName = ReplaceSet(Name, string.punctuation + " ", "_")
1289 d = os.path.dirname(f)
1290 if not os.path.exists(d):
1294 def CloseFile(Config):
1296 print("Closing File...")
1302 CoordinateSystems = (
1303 ("1", "Left-Handed", ""),
1304 ("2", "Right-Handed", ""),
1308 AnimationFrameModes = (
1310 ("1", "Keyframes Only", ""),
1311 ("2", "Full Animation", ""),
1314 AnimationActions = (
1315 ("0", "Default Animation", ""),
1316 ("1", "All Animations", ""),
1320 ("1", "All Objects", ""),
1321 ("2", "Selected Objects", ""),
1326 ("1", "Merge in one big Mesh", ""),
1327 ("2", "Merge in unique Geo File containing several meshes", ""),
1331 from bpy.props import StringProperty, EnumProperty, BoolProperty, IntProperty
1334 class MarmaladeExporter(bpy.types.Operator):
1335 """Export to the Marmalade model format (.group)"""
1337 bl_idname = "export.marmalade"
1338 bl_label = "Export Marmalade"
1340 filepath = StringProperty(subtype='FILE_PATH')
1342 ExportMode = EnumProperty(
1344 description="Select which objects to export. Only Mesh, Empty, " \
1345 "and Armature objects will be exported",
1349 MergeModes = EnumProperty(
1351 description="Select if objects should be merged in one Geo File (it can be usefull if a scene is done by several cube/forms)." \
1352 "Do not merge rigged character that have an armature.",
1357 Scale = IntProperty(
1358 name="Scale Percent",
1359 description="Scale percentage applied for export",
1360 default=100, min=1, max=1000)
1362 FlipNormals = BoolProperty(
1363 name="Flip Normals",
1366 ApplyModifiers = BoolProperty(
1367 name="Apply Modifiers",
1368 description="Apply object modifiers before export",
1370 ExportVertexColors = BoolProperty(
1371 name="Export Vertices Colors",
1372 description="Export colors set on vertices, if any",
1374 ExportMaterialColors = BoolProperty(
1375 name="Export Material Colors",
1376 description="Ambient color is exported on the Material",
1378 ExportTextures = BoolProperty(
1379 name="Export Textures and UVs",
1380 description="Exports UVs and Reference external image files to be used by the model",
1382 CopyTextureFiles = BoolProperty(
1383 name="Copy Textures Files",
1384 description="Copy referenced Textures files in the models\\textures directory",
1386 ExportArmatures = BoolProperty(
1387 name="Export Armatures",
1388 description="Export the bones of any armatures to deform meshes",
1390 ExportAnimationFrames = EnumProperty(
1391 name="Animations Frames",
1392 description="Select the type of animations to export. Only object " \
1393 "and armature bone animations can be exported. Keyframes exports only the keyed frames" \
1394 "Full Animation exports every frames, None disables animationq export. ",
1395 items=AnimationFrameModes,
1397 ExportAnimationActions = EnumProperty(
1398 name="Animations Actions",
1399 description="By default only the Default Animation Action assoiated to an armature is exported." \
1400 "However if you have defined several animations on the same armature,"\
1401 "you can select to export all animations. You can see the list of animation actions in the DopeSheet window.",
1402 items=AnimationActions,
1404 if bpy.context.scene:
1405 defFPS = bpy.context.scene.render.fps
1408 AnimFPS = IntProperty(
1409 name="Animation FPS",
1410 description="Frame rate used to export animation in seconds (can be used to artficially slow down the exported animation, or to speed up it",
1411 default=defFPS, min=1, max=300)
1414 CoordinateSystem = EnumProperty(
1416 description="Select a coordinate system to export to",
1417 items=CoordinateSystems,
1420 Verbose = BoolProperty(
1422 description="Run the exporter in debug mode. Check the console for output",
1425 def execute(self, context):
1427 FilePath = bpy.path.ensure_ext(self.filepath, ".group")
1429 Config = MarmaladeExporterSettings(context,
1431 CoordinateSystem=self.CoordinateSystem,
1432 FlipNormals=self.FlipNormals,
1433 ApplyModifiers=self.ApplyModifiers,
1435 AnimFPS=self.AnimFPS,
1436 ExportVertexColors=self.ExportVertexColors,
1437 ExportMaterialColors=self.ExportMaterialColors,
1438 ExportTextures=self.ExportTextures,
1439 CopyTextureFiles=self.CopyTextureFiles,
1440 ExportArmatures=self.ExportArmatures,
1441 ExportAnimationFrames=self.ExportAnimationFrames,
1442 ExportAnimationActions=self.ExportAnimationActions,
1443 ExportMode=self.ExportMode,
1444 MergeModes=self.MergeModes,
1445 Verbose=self.Verbose)
1447 # Exit edit mode before exporting, so current object states are exported properly.
1448 if bpy.ops.object.mode_set.poll():
1449 bpy.ops.object.mode_set(mode='OBJECT')
1451 ExportMadeWithMarmaladeGroup(Config)
1454 def invoke(self, context, event):
1455 if not self.filepath:
1456 self.filepath = bpy.path.ensure_ext(bpy.data.filepath, ".group")
1457 WindowManager = context.window_manager
1458 WindowManager.fileselect_add(self)
1459 return {'RUNNING_MODAL'}
1462 def menu_func(self, context):
1463 self.layout.operator(MarmaladeExporter.bl_idname, text="Marmalade cross-platform Apps (.group)")
1467 bpy.utils.register_module(__name__)
1469 bpy.types.INFO_MT_file_export.append(menu_func)
1473 bpy.utils.unregister_module(__name__)
1475 bpy.types.INFO_MT_file_export.remove(menu_func)
1478 if __name__ == "__main__":