1- import bpy
21
3- def write_gpmesh (context , filepath , vertFormat ):
4- # Get current object
5- obj = bpy .context .scene .objects .active
6- if obj .type == 'MESH' :
7- mesh = obj .data
8- f = open (filepath , 'w' , encoding = 'utf-8' )
9- f .write ("{\n " )
10- f .write ("\t \" version\" :1,\n " )
11- f .write ("\t \" vertexformat\" :\" " + vertFormat + "\" ,\n " )
12- f .write ("\t \" shader\" :\" BasicMesh\" ,\n " )
2+
3+ bl_info = {
4+ "name" : "gpmesh Exporter" ,
5+ "blender" : (3 ,00 ,0 ),
6+ "category" : "Export"
7+ }
8+
9+ import bpy
10+ import json
11+
12+ def generate_gpmesh_json ():
13+ mesh = bpy .context .active_object .data
14+ uv_layer = mesh .uv_layers .active .data
15+
16+ gpmesh = {
17+ "version" : 1 ,
18+ "vertexformat" : "PosNormSkinTex" ,
19+ "shader" : "Skinned" ,
20+ "textures" : [],
21+ "specularPower" : 100.0 ,
22+ "vertices" : [],
23+ "indices" : []
24+ }
25+
26+ for vert in mesh .vertices :
27+ pos = vert .co
28+ normal = vert .normal
29+ uv = uv_layer [vert .index ].uv
30+ gp_vert = []
31+ gp_vert .extend ([pos .x , pos .y , pos .z ])
32+ gp_vert .extend ([normal .x , normal .y , normal .z ])
33+
34+ # get bone indices and their weights that affect this vertex and sort them by weight from high to low
35+ boneToWeightTuples = []
36+ for group in vert .groups :
37+ u8_weight = int (group .weight * 255 )
38+ boneToWeightTuples .append ((group .group , u8_weight ))
39+ boneToWeightTuples .sort (key = lambda boneToWeight : boneToWeight [1 ], reverse = True )
1340
14- # For now, only one texture
15- # figure out the file name...
16- texname = filepath .split ("\\ " )[- 1 ].split ("/" )[- 1 ].split ("." )[0 ]
17- # Make it a png
18- texname += ".png"
19- f .write ("\t \" textures\" :[\n " )
20- f .write ("\t \t \" " + texname + "\" \n " )
21- f .write ("\t ],\n " )
41+ # Only keep first 4 bones with their weights. As we sorted them by their bone weight (from high to low)
42+ # before, we only keep the once with highest influence.
43+ boneToWeightTuples = boneToWeightTuples [:4 ]
2244
23- # specular power
24- f .write ("\t \" specularPower\" :100,\n " )
25-
26- # vertices
27- # We have to create our own storage for because uvs are stored separately
28- verts = [dict () for x in range (len (mesh .vertices ))]
29- for v in mesh .vertices :
30- verts [v .index ]["pos" ] = v .co
31- verts [v .index ]["norm" ] = v .normal
45+ # The file format expects always 4 bones.
46+ while len (boneToWeightTuples ) < 4 :
47+ boneToWeightTuples .append ((0 , 0 ))
3248
33- for l in mesh .loops :
34- verts [l .vertex_index ]["uv" ] = mesh .uv_layers .active .data [l .index ].uv
49+ boneIndices = []
50+ weights = []
51+ for boneIdx , weight in boneToWeightTuples :
52+ boneIndices .append (boneIdx )
53+ weights .append (weight )
54+
55+ gp_vert .extend (boneIndices )
56+ gp_vert .extend (weights )
57+ gp_vert .extend ([uv .x , uv .y ])
3558
36- f .write ("\t \" vertices\" :[\n " )
37- first = True
38- for v in verts :
39- if first :
40- f .write ("\t \t [" )
41- first = False
42- else :
43- f .write (",\n \t \t [" )
44- f .write ("%f,%f,%f," % (v ["pos" ].x , v ["pos" ].y , v ["pos" ].z ))
45- f .write ("%f,%f,%f," % (v ["norm" ].x , v ["norm" ].y , v ["norm" ].z ))
46- f .write ("%f,%f" % (v ["uv" ].x , - 1.0 * v ["uv" ].y ))
47- f .write ("]" )
48- f .write ("\n \t ],\n " )
59+ gpmesh ["vertices" ].append (gp_vert )
60+
61+ for poly in mesh .polygons :
62+ tri = []
63+ for loop_index in poly .loop_indices :
64+ vertIndex = mesh .loops [loop_index ].vertex_index
65+ tri .append (vertIndex )
4966
50- # indices
51- f .write ("\t \" indices\" :[\n " )
52- first = True
53- for p in mesh .polygons :
54- if first :
55- f .write ("\t \t " )
56- first = False
57- else :
58- f .write (",\n \t \t " )
59- f .write ("[%d,%d,%d]" % (p .vertices [0 ], p .vertices [1 ], p .vertices [2 ]))
60- f .write ("\n \t ]\n " )
67+ gpmesh ["indices" ].append (tri )
68+
69+ textures = []
70+ materialSlots = bpy .context .active_object .material_slots
71+ for matSlot in materialSlots :
72+ if matSlot .material :
73+ if matSlot .material .node_tree :
74+ for node in matSlot .material .node_tree .nodes :
75+ if node .type == 'TEX_IMAGE' :
76+ textures .append (node .image .name )
77+
78+ gpmesh ["textures" ] = textures
79+
80+ return gpmesh
81+
82+ def generate_gpskel_json ():
83+ gpskel = {
84+ "version" : 1 ,
85+ "bonecount" : 0 ,
86+ "bones" : []
87+ }
88+ boneInfos = []
89+
90+ armature = bpy .context .active_object
91+ while armature .type != 'ARMATURE' :
92+ armature = armature .parent
93+
94+ if armature != bpy .context .active_object :
95+ armature = armature .data
96+ for i , bone in enumerate (armature .bones ):
97+ parentBone = bone .parent
98+ parentIndex = - 1
99+ if parentBone :
100+ parentIndex = armature .bones .find (parentBone .name )
101+
102+ local_matrix = bone .matrix_local
103+ trans = local_matrix .to_translation ()
104+ rot = local_matrix .to_quaternion ()
105+ boneInfo = {
106+ "name" : bone .name ,
107+ "index" : i ,
108+ "parent" : parentIndex ,
109+ "bindpose" : {
110+ "rot" : [rot .x , rot .y , rot .z , rot .w ],
111+ "trans" : [trans .x , trans .y , trans .z ]
112+ }
113+ }
114+ boneInfos .append (boneInfo )
61115
62- f .write ("}\n " )
116+ gpskel ["bonecount" ] = len (armature .bones )
117+
118+ gpskel ["bones" ] = boneInfos
119+
120+ return gpskel
121+
122+ def write_some_data (context , filepath , export_gpmesh , export_gpskel , export_gpanim ):
123+ print ("exporting to gpmesh..." )
124+
125+ if export_gpmesh :
126+ gpmesh = generate_gpmesh_json ()
127+ f = open (filepath , 'w' , encoding = 'utf-8' )
128+ # f.write("Hello World %s" % use_some_setting)
129+ f .write (json .dumps (gpmesh , sort_keys = False , indent = 2 ))
130+ f .close ()
131+
132+ if export_gpskel :
133+ gpskel = generate_gpskel_json ()
134+ gpskel_filepath = filepath .split ("." )[0 ] + '.gpskel'
135+ f = open (gpskel_filepath , "w" , encoding = "utf-8" )
136+ f .write (json .dumps (gpskel , sort_keys = False , indent = 2 ))
63137 f .close ()
64- else :
65- raise ValueError ( "No mesh selected " )
138+
139+ print ( "Done! " )
66140
67141 return {'FINISHED' }
68142
@@ -74,49 +148,71 @@ def write_gpmesh(context, filepath, vertFormat):
74148from bpy .types import Operator
75149
76150
77- class ExportGPMesh (Operator , ExportHelper ):
78- """Export to Game Programming in C++ mesh format"""
79- bl_idname = "export_test .gpmesh" # important since its how bpy.ops.import_test.some_data is constructed
80- bl_label = "Export Mesh "
151+ class ExportGPMESH (Operator , ExportHelper ):
152+ """Export mesh in gpmesh format. """
153+ bl_idname = "export .gpmesh" # important since its how bpy.ops.import_test.some_data is constructed
154+ bl_label = "Export as gpmesh "
81155
82156 # ExportHelper mixin class uses this
83157 filename_ext = ".gpmesh"
84158
85- filter_glob = StringProperty (
86- default = "*.gpmesh" ,
87- options = {'HIDDEN' },
88- maxlen = 255 , # Max internal buffer length, longer would be clamped.
89- )
90-
91- vertFormat = EnumProperty (
92- name = "Vertex Format" ,
93- description = "Choose the vertex format" ,
94- items = (('PosNormTex' , "PosNormTex" , "Position, normal, tex coord" ),
95- ('PosTex' , "PosTex" , "Position, tex coord" )),
96- default = 'PosNormTex' ,
97- )
98-
159+ filter_glob : StringProperty (
160+ default = "*.gpmesh" ,
161+ options = {'HIDDEN' },
162+ maxlen = 255 , # Max internal buffer length, longer would be clamped.
163+ )
164+
165+ # List of operator properties, the attributes will be assigned
166+ # to the class instance from the operator settings before calling.
167+ export_gpmesh : BoolProperty (
168+ name = "Export gpmesh" ,
169+ description = "Writes the mesh as .gpmesh to disk" ,
170+ default = True
171+ )
172+
173+ export_gpskel : BoolProperty (
174+ name = "Export gpskel" ,
175+ description = "Writes .gpskel file to disk" ,
176+ default = True
177+ )
178+
179+ export_gpanim : BoolProperty (
180+ name = "Export gpanim" ,
181+ description = "Writes .gpanim file to disk" ,
182+ default = True
183+ )
184+
185+ type : EnumProperty (
186+ name = "Example Enum" ,
187+ description = "Choose between two items" ,
188+ items = (
189+ ('OPT_A' , "First Option" , "Description one" ),
190+ ('OPT_B' , "Second Option" , "Description two" ),
191+ ),
192+ default = 'OPT_A' ,
193+ )
194+
99195 def execute (self , context ):
100- return write_gpmesh (context , self .filepath , self .vertFormat )
196+ return write_some_data (context , self .filepath , self .export_gpmesh , self . export_gpskel , self . export_gpanim )
101197
102198
103199# Only needed if you want to add into a dynamic menu
104200def menu_func_export (self , context ):
105- self .layout .operator (ExportGPMesh .bl_idname , text = "GPMesh Exporter (.gpmesh)" )
106-
201+ self .layout .operator (ExportGPMESH .bl_idname , text = "gpmesh" )
107202
203+ # Register and add to the "file selector" menu (required to use F3 search "Text Export Operator" for quick access)
108204def register ():
109- bpy .utils .register_class (ExportGPMesh )
110- bpy .types .INFO_MT_file_export .append (menu_func_export )
205+ bpy .utils .register_class (ExportGPMESH )
206+ bpy .types .TOPBAR_MT_file_export .append (menu_func_export )
111207
112208
113209def unregister ():
114- bpy .utils .unregister_class (ExportGPMesh )
115- bpy .types .INFO_MT_file_export .remove (menu_func_export )
210+ bpy .utils .unregister_class (ExportGPMESH )
211+ bpy .types .TOPBAR_MT_file_export .remove (menu_func_export )
116212
117213
118214if __name__ == "__main__" :
119215 register ()
120216
121217 # test call
122- bpy .ops .export_test .gpmesh ('INVOKE_DEFAULT' )
218+ bpy .ops .export .gpmesh ('INVOKE_DEFAULT' )
0 commit comments