-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathio_export_marmalade.py
1473 lines (1242 loc) · 61.7 KB
/
io_export_marmalade.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# ***** GPL LICENSE BLOCK *****
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# All rights reserved.
# ***** GPL LICENSE BLOCK *****
# Marmalade SDK is not responsible in any case of the following code.
# This Blender add-on is freely shared for the Blender and Marmalade user communities.
bl_info = {
"name": "Marmalade Cross-platform Apps (.group)",
"author": "Benoit Muller",
"version": (0, 6, 2),
"blender": (2, 63, 0),
"location": "File > Export > Marmalade cross-platform Apps (.group)",
"description": "Export Marmalade Format files (.group)",
"warning": "",
"wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/"
"Scripts/Import-Export/Marmalade_Exporter",
"tracker_url": "https://developer.blender.org/maniphest/task/edit/form/2/",
"category": "Import-Export"}
import os
import shutil
from math import radians
import bpy
from mathutils import Matrix
import mathutils
import math
import datetime
import subprocess
#Container for the exporter settings
class MarmaladeExporterSettings:
def __init__(self,
context,
FilePath,
CoordinateSystem=1,
FlipNormals=False,
ApplyModifiers=False,
Scale=100,
AnimFPS=30,
ExportVertexColors=True,
ExportMaterialColors=True,
ExportTextures=True,
CopyTextureFiles=True,
ExportArmatures=False,
ExportAnimationFrames=0,
ExportAnimationActions=0,
ExportMode=1,
MergeModes=0,
Verbose=False):
self.context = context
self.FilePath = FilePath
self.CoordinateSystem = int(CoordinateSystem)
self.FlipNormals = FlipNormals
self.ApplyModifiers = ApplyModifiers
self.Scale = Scale
self.AnimFPS = AnimFPS
self.ExportVertexColors = ExportVertexColors
self.ExportMaterialColors = ExportMaterialColors
self.ExportTextures = ExportTextures
self.CopyTextureFiles = CopyTextureFiles
self.ExportArmatures = ExportArmatures
self.ExportAnimationFrames = int(ExportAnimationFrames)
self.ExportAnimationActions = int(ExportAnimationActions)
self.ExportMode = int(ExportMode)
self.MergeModes = int(MergeModes)
self.Verbose = Verbose
self.WarningList = []
def ExportMadeWithMarmaladeGroup(Config):
print("----------\nExporting to {}".format(Config.FilePath))
if Config.Verbose:
print("Opening File...")
Config.File = open(Config.FilePath, "w")
if Config.Verbose:
print("Done")
if Config.Verbose:
print("writing group header")
Config.File.write('// Marmalade group file exported from : %s\n' % bpy.data.filepath)
Config.File.write('// Exported %s\n' % str(datetime.datetime.now()))
Config.File.write("CIwResGroup\n{\n\tname \"%s\"\n" % bpy.path.display_name_from_filepath(Config.FilePath))
if Config.Verbose:
print("Generating Object list for export... (Root parents only)")
if Config.ExportMode == 1:
Config.ExportList = [Object for Object in Config.context.scene.objects
if Object.type in {'ARMATURE', 'EMPTY', 'MESH'}
and Object.parent is None]
else:
ExportList = [Object for Object in Config.context.selected_objects
if Object.type in {'ARMATURE', 'EMPTY', 'MESH'}]
Config.ExportList = [Object for Object in ExportList
if Object.parent not in ExportList]
if Config.Verbose:
print(" List: {}\nDone".format(Config.ExportList))
if Config.Verbose:
print("Setting up...")
if Config.ExportAnimationFrames:
if Config.Verbose:
print(bpy.context.scene)
print(bpy.context.scene.frame_current)
CurrentFrame = bpy.context.scene.frame_current
if Config.Verbose:
print("Done")
Config.ObjectList = []
if Config.Verbose:
print("Writing Objects...")
WriteObjects(Config, Config.ExportList)
if Config.Verbose:
print("Done")
if Config.Verbose:
print("Objects Exported: {}".format(Config.ExportList))
if Config.ExportAnimationFrames:
if Config.Verbose:
print("Writing Animation...")
WriteKeyedAnimationSet(Config, bpy.context.scene)
bpy.context.scene.frame_current = CurrentFrame
if Config.Verbose:
print("Done")
Config.File.write("}\n")
CloseFile(Config)
print("Finished")
def GetObjectChildren(Parent):
return [Object for Object in Parent.children
if Object.type in {'ARMATURE', 'EMPTY', 'MESH'}]
#Returns the file path of first image texture from Material.
def GetMaterialTextureFullPath(Config, Material):
if Material:
#Create a list of Textures that have type "IMAGE"
ImageTextures = [Material.texture_slots[TextureSlot].texture for TextureSlot in Material.texture_slots.keys() if Material.texture_slots[TextureSlot].texture.type == "IMAGE"]
#Refine a new list with only image textures that have a file source
TexImages = [Texture.image for Texture in ImageTextures if getattr(Texture.image, "source", "") == "FILE"]
ImageFiles = [Texture.image.filepath for Texture in ImageTextures if getattr(Texture.image, "source", "") == "FILE"]
if TexImages:
filepath = TexImages[0].filepath
if TexImages[0].packed_file:
TexImages[0].unpack()
if not os.path.exists(filepath):
#try relative path to the blend file
filepath = os.path.dirname(bpy.data.filepath) + filepath
#Marmalade doesn't like jpeg/tif so try to convert in png on the fly
if (TexImages[0].file_format == 'JPEG' or TexImages[0].file_format == 'TIFF') and os.path.exists(filepath):
marmaladeConvert = os.path.expandvars("%S3E_DIR%\\..\\tools\\ImageMagick\\win32\\convert.exe")
if (os.path.exists(marmaladeConvert)):
srcImagefilepath = filepath
filepath = os.path.splitext(filepath)[0] + '.png'
if Config.Verbose:
print(" /!\\ Converting Texture %s in PNG: %s{}..." % (TexImages[0].file_format, filepath))
print('"%s" "%s" "%s"' % (marmaladeConvert, srcImagefilepath, filepath))
subprocess.call([marmaladeConvert, srcImagefilepath, filepath])
return filepath
return None
def WriteObjects(Config, ObjectList, geoFile=None, mtlFile=None, GeoModel=None, bChildObjects=False):
Config.ObjectList += ObjectList
if bChildObjects == False and Config.MergeModes > 0:
if geoFile == None:
#we merge objects, so use name of group file for the name of Geo
geoFile, mtlFile = CreateGeoMtlFiles(Config, bpy.path.display_name_from_filepath(Config.FilePath))
GeoModel = CGeoModel(bpy.path.display_name_from_filepath(Config.FilePath))
for Object in ObjectList:
if Config.Verbose:
print(" Writing Object: {}...".format(Object.name))
if Config.ExportArmatures and Object.type == "ARMATURE":
Armature = Object.data
ParentList = [Bone for Bone in Armature.bones if Bone.parent is None]
if Config.Verbose:
print(" Writing Armature Bones...")
#Create the skel file
skelfullname = os.path.dirname(Config.FilePath) + os.sep + "models" + os.sep + "%s.skel" % (StripName(Object.name))
ensure_dir(skelfullname)
if Config.Verbose:
print(" Creating skel file %s" % (skelfullname))
skelFile = open(skelfullname, "w")
skelFile.write('// skel file exported from : %r\n' % os.path.basename(bpy.data.filepath))
skelFile.write("CIwAnimSkel\n")
skelFile.write("{\n")
skelFile.write("\tnumBones %d\n" % (len(Armature.bones)))
Config.File.write("\t\".\models\%s.skel\"\n" % (StripName(Object.name)))
WriteArmatureParentRootBones(Config, Object, ParentList, skelFile)
skelFile.write("}\n")
skelFile.close()
if Config.Verbose:
print(" Done")
ChildList = GetObjectChildren(Object)
if Config.ExportMode == 2: # Selected Objects Only
ChildList = [Child for Child in ChildList
if Child in Config.context.selected_objects]
if Config.Verbose:
print(" Writing Children...")
WriteObjects(Config, ChildList, geoFile, mtlFile, GeoModel, True)
if Config.Verbose:
print(" Done Writing Children")
if Object.type == "MESH":
if Config.Verbose:
print(" Generating Mesh...")
if Config.ApplyModifiers:
if Config.ExportArmatures:
#Create a copy of the object and remove all armature modifiers so an unshaped
#mesh can be created from it.
Object2 = Object.copy()
for Modifier in [Modifier for Modifier in Object2.modifiers if Modifier.type == "ARMATURE"]:
Object2.modifiers.remove(Modifier)
Mesh = Object2.to_mesh(bpy.context.scene, True, "PREVIEW")
else:
Mesh = Object.to_mesh(bpy.context.scene, True, "PREVIEW")
else:
Mesh = Object.to_mesh(bpy.context.scene, False, "PREVIEW")
if Config.Verbose:
print(" Done")
print(" Writing Mesh...")
# Flip ZY axis (Blender Z up: Marmalade: Y up) ans Scale appropriately
X_ROT = mathutils.Matrix.Rotation(-math.pi / 2, 4, 'X')
if Config.MergeModes == 0:
# No merge, so all objects are exported in MODEL SPACE and not in world space
# Calculate Scale of the Export
meshScale = Object.matrix_world.to_scale() # Export is working, even if user doesn't have use apply scale in Edit mode.
scalematrix = Matrix()
scalematrix[0][0] = meshScale.x * Config.Scale
scalematrix[1][1] = meshScale.y * Config.Scale
scalematrix[2][2] = meshScale.z * Config.Scale
meshRot = Object.matrix_world.to_quaternion() # Export is working, even if user doesn't have use apply Rotation in Edit mode.
Mesh.transform(X_ROT * meshRot.to_matrix().to_4x4() * scalematrix)
else:
# In Merge mode, we need to keep relative postion of each objects, so we export in WORLD SPACE
SCALE_MAT = mathutils.Matrix.Scale(Config.Scale, 4)
Mesh.transform(SCALE_MAT * X_ROT * Object.matrix_world)
# manage merge options
if Config.MergeModes == 0:
#one geo per Object, so use name of Object for the Geo file
geoFile, mtlFile = CreateGeoMtlFiles(Config, StripName(Object.name))
GeoModel = CGeoModel(StripName(Object.name))
# Write the Mesh in the Geo file
WriteMesh(Config, Object, Mesh, geoFile, mtlFile, GeoModel)
if Config.MergeModes == 0:
# no merge so finalize the file, and discard the file and geo class
FinalizeGeoMtlFiles(Config, geoFile, mtlFile)
geoFile = None
mtlFile = None
GeoModel = None
elif Config.MergeModes == 1:
# merge in one Mesh, so keep the Geo class and prepare to change object
GeoModel.NewObject()
elif Config.MergeModes == 2:
# merge several Meshes in one file: so clear the mesh data that we just written in the file,
# but keep Materials info that need to be merged across objects
GeoModel.ClearAllExceptMaterials()
if Config.Verbose:
print(" Done")
if Config.ApplyModifiers and Config.ExportArmatures:
bpy.data.objects.remove(Object2)
bpy.data.meshes.remove(Mesh)
if Config.Verbose:
print(" Done Writing Object: {}".format(Object.name))
if bChildObjects == False:
# we have finish to do all objects
if GeoModel:
if Config.MergeModes == 1:
# we have Merges all objects in one Mesh, so time to write this big mesh in the file
GeoModel.PrintGeoMesh(geoFile)
# time to write skinfile if any
if len(GeoModel.useBonesDict) > 0:
# some mesh was not modified by the armature. so we must skinned the merged mesh.
# So unskinned vertices from unarmatured meshes, are assigned to the root bone of the armature
for i in range(0, len(GeoModel.vList)):
if not i in GeoModel.skinnedVertices:
GeoModel.skinnedVertices.append(i)
useBonesKey = pow(2, GeoModel.armatureRootBoneIndex)
vertexGroupIndices = list((GeoModel.armatureRootBoneIndex,))
if useBonesKey not in GeoModel.useBonesDict:
GeoModel.mapVertexGroupNames[GeoModel.armatureRootBoneIndex] = StripBoneName(GeoModel.armatureRootBone.name)
VertexList = []
VertexList.append("\t\tvertWeights { %d, 1.0}" % i)
GeoModel.useBonesDict[useBonesKey] = (vertexGroupIndices, VertexList)
else:
pair_ListGroupIndices_ListAssignedVertices = GeoModel.useBonesDict[useBonesKey]
pair_ListGroupIndices_ListAssignedVertices[1].append("\t\tvertWeights { %d, 1.0}" % i)
GeoModel.useBonesDict[useBonesKey] = pair_ListGroupIndices_ListAssignedVertices
# now generates the skin file
PrintSkinWeights(Config, GeoModel.armatureObjectName, GeoModel.useBonesDict, GeoModel.mapVertexGroupNames, GeoModel.name)
if Config.MergeModes > 0:
WriteMeshMaterialsForGeoModel(Config, mtlFile, GeoModel)
FinalizeGeoMtlFiles(Config, geoFile, mtlFile)
geoFile = None
mtlFile = None
GeoModel = None
def CreateGeoMtlFiles(Config, Name):
#Create the geo file
geofullname = os.path.dirname(Config.FilePath) + os.sep + "models" + os.sep + "%s.geo" % Name
ensure_dir(geofullname)
if Config.Verbose:
print(" Creating geo file %s" % (geofullname))
geoFile = open(geofullname, "w")
geoFile.write('// geo file exported from : %r\n' % os.path.basename(bpy.data.filepath))
geoFile.write("CIwModel\n")
geoFile.write("{\n")
geoFile.write("\tname \"%s\"\n" % Name)
# add it to the group
Config.File.write("\t\".\models\%s.geo\"\n" % Name)
# Create the mtl file
mtlfullname = os.path.dirname(Config.FilePath) + os.sep + "models" + os.sep + "%s.mtl" % Name
ensure_dir(mtlfullname)
if Config.Verbose:
print(" Creating mtl file %s" % (mtlfullname))
mtlFile = open(mtlfullname, "w")
mtlFile.write('// mtl file exported from : %r\n' % os.path.basename(bpy.data.filepath))
return geoFile, mtlFile
def FinalizeGeoMtlFiles(Config, geoFile, mtlFile):
if Config.Verbose:
print(" Closing geo file")
geoFile.write("}\n")
geoFile.close()
if Config.Verbose:
print(" Closing mtl file")
mtlFile.close()
def WriteMesh(Config, Object, Mesh, geoFile=None, mtlFile=None, GeoModel=None):
if geoFile == None or mtlFile == None:
print (" ERROR not geo file arguments in WriteMesh method")
return
if GeoModel == None:
print (" ERROR not GeoModel arguments in WriteMesh method")
return
BuildOptimizedGeo(Config, Object, Mesh, GeoModel)
if Config.MergeModes == 0 or Config.MergeModes == 2:
#if we don't merge, or if we write several meshes into one file ... write the mesh everytime we do an object
GeoModel.PrintGeoMesh(geoFile)
if Config.Verbose:
print(" Done\n Writing Mesh Materials...")
if Config.MergeModes == 0:
#No merge, so we can diretly write the Mtl file associated to this object
WriteMeshMaterialsForGeoModel(Config, mtlFile, GeoModel)
if Config.Verbose:
print(" Done")
if Config.ExportArmatures:
if Config.Verbose:
print(" Writing Mesh Weights...")
WriteMeshSkinWeightsForGeoModel(Config, Object, Mesh, GeoModel)
if Config.Verbose:
print(" Done")
###### optimized version fo Export, can be used also to merge several object in one single geo File ######
# CGeoModel
# -> List Vertices
# -> List Normales
# -> List uv 0
# -> List uv 1
# -> List Vertex Colors
# -> List Materials
# -> Material name
# -> Blender Material Object
# -> List Tris -> Stream Indices v,vn,uv0,uv1,vc
# -> List Quads -> Stream Indices v,vn,uv0,uv1,vc
#############
#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
#############
class CGeoIndexList:
__slots__ = "v", "vn", "uv0", "uv1", "vc"
def __init__(self, v, vn, uv0, uv1, vc):
self.v = v
self.vn = vn
self.uv0 = uv0
self.uv1 = uv1
self.vc = vc
#############
#Store a Quad or a Tri in marmalade geo format : 3 or 4 CIndexList depending it is a Tri or a Quad
#############
class CGeoPoly:
__slots__ = "pointsList",
def __init__(self):
self.pointsList = []
def AddPoint(self, v, vn, uv0, uv1, vc):
self.pointsList.append( CGeoIndexList(v, vn, uv0, uv1, vc))
def PointsCount(self):
return len(self.pointsList)
def PrintPoly(self, geoFile):
if len(self.pointsList) == 3:
geoFile.write("\t\t\t\tt ")
if len(self.pointsList) == 4:
geoFile.write("\t\t\t\tq ")
for point in self.pointsList:
geoFile.write(" {%d, %d, %d, %d, %d}" % (point.v, point.vn, point.uv0, point.uv1, point.vc))
geoFile.write("\n")
#############
#Store all the poly (tri or quad) assigned to a Material in marmalade geo format
#############
class CGeoMaterialPolys:
__slots__ = "name", "material", "quadList", "triList", "currentPoly"
def __init__(self, name, material=None):
self.name = name
self.material = material
self.quadList = []
self.triList = []
self.currentPoly = None
def BeginPoly(self):
self.currentPoly = CGeoPoly()
def AddPoint(self, v, vn, uv0, uv1, vc):
self.currentPoly.AddPoint(v, vn, uv0, uv1, vc)
def EndPoly(self):
if (self.currentPoly.PointsCount() == 3):
self.triList.append(self.currentPoly)
if (self.currentPoly.PointsCount() == 4):
self.quadList.append(self.currentPoly)
self.currentPoly = None
def ClearPolys(self):
self.quadList = []
self.triList = []
self.currentPoly = None
def PrintMaterialPolys(self, geoFile):
geoFile.write("\t\tCSurface\n")
geoFile.write("\t\t{\n")
geoFile.write("\t\t\tmaterial \"%s\"\n" % self.name)
if self.triList:
geoFile.write("\t\t\tCTris\n")
geoFile.write("\t\t\t{\n")
geoFile.write("\t\t\t\tnumTris %d\n" % (len(self.triList)))
for poly in self.triList:
poly.PrintPoly(geoFile)
geoFile.write("\t\t\t}\n")
if self.quadList:
geoFile.write("\t\t\tCQuads\n")
geoFile.write("\t\t\t{\n")
geoFile.write("\t\t\t\tnumQuads %d\n" % (len(self.quadList)))
for poly in self.quadList:
poly.PrintPoly(geoFile)
geoFile.write("\t\t\t}\n")
geoFile.write("\t\t}\n")
#############
#Store all the information on a Model/Mesh (vertices, normal, certcies color, uv0, uv1, TRI, QUAD) in marmalade geo format
#############
class CGeoModel:
__slots__ = ("name", "MaterialsDict", "vList", "vnList", "vcList", "uv0List", "uv1List",
"currentMaterialPolys", "vbaseIndex","vnbaseIndex", "uv0baseIndex", "uv1baseIndex",
"armatureObjectName", "useBonesDict", "mapVertexGroupNames", "armatureRootBone", "armatureRootBoneIndex", "skinnedVertices")
def __init__(self, name):
self.name = name
self.MaterialsDict = {}
self.vList = []
self.vnList = []
self.vcList = []
self.uv0List = []
self.uv1List = []
self.currentMaterialPolys = None
#used xx baseIndex are used when merging several blender objects into one Mesh in the geo file (internal offset)
self.vbaseIndex = 0
self.vnbaseIndex = 0
self.uv0baseIndex = 0
self.uv1baseIndex = 0
# Store some information for skin management , when we merge several object in one big mesh (MergeModes 1)
# can only work if in the object list only one is rigged with an armature... and if it is located in 0,0,0
self.armatureObjectName = ""
#useBonesKey : bit field, where each bit is a VertexGroup.Index): Sum(2^VertGroupIndex).
#useBonesDict[useBonesKey] = tuple(VertexGroups.group, list(Vertex))
self.useBonesDict = {}
self.mapVertexGroupNames = {}
self.armatureRootBone = None
self.armatureRootBoneIndex = 0
self.skinnedVertices = []
def AddVertex(self, vertex):
self.vList.append(vertex.copy())
def AddVertexNormal(self, vertexN):
self.vnList.append(vertexN.copy())
# 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)
def AddVertexUV0(self, u, v):
self.uv0List.append((u, v))
return len(self.uv0List) - 1 - self.uv0baseIndex
def AddVertexUV1(self, u, v):
self.uv1List.append((u, v))
return len(self.uv1List) - 1 - self.uv1baseIndex
# 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)
def AddVertexColor(self, r, g, b, a):
for i in range(0, len(self.vcList)):
col = self.vcList[i]
if col[0] == r and col[1] == g and col[2] == b and col[3] == a:
return i
self.vcList.append((r, g, b, a))
return len(self.vcList)-1
def BeginPoly(self, MaterialName, material=None):
if MaterialName not in self.MaterialsDict:
self.currentMaterialPolys = CGeoMaterialPolys(MaterialName, material)
else:
self.currentMaterialPolys = self.MaterialsDict[MaterialName]
self.currentMaterialPolys.BeginPoly()
def AddPoint(self, v, vn, uv0, uv1, vc):
if v != -1:
v += self.vbaseIndex
if vn != -1:
vn += self.vnbaseIndex
if uv0 != -1:
uv0 += self.uv0baseIndex
if uv1 != -1:
uv1 += self.uv1baseIndex
self.currentMaterialPolys.AddPoint(v, vn, uv0, uv1, vc)
def EndPoly(self):
self.currentMaterialPolys.EndPoly()
self.MaterialsDict[self.currentMaterialPolys.name] = self.currentMaterialPolys
self.currentMaterialPolys = None
def NewObject(self):
#used in Merge mode 1: allows to merge several blender objects into one Mesh.
self.vbaseIndex = len(self.vList)
self.vnbaseIndex = len(self.vnList)
self.uv0baseIndex = len(self.uv0List)
self.uv1baseIndex = len(self.uv1List)
def ClearAllExceptMaterials(self):
#used in Merge mode 2: one geo with several mesh
self.vList = []
self.vnList = []
self.vcList = []
self.uv0List = []
self.uv1List = []
self.currentMaterialPolys = None
self.vbaseIndex = 0
self.vnbaseIndex = 0
self.uv0baseIndex = 0
self.uv1baseIndex = 0
for GeoMaterialPolys in self.MaterialsDict.values():
GeoMaterialPolys.ClearPolys()
self.useBonesDict = {}
self.mapVertexGroupNames = {}
self.armatureObjectName = ""
self.armatureRootBone = None
self.armatureRootBoneIndex = 0
self.skinnedVertices = []
def PrintGeoMesh(self, geoFile):
geoFile.write("\tCMesh\n")
geoFile.write("\t{\n")
geoFile.write("\t\tname \"%s\"\n" % (StripName(self.name)))
if self.vList:
geoFile.write("\t\tCVerts\n")
geoFile.write("\t\t{\n")
geoFile.write("\t\t\tnumVerts %d\n" % len(self.vList))
for vertex in self.vList:
geoFile.write("\t\t\tv { %.9f, %.9f, %.9f }\n" % (vertex[0], vertex[1], vertex[2]))
geoFile.write("\t\t}\n")
if self.vnList:
geoFile.write("\t\tCVertNorms\n")
geoFile.write("\t\t{\n")
geoFile.write("\t\t\tnumVertNorms %d\n" % len(self.vnList))
for vertexn in self.vnList:
geoFile.write("\t\t\tvn { %.9f, %.9f, %.9f }\n" % (vertexn[0], vertexn[1], vertexn[2]))
geoFile.write("\t\t}\n")
if self.vcList:
geoFile.write("\t\tCVertCols\n")
geoFile.write("\t\t{\n")
geoFile.write("\t\t\tnumVertCols %d\n" % len(self.vcList))
for color in self.vcList:
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
geoFile.write("\t\t}\n")
if self.uv0List:
geoFile.write("\t\tCUVs\n")
geoFile.write("\t\t{\n")
geoFile.write("\t\t\tsetID 0\n")
geoFile.write("\t\t\tnumUVs %d\n" % len(self.uv0List))
for uv in self.uv0List:
geoFile.write("\t\t\tuv { %.9f, %.9f }\n" % (uv[0], uv[1]))
geoFile.write("\t\t}\n")
if self.uv1List:
geoFile.write("\t\tCUVs\n")
geoFile.write("\t\t{\n")
geoFile.write("\t\t\tsetID 1\n")
geoFile.write("\t\t\tnumUVs %d\n" % len(self.uv1List))
for uv in self.uv1List:
geoFile.write("\t\t\tuv { %.9f, %.9f }\n" % (uv[0], uv[1]))
geoFile.write("\t\t}\n")
for GeoMaterialPolys in self.MaterialsDict.values():
GeoMaterialPolys.PrintMaterialPolys(geoFile)
geoFile.write("\t}\n")
def GetMaterialList(self):
return list(self.MaterialsDict.keys())
def GetMaterialByName(self, name):
if name in self.MaterialsDict:
return self.MaterialsDict[name].material
else:
return None
#############
# iterates faces, vertices ... and store the information in the GeoModel container
def BuildOptimizedGeo(Config, Object, Mesh, GeoModel):
if GeoModel == None:
GeoModel = CGeoModel(filename, Object.name)
#Ensure tessfaces data are here
Mesh.update (calc_tessface=True)
#Store Vertex stream, and Normal stream (use directly the order from blender collection
for Vertex in Mesh.vertices:
GeoModel.AddVertex(Vertex.co)
Normal = Vertex.normal
if Config.FlipNormals:
Normal = -Normal
GeoModel.AddVertexNormal(Normal)
#Check if some colors have been defined
vertexColors = None
if Config.ExportVertexColors and (len(Mesh.vertex_colors) > 0):
vertexColors = Mesh.tessface_vertex_colors[0].data
#Check if some uv coordinates have been defined
UVCoordinates = None
if Config.ExportTextures and (len(Mesh.uv_textures) > 0):
for UV in Mesh.tessface_uv_textures:
if UV.active_render:
UVCoordinates = UV.data
break
#Iterate on Faces and Store the poly (quad or tri) and the associate colors,UVs
for Face in Mesh.tessfaces:
# stream for vertex (we use the same for normal)
Vertices = list(Face.vertices)
if Config.CoordinateSystem == 1:
Vertices = Vertices[::-1]
# stream for vertex colors
if vertexColors:
MeshColor = vertexColors[Face.index]
if len(Vertices) == 3:
FaceColors = list((MeshColor.color1, MeshColor.color2, MeshColor.color3))
else:
FaceColors = list((MeshColor.color1, MeshColor.color2, MeshColor.color3, MeshColor.color4))
if Config.CoordinateSystem == 1:
FaceColors = FaceColors[::-1]
colorIndex = []
for color in FaceColors:
index = GeoModel.AddVertexColor(color[0], color[1], color[2], 1) #rgba => no alpha on vertex color in Blender so use 1
colorIndex.append(index)
else:
colorIndex = list((-1,-1,-1,-1))
# stream for UV0 coordinates
if UVCoordinates:
uvFace = UVCoordinates[Face.index]
uvVertices = []
for uvVertex in uvFace.uv:
uvVertices.append(tuple(uvVertex))
if Config.CoordinateSystem == 1:
uvVertices = uvVertices[::-1]
uv0Index = []
for uvVertex in uvVertices:
index = GeoModel.AddVertexUV0(uvVertex[0], 1 - uvVertex[1])
uv0Index.append(index)
else:
uv0Index = list((-1, -1, -1, -1))
# stream for UV1 coordinates
uv1Index = list((-1, -1, -1, -1))
mat = None
# find the associated material
if Face.material_index < len(Mesh.materials):
mat = Mesh.materials[Face.material_index]
if mat:
matName = mat.name
else:
matName = "NoMaterialAssigned" # There is no material assigned in blender !!!, exporter have generated a default one
# now on the material, generates the tri/quad in v,vn,uv0,uv1,vc stream index
GeoModel.BeginPoly(matName, mat)
for i in range(0, len(Vertices)):
GeoModel.AddPoint(Vertices[i], Vertices[i], uv0Index[i], uv1Index[i], colorIndex[i])
GeoModel.EndPoly()
#############
# Get the list of Material in use by the CGeoModel
def WriteMeshMaterialsForGeoModel(Config, mtlFile, GeoModel):
for matName in GeoModel.GetMaterialList():
Material = GeoModel.GetMaterialByName(matName)
WriteMaterial(Config, mtlFile, Material)
def WriteMaterial(Config, mtlFile, Material=None):
mtlFile.write("CIwMaterial\n")
mtlFile.write("{\n")
if Material:
mtlFile.write("\tname \"%s\"\n" % Material.name)
if Config.ExportMaterialColors:
#if bpy.context.scene.world:
# MatAmbientColor = Material.ambient * bpy.context.scene.world.ambient_color
MatAmbientColor = Material.ambient * Material.diffuse_color
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)))
MatDiffuseColor = 255 * Material.diffuse_intensity * Material.diffuse_color
MatDiffuseColor = min((255, 255, 255)[:],MatDiffuseColor[:])
mtlFile.write("\tcolDiffuse {%.2f,%.2f,%.2f} \n" % (MatDiffuseColor[:]))
MatSpecularColor = 255 * Material.specular_intensity * Material.specular_color
MatSpecularColor = min((255, 255, 255)[:],MatSpecularColor[:])
mtlFile.write("\tcolSpecular {%.2f,%.2f,%.2f} \n" % (MatSpecularColor[:]))
# EmitColor = Material.emit * Material.diffuse_color
# mtlFile.write("\tcolEmissive {%.2f,%.2f,%.2f} \n" % (EmitColor* 255)[:])
else:
mtlFile.write("\tname \"NoMaterialAssigned\" // There is no material assigned in blender !!!, exporter have generated a default one\n")
#Copy texture
if Config.ExportTextures:
Texture = GetMaterialTextureFullPath(Config, Material)
if Texture:
mtlFile.write("\ttexture0 .\\textures\\%s\n" % (bpy.path.basename(Texture)))
if Config.CopyTextureFiles:
if not os.path.exists(Texture):
#try relative path to the blend file
Texture = os.path.dirname(bpy.data.filepath) + Texture
if os.path.exists(Texture):
textureDest = os.path.dirname(Config.FilePath) + os.sep + "models" + os.sep + "textures" + os.sep + ("%s" % bpy.path.basename(Texture))
ensure_dir(textureDest)
if Config.Verbose:
print(" Copying the texture file %s ---> %s" % (Texture, textureDest))
shutil.copy(Texture, textureDest)
else:
if Config.Verbose:
print(" CANNOT Copy texture file (not found) %s" % (Texture))
mtlFile.write("}\n")
def GetFirstRootBone(ArmatureObject):
ArmatureBones = ArmatureObject.data.bones
ParentBoneList = [Bone for Bone in ArmatureBones if Bone.parent is None]
if ParentBoneList:
return ParentBoneList[0]
return None
def GetVertexGroupFromBone(Object, Bone):
if Bone:
vertexGroupList = [VertexGroup for VertexGroup in Object.vertex_groups if VertexGroup.name == Bone.name]
if vertexGroupList:
return vertexGroupList[0]
return None
def GetBoneListNames(Bones):
boneList = []
for Bone in Bones:
boneList.append(Bone.name)
boneList += GetBoneListNames(Bone.children)
return boneList
def FindUniqueIndexForRootBone(Object, RootVertexGroup):
if RootVertexGroup:
return RootVertexGroup.index
else:
#If there is not VertexGroup associated to the root bone name, we don't have a vertex index.
#so use the next available free index
return len(Object.vertex_groups)
def WriteMeshSkinWeightsForGeoModel(Config, Object, Mesh, GeoModel):
ArmatureList = [Modifier for Modifier in Object.modifiers if Modifier.type == "ARMATURE"]
if ArmatureList:
ArmatureObject = ArmatureList[0].object
if ArmatureObject is None:
return
RootBone = GetFirstRootBone(ArmatureObject)
RootVertexGroup = GetVertexGroupFromBone(Object, RootBone)
BoneNames = GetBoneListNames(ArmatureObject.data.bones)
GeoModel.armatureObjectName = StripName(ArmatureObject.name)
if RootBone:
GeoModel.armatureRootBone = RootBone
GeoModel.armatureRootBoneIndex = FindUniqueIndexForRootBone(Object, RootVertexGroup)
# Marmalade need to declare a vertex per list of affected bones
# so first we have to get all the combinations of affected bones that exist in the mesh
# 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
for Vertex in Mesh.vertices:
VertexIndex = Vertex.index + GeoModel.vbaseIndex
AddVertexToDicionarySkinWeights(Config, Object, Mesh, Vertex, GeoModel.useBonesDict, GeoModel.mapVertexGroupNames, VertexIndex, RootBone, RootVertexGroup, BoneNames)
GeoModel.skinnedVertices.append(VertexIndex)
if Config.MergeModes != 1:
# write skin file directly
PrintSkinWeights(Config, GeoModel.armatureObjectName, GeoModel.useBonesDict, GeoModel.mapVertexGroupNames, StripName(Object.name))
def PrintSkinWeights(Config, ArmatureObjectName, useBonesDict, mapVertexGroupNames, GeoName):
#Create the skin file
skinfullname = os.path.dirname(Config.FilePath) + os.sep + "models" + os.sep + "%s.skin" % GeoName
ensure_dir(skinfullname)
if Config.Verbose:
print(" Creating skin file %s" % (skinfullname))
skinFile = open(skinfullname, "w")
skinFile.write('// skin file exported from : %r\n' % os.path.basename(bpy.data.filepath))
skinFile.write("CIwAnimSkin\n")
skinFile.write("{\n")
skinFile.write("\tskeleton \"%s\"\n" % ArmatureObjectName)
skinFile.write("\tmodel \"%s\"\n" % GeoName)
# now we have Bones grouped in the dictionary , along with the associated influenced vertex weighting
# So simply iterate the dictionary
Config.File.write("\t\".\models\%s.skin\"\n" % GeoName)
for pair_ListGroupIndices_ListAssignedVertices in useBonesDict.values():
skinFile.write("\tCIwAnimSkinSet\n")
skinFile.write("\t{\n")
skinFile.write("\t\tuseBones {")
for vertexGroupIndex in pair_ListGroupIndices_ListAssignedVertices[0]:
skinFile.write(" %s" % mapVertexGroupNames[vertexGroupIndex])
skinFile.write(" }\n")
skinFile.write("\t\tnumVerts %d\n" % len(pair_ListGroupIndices_ListAssignedVertices[1]))
for VertexWeightString in pair_ListGroupIndices_ListAssignedVertices[1]:
skinFile.write(VertexWeightString)
skinFile.write("\t}\n")
skinFile.write("}\n")
skinFile.close()
def AddVertexToDicionarySkinWeights(Config, Object, Mesh, Vertex, useBonesDict, mapVertexGroupNames, VertexIndex, RootBone, RootVertexGroup, BoneNames):
#build useBones
useBonesKey = 0
vertexGroupIndices = []
weightTotal = 0.0
if (len(Vertex.groups)) > 4:
print ("ERROR Vertex %d is influenced by more than 4 bones\n" % (VertexIndex))
for VertexGroup in Vertex.groups:
if (VertexGroup.weight > 0):
groupName = Object.vertex_groups[VertexGroup.group].name
if groupName in BoneNames:
mapVertexGroupNames[VertexGroup.group] = StripBoneName(groupName)
if (len(vertexGroupIndices))<4: #ignore if more 4 bones are influencing the vertex
useBonesKey = useBonesKey + pow(2, VertexGroup.group)
vertexGroupIndices.append(VertexGroup.group)
weightTotal = weightTotal + VertexGroup.weight
if (weightTotal == 0):
bWeightTotZero = True #avoid divide by zero later on
if (RootBone):
if Config.Verbose:
print(" Warning Weight is ZERO for vertex %d => Add it to the root bone" % (VertexIndex))
RootBoneGroupIndex = FindUniqueIndexForRootBone(Object, RootVertexGroup)
mapVertexGroupNames[RootBoneGroupIndex] = StripBoneName(RootBone.name)
useBonesKey = pow(2, RootBoneGroupIndex)
vertexGroupIndices = list((RootBoneGroupIndex,))
weightTotal = 1
else:
bWeightTotZero = False
if len(vertexGroupIndices) > 0:
vertexGroupIndices.sort();
#build the vertex weight string: vertex indices, followed by influence weight for each bone
VertexWeightString = "\t\tvertWeights { %d" % (VertexIndex)
for vertexGroupIndex in vertexGroupIndices:
#get the weight of this specific VertexGroup (aka bone)
boneWeight = 1
for VertexGroup in Vertex.groups:
if VertexGroup.group == vertexGroupIndex:
boneWeight = VertexGroup.weight
#calculate the influence of this bone compared to the total of weighting applied to this Vertex
if not bWeightTotZero:
VertexWeightString += ", %.7f" % (boneWeight / weightTotal)
else:
VertexWeightString += ", %.7f" % (1.0 / len(vertexGroupIndices))
VertexWeightString += "}"
if bWeightTotZero:
VertexWeightString += " // total weight was zero in blender , export assign it to the RootBone with weight 1."
if (len(Vertex.groups)) > 4:
VertexWeightString += " // vertex is associated to more than 4 bones in blender !! skip some bone association (was associated to %d bones)." % (len(Vertex.groups))
VertexWeightString += "\n"
#store in dictionnary information
if useBonesKey not in useBonesDict:
VertexList = []
VertexList.append(VertexWeightString)
useBonesDict[useBonesKey] = (vertexGroupIndices, VertexList)
else:
pair_ListGroupIndices_ListAssignedVertices = useBonesDict[useBonesKey]
pair_ListGroupIndices_ListAssignedVertices[1].append(VertexWeightString)
useBonesDict[useBonesKey] = pair_ListGroupIndices_ListAssignedVertices
else:
print ("ERROR Vertex %d is not skinned (it doesn't belong to any vertex group\n" % (VertexIndex))
############# ARMATURE: Bone export, and Bone animation export
def WriteArmatureParentRootBones(Config, Object, RootBonesList, skelFile):
if len(RootBonesList) > 1:
print(" /!\\ WARNING ,Marmelade need only one ROOT bone per armature, there is %d root bones " % len(RootBonesList))
print(RootBonesList)
PoseBones = Object.pose.bones