io_scene_m3d.py 74 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746
  1. # ##### BEGIN MIT LICENSE BLOCK #####
  2. #
  3. # blender/io_scene_m3d.py
  4. #
  5. # Copyright (C) 2019 - 2022 bzt (bztsrc@gitlab)
  6. #
  7. # Permission is hereby granted, free of charge, to any person
  8. # obtaining a copy of this software and associated documentation
  9. # files (the "Software"), to deal in the Software without
  10. # restriction, including without limitation the rights to use, copy,
  11. # modify, merge, publish, distribute, sublicense, and/or sell copies
  12. # of the Software, and to permit persons to whom the Software is
  13. # furnished to do so, subject to the following conditions:
  14. #
  15. # The above copyright notice and this permission notice shall be
  16. # included in all copies or substantial portions of the Software.
  17. #
  18. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  19. # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  20. # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  21. # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
  22. # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
  23. # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  24. # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
  25. # DEALINGS IN THE SOFTWARE.
  26. #
  27. # @brief Blender 2.80 Model 3D Exporter (and one day Importer too)
  28. # https://gitlab.com/bztsrc/model3d
  29. #
  30. # ##### END MIT LICENSE BLOCK #####
  31. # <pep8-80 compliant>
  32. bl_info = {
  33. "name": "Model 3D (.m3d) format",
  34. "author": "bzt",
  35. "version": (0, 0, 1),
  36. "blender": (2, 80, 0),
  37. "location": "File > Import-Export",
  38. "description": "Export M3D",
  39. "wiki_url": "https://gitlab.com/bztsrc/model3d/blob/master/docs/m3d_format.md",
  40. "category": "Import-Export"}
  41. # -----------------------------------------------------------------------------
  42. # Import libraries
  43. import bmesh
  44. import os
  45. from operator import itemgetter
  46. from struct import pack, unpack
  47. from mathutils import Matrix
  48. from bpy_extras import io_utils, node_shader_utils
  49. from bpy_extras.wm_utils.progress_report import (
  50. ProgressReport,
  51. ProgressReportSubstep,
  52. )
  53. # -----------------------------------------------------------------------------
  54. # Blender material property and M3D property type assignments
  55. # See https://gitlab.com/bztsrc/model3d/blob/master/docs/m3d_format.md section Materials)
  56. mat_property_map = {
  57. #type format PrincipledBSDF property ASCII variant
  58. 0: ["color", "base_color", "Kd"],
  59. 1: ["gscale", "metallic", "Ka"],
  60. 2: ["gscale", "specular", "Ks"],
  61. 3: ["//float","specular_tint", "Ns"],
  62. 4: ["//color","emissive", "Ke"], # not in BSDF?
  63. 5: ["gscale", "transmission", "Tf"],
  64. 6: ["float", "normalmap_strength", "Km"],
  65. 7: ["float", "alpha", "d"],
  66. 8: ["//byte", "illumination", "il"], # not in PBR at all
  67. 64: ["float", "roughness", "Pr"],
  68. 65: ["float", "metallic", "Pm"],
  69. 66: ["//float","sheen", "Ps"], # not in BSDF?
  70. 67: ["float", "ior", "Ni"],
  71. 128: ["map", "base_color_texture", "map_Kd"],
  72. 130: ["//map", "specular_texture", "map_Ks"], # should work, but it does not
  73. 133: ["map", "transmission_texture", "map_Tf"],
  74. 134: ["map", "normalmap_texture", "map_Km"],
  75. 135: ["map", "alpha_texture", "map_D"],
  76. 192: ["map", "roughness_texture", "map_Pr"],
  77. 193: ["map", "metallic_texture", "map_Pm"],
  78. 195: ["map", "ior_texture", "map_Ni"],
  79. }
  80. # -----------------------------------------------------------------------------
  81. # Load and parse a Model 3D file (unlike the exporter, this is WIP)
  82. def read_m3d(context,
  83. filepath,
  84. report,
  85. global_matrix=None,
  86. ):
  87. # read in an index
  88. def getidx(data, fmt):
  89. idx = -1
  90. if fmt == 0:
  91. if len(data) > 0:
  92. idx = unpack("<B", data[0:1])[0]
  93. if idx > 253:
  94. idx = idx - 256
  95. data = data[1:]
  96. elif fmt == 1:
  97. if len(data) > 1:
  98. idx = unpack("<H", data[0:2])[0]
  99. if idx > 65533:
  100. idx = idx - 65536
  101. data = data[2:]
  102. elif fmt == 2:
  103. if len(data) > 3:
  104. idx = unpack("<I", data[0:4])[0]
  105. if idx > 4294967293:
  106. idx = idx - 4294967296
  107. data = data[4:]
  108. return data, idx
  109. # read in a coordinate
  110. def getcrd(data, fmt):
  111. crd = 0
  112. if fmt == 0:
  113. crd = round(float(unpack("<b", data[0:1])[0]) / 127.0, 4)
  114. data = data[1:]
  115. elif fmt == 1:
  116. crd = round(float(unpack("<h", data[0:2])[0]) / 32767.0, 4)
  117. data = data[2:]
  118. elif fmt == 2:
  119. crd = unpack("<f", data[0:4])[0]
  120. data = data[4:]
  121. elif fmt == 3:
  122. crd = unpack("<d", data[0:8])[0]
  123. data = data[8:]
  124. if crd == -0.0:
  125. crd = 0.0
  126. return data, crd
  127. with ProgressReport(context.window_manager) as progress:
  128. ############ !!!!!!!!! Work In Progress !!!!!!!!!! ###############
  129. if global_matrix is None:
  130. global_matrix = axis_conversion(from_forward='Z', from_up='Y').to_4x4()
  131. # read in file
  132. f = open(filepath, 'rb')
  133. data = f.read()
  134. f.close()
  135. if len(data) > 2 and data[0:2] == b'\x1F\x8B':
  136. import gzip
  137. data = gzip.decompress(data)
  138. if len(data) < 8 or (data[0:4] != b'3DMO' and data[0:7] != b'3dmodel'):
  139. report({"ERROR"}, filepath + " is not a valid Model 3D file!")
  140. return {'FINISHED'}
  141. # load into Blender independent lists
  142. cmap = []
  143. tmap = []
  144. vrts = []
  145. faces = []
  146. shapes = []
  147. labels = []
  148. materials = []
  149. bones = []
  150. skins = []
  151. if data[0:7] == b'3dmodel':
  152. # Model 3D ASCII variant
  153. data = str(data)
  154. if data[0:2] == "b'":
  155. data = data[2:len(data)-3] # cut off "b'" and "'"
  156. # parse model header
  157. head = {
  158. 'name':'', 'license':'', 'author':'', 'description':'',
  159. 'scale':1.0
  160. }
  161. # TODO
  162. else:
  163. # Model 3D binary variant
  164. # skip over preview image chunk
  165. if data[8:12] == b'PRVW':
  166. length = unpack("<I", data[4:8])[0]
  167. data = data[length:]
  168. if data[8:12] != b'HEAD':
  169. import zlib
  170. data = zlib.decompress(data[8:])
  171. else:
  172. data = data[8:]
  173. if data[0:4] != b'HEAD':
  174. report({"ERROR"}, filepath + " is not a valid Model 3D file!")
  175. return {'FINISHED'}
  176. # parse model header and string table
  177. head = {
  178. 'name':'', 'license':'', 'author':'', 'description':'',
  179. 'scale':unpack("<f", data[8:12])[0],
  180. 'vc_s':(data[12] >> 0) & 3, 'vi_s':(data[12] >> 2) & 3, 'si_s':(data[12] >> 4) & 3,
  181. 'ci_s':(data[12] >> 6) & 3, 'ti_s':(data[13] >> 2) & 3, 'bi_s':(data[13] >> 2) & 3,
  182. 'nb_s':(data[13] >> 4) & 3, 'sk_s':(data[13] >> 6) & 3, 'fc_s':(data[14] >> 0) & 3,
  183. 'hi_s':(data[14] >> 2) & 3, 'fi_s':(data[14] >> 4) & 3,
  184. }
  185. length = unpack("<I", data[4:8])[0]
  186. chunk = data[16:length]
  187. data = data[length:]
  188. s = chunk.split(b'\000')
  189. head['name'] = str(s[0], 'utf-8')
  190. head['license'] = str(s[1], 'utf-8')
  191. head['author'] = str(s[2], 'utf-8')
  192. head['description'] = str(s[3], 'utf-8')
  193. strs = {0:''}
  194. i = len(s[0]) + len(s[1]) + len(s[2]) + len(s[3]) + 4
  195. for t in s[4:]:
  196. strs[i] = str(t, 'utf-8')
  197. i = i + len(t) + 1
  198. # decode chunks
  199. while len(data) > 4 and data[0:4] != b'OMD3':
  200. magic = data[0:4]
  201. length = unpack("<I", data[4:8])[0]
  202. if length < 8 or length > 16 * 1024 * 1024:
  203. report({"ERROR"}, str(magic,'utf-8') + " bad chunk in Model 3D file!")
  204. break
  205. chunk = data[8:length]
  206. # color map
  207. if magic == b'CMAP':
  208. while len(chunk) > 0:
  209. cmap.append([
  210. unpack("<B", chunk[0:1])[0] / 255.0,
  211. unpack("<B", chunk[1:2])[0] / 255.0,
  212. unpack("<B", chunk[2:3])[0] / 255.0,
  213. unpack("<B", chunk[3:4])[0] / 255.0])
  214. chunk = chunk[4:]
  215. # texture map
  216. elif magic == b'TMAP':
  217. while len(chunk) > 0:
  218. u = v = 0.0
  219. # don't use getcrd, because this is scaled to 255
  220. if head['vc_s'] == 0:
  221. u = float(unpack("<B", chunk[0:1])[0]) / 255.0
  222. v = float(unpack("<B", chunk[1:2])[0]) / 255.0
  223. chunk = chunk[2:]
  224. elif head['vc_s'] == 1:
  225. u = float(unpack("<H", chunk[0:2])[0]) / 65535.0
  226. v = float(unpack("<H", chunk[2:4])[0]) / 65535.0
  227. chunk = chunk[4:]
  228. elif head['vc_s'] == 2:
  229. u, v = unpack("<ff", chunk[0:8])
  230. chunk = chunk[8:]
  231. elif head['vc_s'] == 3:
  232. u, v = unpack("<dd", chunk[0:16])
  233. chunk = chunk[16:]
  234. tmap.append([round(u, 4), round(v, 4)])
  235. # vertex list (for vertex and normals)
  236. elif magic == b'VRTS':
  237. while len(chunk) > 0:
  238. chunk, x = getcrd(chunk, head['vc_s'])
  239. chunk, y = getcrd(chunk, head['vc_s'])
  240. chunk, z = getcrd(chunk, head['vc_s'])
  241. chunk, w = getcrd(chunk, head['vc_s'])
  242. chunk, c = getidx(chunk, head['ci_s'])
  243. chunk, s = getidx(chunk, head['sk_s'])
  244. if ci_s < 4 and c != -1:
  245. c = cmap[c]
  246. elif ci_s == 4:
  247. c = [c[0:1] / 255.0, c[1:2] / 255.0, c[2:3] / 255.0, c[3:4] / 255.0]
  248. vrts.append([x, y, z, w, c, s])
  249. # material chunk
  250. elif magic == b'MTRL':
  251. chunk, n = getidx(chunk, head['si_s'])
  252. props = {}
  253. while len(chunk) > 0:
  254. typ = unpack("<B", chunk[0:1])[0]
  255. chunk = chunk[1:]
  256. try:
  257. t = mat_property_map[typ];
  258. except:
  259. print("unknown property ", typ)
  260. break;
  261. if typ == 0:
  262. chunk, c = getidx(chunk, head['ci_s'])
  263. props["base_color"] = cmap[c]
  264. props["alpha"] = cmap[c][3]
  265. elif typ >= 128:
  266. chunk, s = getidx(chunk, head['si_s'])
  267. if t[1][0:2] != "//":
  268. props[t[1]] = strs[s] + ".png"
  269. elif t[0] == "gscale":
  270. chunk, c = getidx(chunk, head['ci_s'])
  271. props[t[1]] = round((cmap[c][0] + cmap[c][1] + cmap[c][2]) / 3, 4)
  272. elif t[0] == "color":
  273. chunk, c = getidx(chunk, head['ci_s'])
  274. if ci_s < 4 and c != -1:
  275. c = cmap[c]
  276. elif ci_s == 4:
  277. c = [c[0:1] / 255.0, c[1:2] / 255.0, c[2:3] / 255.0, c[3:4] / 255.0]
  278. props[t[1]] = c
  279. elif t[0] == "float":
  280. chunk, c = getcrd(chunk, 2)
  281. props[t[1]] = c
  282. elif t[0] == "byte":
  283. props[t[1]] = unpack("<B", chunk[0:1])[0]
  284. chunk = chunk[1:]
  285. elif t[0] == "int":
  286. props[t[1]] = unpack("<I", chunk[0:4])[0]
  287. chunk = chunk[4:]
  288. elif t[0] == "//map":
  289. chunk = chunk[head['si_s']:]
  290. elif t[0] == "//color" or t[0] == "//gscale":
  291. chunk = chunk[head['ci_s']:]
  292. elif t[0] == "//float" or t[0] == "//int":
  293. chunk = chunk[4:]
  294. elif t[0] == "//byte":
  295. chunk = chunk[1:]
  296. else:
  297. break;
  298. materials.append([strs[n], props])
  299. # mesh (only triangles supported for now)
  300. elif magic == b'MESH':
  301. m = -1
  302. while len(chunk) > 0:
  303. typ = unpack("<B", chunk[0:1])[0]
  304. chunk = chunk[1:]
  305. if typ == 0:
  306. chunk, s = getidx(chunk, head['si_s'])
  307. m = -1
  308. if s != 0:
  309. for i, mat in enumerate(materials):
  310. if strs[s] == mat[0]:
  311. m = i
  312. break
  313. elif (typ >> 4) == 3:
  314. v = [-1, -1, -1]
  315. t = [-1, -1, -1]
  316. n = [-1, -1, -1]
  317. for i in range(0, 3):
  318. chunk, v[i] = getidx(chunk, head['vi_s'])
  319. if typ & 1:
  320. chunk, t[i] = getidx(chunk, head['ti_s'])
  321. if typ & 2:
  322. chunk, n[i] = getidx(chunk, head['vi_s'])
  323. faces.append({'m':m, 'v':v, 't':t, 'n':n})
  324. else:
  325. print("Only triangles supported")
  326. break
  327. # shapes
  328. elif magic == b'SHPE':
  329. chunk, s = getidx(chunk, head['si_s'])
  330. # TODO
  331. print("shape ", strs[s])
  332. # annotation labels
  333. elif magic == b'LBLS':
  334. chunk, n = getidx(chunk, head['si_s'])
  335. chunk, l = getidx(chunk, head['si_s'])
  336. chunk, c = getidx(chunk, head['ci_s'])
  337. if ci_s < 4 and c != -1:
  338. c = cmap[c]
  339. elif ci_s == 4:
  340. c = [c[0:1] / 255.0, c[1:2] / 255.0, c[2:3] / 255.0, c[3:4] / 255.0]
  341. # TODO
  342. print("Labels layer:", strs[n], ", lang:", strs[l], ", color ", c)
  343. # armature bones and vertex groups
  344. elif magic == b'BONE':
  345. chunk, b = getidx(chunk, head['bi_s'])
  346. chunk, s = getidx(chunk, head['sk_s'])
  347. # TODO
  348. print("Skeleton ", b, "bone(s),", s, "skin record(s) (unique bone/weight combos)")
  349. # animation and timeline marker
  350. elif magic == b'ACTN':
  351. chunk, s = getidx(chunk, head['si_s'])
  352. chunk, f = getidx(chunk, 1)
  353. chunk, l = getidx(chunk, 2)
  354. # TODO
  355. print("Action ", strs[s], ", durationmsec", l, ", numframes ", f)
  356. # inlined asset
  357. elif magic == b'ASET':
  358. chunk, s = getidx(chunk, head['si_s'])
  359. # TODO
  360. print("Inlined asset ", strs[s], "(", len(chunk), " bytes)")
  361. else:
  362. print("Unknown chunk '%s' skipping..." % (str(magic,'utf-8')))
  363. data = data[length:]
  364. del strs
  365. del cmap
  366. # ----------------- Start of Blender Specific Stuff ---------------------
  367. print("\nhead ", head)
  368. print("\ntmap ", tmap) # texture map, array of [u, v]
  369. print("\nvrts ", vrts) # vertex list, array of [x, y, z, w, [r,g,b,a], skinid]
  370. print("\nmaterials", materials) # array of [name, array of [principledBSDFpropname, value]]
  371. print("\nfaces ", faces) # triangles, array of [m:materialidx, v[3]:vertexidx, t[3]:tmapidx, n[3]:normalvertexidx]
  372. print("\nshapes ", shapes)
  373. print("\nlabels ", labels)
  374. print("\nbones ", bones)
  375. print("\nskins ", skins)
  376. # TODO: add to bpy
  377. # ----------------- End of Blender Specific Stuff ---------------------
  378. report({"ERROR"}, "Model 3D importer not fully implemented yet.")
  379. return {'FINISHED'}
  380. # -----------------------------------------------------------------------------
  381. # Construct and save a Model 3D file
  382. def write_m3d(context,
  383. filepath,
  384. report,
  385. *,
  386. use_name='', # model's name
  387. use_license='MIT', # model's license
  388. use_author='', # model's author
  389. use_comment='', # model's comment
  390. use_scale=1.0, # model-space 1.0 in SI meters
  391. use_selection=True, # export selected items only
  392. use_mesh_modifiers=True, # apply mesh modifiers
  393. use_normals=False, # save normal vectors too
  394. use_uvs=True, # save texture map UV coordinates
  395. use_colors=True, # save per vertex colors
  396. use_shapes=False, # save shape commands
  397. use_materials=True, # save materials
  398. use_skeleton=True, # save bind-pose armature
  399. use_animation=True, # save skeletal animations
  400. use_markers=False, # use timeline markers for animations
  401. use_fps=25, # frame per second
  402. use_quality='-1', # -1: auto, 0: 8 bit, 1: 16 bit, 2: 32 bit, 3: 64 bit
  403. use_inline=False, # inline textures
  404. use_gridcompress=True, # use lossy grid compression
  405. use_strmcompress=True, # use lossless stream compression
  406. use_ascii=False, # save ASCII variant
  407. use_relbones=True, # (debug only) use parent relative bone positions
  408. global_matrix=None, # default orientation
  409. check_existing=True,
  410. ):
  411. # convert string to name identifier
  412. def safestr(name, morelines=0):
  413. if name is None:
  414. return ''
  415. elif morelines == 3:
  416. return name.replace('\r', '').strip()
  417. elif morelines == 2:
  418. return name.replace('\r', '').replace('\n', ' ').strip()
  419. elif morelines == 1:
  420. return name.replace('\r', '').replace('\n', '\r\n').strip()
  421. else:
  422. return name.replace(' ', '_').replace('/', '_').replace('\\', '_').replace('\r', '').replace('\n', ' ').strip()
  423. # set is unique, but has no index, list has index, but not unique...
  424. # this is utterly and painfully slow, hence the dict wrapper below
  425. def uniquelist(l, e):
  426. try:
  427. i = l.index(e)
  428. except ValueError:
  429. i = len(l)
  430. l.append(e)
  431. return i
  432. # use hash table and then convert dict to list instead
  433. # this uses considerably more memory, but we have no choice:
  434. # using uniquelist takes several minutes with 50000 triangles...
  435. def uniquedict(l, e):
  436. h = hash(str(e))
  437. try:
  438. return l[h][0]
  439. except KeyError:
  440. i = len(l)
  441. l[h] = [i, e]
  442. return i
  443. def dict2list(l):
  444. r = []
  445. for i, v in l.items():
  446. r.insert(v[0], v[1])
  447. return r
  448. # get index size (we use -1 and -2 as special indices)
  449. def idxsize(cnt):
  450. if cnt == 0:
  451. return 3
  452. elif cnt < 254:
  453. return 0
  454. elif cnt < 65534:
  455. return 1
  456. return 2
  457. # write out an index
  458. def addidx(fmt, idx):
  459. # we rely on the fact that in C -1 is a full binary 1 which
  460. # gives the maximum unsigned value regardless to size, but
  461. # pack stops us from taking advantage of that
  462. if fmt == 0:
  463. if idx < 0:
  464. idx = 256 + idx
  465. return pack("<B", idx)
  466. elif fmt == 1:
  467. if idx < 0:
  468. idx = 65536 + idx
  469. return pack("<H", idx)
  470. elif fmt == 2:
  471. if idx < 0:
  472. idx = 4294967296 + idx
  473. return pack("<I", idx)
  474. return b''
  475. # eliminate minus zero
  476. def vert(x,y,z,w,c,s):
  477. if x == -0.0:
  478. x = 0.0
  479. if y == -0.0:
  480. y = 0.0
  481. if z == -0.0:
  482. z = 0.0
  483. if w == -0.0:
  484. w = 0.0
  485. return [x,y,z,w,c,s]
  486. # normalize matrix, decompose and recompose to eliminate errors
  487. def matnorm(a):
  488. p, q, s = a.decompose()
  489. q.normalize()
  490. return Matrix.Translation(p) @ q.to_matrix().to_4x4()
  491. # get texture
  492. def gettexture(fn, use_inline):
  493. data = b''
  494. if fn[0:2] == "//":
  495. fn = fn[2:]
  496. imgpath = repr(os.path.basename(fn))[1:-1]
  497. imgpath = os.path.splitext(imgpath)[0]
  498. if imgpath != "" and use_inline:
  499. try:
  500. data = open(os.path.join(os.path.dirname(filepath), fn), 'rb').read()
  501. except:
  502. try:
  503. data = open(os.path.join(os.path.dirname(filepath), os.path.basename(fn)), 'rb').read()
  504. except:
  505. try:
  506. data = open(os.path.join(os.path.dirname(filepath), imgpath + ".png"), 'rb').read()
  507. except:
  508. try:
  509. data = open(imgpath + ".png", 'rb').read()
  510. except:
  511. try:
  512. data = open(fn, 'rb').read()
  513. except:
  514. data = b''
  515. if len(data) < 8 or data[0:4] != b'\x89PNG':
  516. report({"ERROR"}, "Texture file '" + fn + "' not found or not a valid PNG. Cannot be inlined.")
  517. data = b''
  518. return [ imgpath, data ]
  519. # recursively walk skeleton and construct string representation
  520. def bonestr(strs, bones, parent, level):
  521. ret = ""
  522. for i,b in enumerate(bones):
  523. if b[0] == parent:
  524. ret += "/"*level + str(b[2]) + " " + str(b[3]) + " " + strs[b[1]] + "\r\n"
  525. ret += bonestr(strs, bones, i, level+1)
  526. return ret
  527. with ProgressReport(context.window_manager) as progress:
  528. if global_matrix is None:
  529. global_matrix = axis_conversion(from_forward='-Y', from_up='Z',to_forward='Z', to_up='Y').to_4x4()
  530. if use_animation:
  531. use_skeleton = True
  532. if use_fps < 1 or use_fps > 120:
  533. use_fps = 25
  534. # Get Blender objects to export
  535. depsgraph = context.evaluated_depsgraph_get()
  536. scene = context.scene
  537. if use_selection:
  538. objects = context.selected_objects
  539. else:
  540. objects = context.scene.objects
  541. # if use_quality is set to auto, then count the number of triangles to decide
  542. use_quality = int(use_quality)
  543. if use_quality < 0 or use_quality > 3:
  544. n = 0
  545. for i, ob_main in enumerate(objects):
  546. if ob_main.parent and ob_main.parent.instance_type in {'VERTS', 'FACES'}:
  547. continue
  548. try:
  549. me = ob_main.original.to_mesh()
  550. n += len(me.polygons)
  551. except:
  552. continue
  553. if n < 1024:
  554. use_quality = 0
  555. else:
  556. use_quality = 1
  557. # we must use floating point without grid compression
  558. if use_gridcompress == False and use_quality < 2:
  559. use_quality = 2
  560. # get the number of significant digits depending on quality
  561. if use_quality == 3:
  562. digits = 15
  563. if use_quality == 2:
  564. digits = 7
  565. else:
  566. digits = 4
  567. # Build global lists with unique elements
  568. # we use a dict wrapper to speed up things
  569. cmap = {} # color map entries
  570. strs = {} # string table with unique strings
  571. verts = {} # unique list of vertices
  572. tmaps = {} # texture map UV coordinates
  573. faces = [] # triangles list
  574. shapes = [] # shapes list
  575. labels = [] # annotation labels
  576. materials = [] # translated material name and properties
  577. bones = {} # bind-pose skeleton
  578. skins = {} # array of bone id / weight combinations per vertex
  579. actions = [] # animations
  580. inlined = {} # inlined textures
  581. extras = [] # extra chunks (engine specific)
  582. progress.enter_substeps(2 + use_materials + use_skeleton + use_animation)
  583. # ----------------- Start of Blender Specific Stuff ---------------------
  584. refmats = {} # unique list of referenced Blender material objects
  585. nb_m = 0 # maximum number of bone weights per vertex
  586. fi_m = 0 # frame index maximum
  587. # set rest armature (bind-pose skeleton)
  588. # if we don't do this, we'll get strange bones and distorted mesh
  589. oldaction = None
  590. oldframe = context.scene.frame_current
  591. oldpose = {}
  592. for i,ob_main in enumerate(objects):
  593. if ob_main.type == "ARMATURE":
  594. oldpose[i] = ob_main.data.pose_position
  595. ob_main.data.pose_position = "REST"
  596. ob_main.data.update_tag()
  597. if oldaction == None and ob_main.animation_data and ob_main.animation_data.action:
  598. oldaction = ob_main.animation_data.action
  599. context.scene.frame_set(0)
  600. ### Armature ###
  601. if use_skeleton:
  602. # this must be done before the mesh so that skin can refer to bones
  603. progress.step("Exporting Armature")
  604. idx = 0
  605. for i,ob_main in enumerate(objects):
  606. if ob_main.type != "ARMATURE":
  607. continue
  608. for b in ob_main.data.bones:
  609. m = matnorm(global_matrix @ ob_main.matrix_world @ b.matrix_local)
  610. a = -1
  611. if b.parent:
  612. # is there a better way to get the parent's
  613. # index in the armature's bone collection?
  614. for j,p in enumerate(ob_main.data.bones):
  615. if p == b.parent:
  616. a = j
  617. break
  618. if use_relbones == True:
  619. p = matnorm(global_matrix @ ob_main.matrix_world @ b.parent.matrix_local)
  620. m = p.inverted() @ m
  621. # For the top level bones, we need model-space p,q
  622. # for the children, parent relative p,q
  623. p = m.to_translation() # position
  624. q = m.to_quaternion() # orientation
  625. q.normalize()
  626. n = safestr(b.name)
  627. try:
  628. ni = strs[hash(str(n))][0]
  629. name = "'" + b.name + "'"
  630. if b.name != n:
  631. name += " (" + n + ")"
  632. report({"ERROR"}, "Bone name " + name + " not unique.")
  633. use_skeleton = False
  634. use_animation = False
  635. bones = {}
  636. break
  637. except:
  638. pass
  639. bones[b.name] = [idx, [a, uniquedict(strs, n),
  640. uniquedict(verts, vert(
  641. round(p[0], digits),
  642. round(p[1], digits),
  643. round(p[2], digits), 1.0, 0, -1)),
  644. uniquedict(verts, vert(
  645. round(q.x, digits),
  646. round(q.y, digits),
  647. round(q.z, digits),
  648. round(q.w, digits), 0, -2))]]
  649. idx = idx + 1
  650. if len(bones) < 1 and use_animation:
  651. report({"ERROR"}, "Skipping skeletal animation in lack of armature.")
  652. use_animation = False
  653. ### Mesh data ###
  654. progress.step("Exporting Mesh")
  655. progress.enter_substeps(len(objects))
  656. for i, ob_main in enumerate(objects):
  657. # this mess was taken from io_scene_obj. The point is, at the end we have
  658. # something that has faces with triangles and model-space coordinate vertices
  659. if ob_main.parent and ob_main.parent.instance_type in {'VERTS', 'FACES'}:
  660. continue
  661. obs = [(ob_main, ob_main.matrix_world)]
  662. if ob_main.is_instancer:
  663. obs += [(dup.instance_object.original, dup.matrix_world.copy())
  664. for dup in depsgraph.object_instances
  665. if dup.parent and dup.parent.original == ob_main]
  666. for ob, ob_mat in obs:
  667. # get a copy of the mesh object
  668. try:
  669. o = ob.evaluated_get(depsgraph) if use_mesh_modifiers else ob.original
  670. me = o.to_mesh()
  671. except:
  672. me = None
  673. if me is None or len(me.polygons) < 1:
  674. continue
  675. if use_name is None or use_name == '':
  676. use_name = ob.name
  677. # triangulate if we must
  678. r = False
  679. for poly in me.polygons:
  680. if len(poly.loop_indices) != 3:
  681. r = True
  682. break
  683. if r == True:
  684. #print("Need to triangulate mesh '" + me.name + "'")
  685. bm = bmesh.new()
  686. bm.from_mesh(me)
  687. bmesh.ops.triangulate(bm, faces=bm.faces[:])
  688. bm.to_mesh(me)
  689. bm.free()
  690. # transform vertices to model-space
  691. me.transform(global_matrix @ ob_mat)
  692. if ob_mat.determinant() < 0.0:
  693. me.flip_normals()
  694. if use_normals:
  695. # needed pre 4.1, but breaks 4.1 and above
  696. try:
  697. me.calc_normals_split()
  698. except:
  699. pass
  700. if use_skeleton and len(ob.vertex_groups) > 0:
  701. vg = ob.vertex_groups
  702. else:
  703. vg = []
  704. if use_skeleton == True and use_animation == True:
  705. report({"ERROR"}, "Mesh '" + me.name + "' in object '" + ob.name + "' has no vertex groups, no skeletal animation possible!")
  706. if use_uvs and len(me.uv_layers) > 0:
  707. uv_layer = me.uv_layers.active.data[:]
  708. else:
  709. uv_layer = []
  710. if use_colors and len(me.vertex_colors) > 0:
  711. # bug in Blender 3.6, vertex_colors.active_index might be out of bounds...
  712. if me.vertex_colors.active_index >= 0 and me.vertex_colors.active_index < len(me.vertex_colors) and len(me.vertex_colors[me.vertex_colors.active_index].data) > 0:
  713. vcol = me.vertex_colors[me.vertex_colors.active_index].data
  714. else:
  715. report({"ERROR"}, "Vertex color in mesh '" + me.name + "' in object '" + ob.name + "' has invalid out-of-bounds index (vertex_colors.active_index is " + str(me.vertex_colors.active_index) + ", largest can be " + str(len(me.vertex_colors) - 1) + ").")
  716. # try to fallback to the first vertex_colors index
  717. if len(me.vertex_colors[0].data) > 0:
  718. vcol = me.vertex_colors[0].data
  719. else:
  720. report({"ERROR"}, "Vertex color in mesh '" + me.name + "' in object '" + ob.name + "' unable to fallback to vertex_colors[0], no data.")
  721. vcol = []
  722. else:
  723. vcol = []
  724. matnames = []
  725. if use_materials:
  726. for m in me.materials[:]:
  727. if m and m.name:
  728. matnames.append(uniquedict(strs, safestr(m.name)))
  729. else:
  730. matnames.append(-1)
  731. # Ahhh finally we can get the vertices and faces
  732. badref = {}
  733. for pi,poly in enumerate(me.polygons):
  734. face = [ -1, [-1,-1,-1], [-1,-1,-1], [-1,-1,-1], -1 ]
  735. if len(matnames) > 0:
  736. if poly.material_index < len(matnames):
  737. i = poly.material_index
  738. else:
  739. i = 0
  740. # workaround to report each bad material index only once
  741. try:
  742. dummy = badref[poly.material_index]
  743. except:
  744. badref[poly.material_index] = 1
  745. report({"ERROR"}, "Polygon face in mesh '" + me.name + "' referencing a non-existent material (index " + str(poly.material_index) + ", largest can be " + str(len(matnames) - 1) + ").")
  746. if i >= 0:
  747. face[0] = matnames[i]
  748. uniquedict(refmats, me.materials[i])
  749. for i, li in enumerate(poly.loop_indices):
  750. if len(vcol) > 0:
  751. c = uniquedict(cmap, [vcol[li].color[0], vcol[li].color[1], vcol[li].color[2], vcol[li].color[3]])
  752. else:
  753. c = 0
  754. v = me.vertices[poly.vertices[i]]
  755. if use_skeleton and len(vg) > 0 and len(v.groups) > 0:
  756. wf = 0.0
  757. for g in v.groups:
  758. wf += g.weight
  759. if wf > 0.0:
  760. skin = []
  761. w = wi = wm = 0
  762. for g in v.groups:
  763. try:
  764. s = round(g.weight / wf * 255.0)
  765. if s > wm:
  766. wm = s
  767. si = len(skin)
  768. if s < 1:
  769. s = 1
  770. if s > 255:
  771. s = 255
  772. skin.append([bones[vg[g.group].name][0], s])
  773. w = w + s
  774. except:
  775. report({"ERROR"}, "Vertex group name '" + vg[g.group].name + "' does not match any bone.")
  776. use_skeleton = False
  777. vg = []
  778. s = -1
  779. break
  780. try:
  781. if w != 255:
  782. skin[si][1] += 255 - w
  783. except:
  784. pass
  785. s = uniquedict(skins, skin)
  786. if len(skin) > nb_m:
  787. nb_m = len(skin)
  788. else:
  789. s = -1
  790. else:
  791. s = -1
  792. face[1][i] = uniquedict(verts, vert(
  793. round(v.co.x, digits),
  794. round(v.co.y, digits),
  795. round(v.co.z, digits), 1.0, c, s))
  796. if use_normals:
  797. try:
  798. no = v.normal.copy()
  799. except:
  800. no = poly.loops[i].normal.copy()
  801. no.normalize()
  802. face[3][i] = uniquedict(verts, vert(
  803. round(no.x, digits),
  804. round(no.y, digits),
  805. round(no.z, digits), 1.0, 0, -1))
  806. del no
  807. if use_uvs and len(uv_layer) > 0:
  808. face[2][i] = uniquedict(tmaps, list(uv_layer[li].uv[:]))
  809. faces.append(face)
  810. del me
  811. progress.step()
  812. progress.leave_substeps()
  813. ### Materials ###
  814. if use_materials:
  815. progress.step("Exporting Materials")
  816. progress.enter_substeps(len(refmats))
  817. matopa = {}
  818. matopa[-1] = -1
  819. for i,v in refmats.items():
  820. mi = v[0]
  821. mat = v[1]
  822. if mat is not None:
  823. props = {}
  824. d = 1.0
  825. if mat.node_tree:
  826. # at least try to get the diffuse texture from other material types,
  827. # because not all wrapped in PrincipledBSDF properly
  828. for n in mat.node_tree.nodes:
  829. if n.type == 'TEX_IMAGE' and n.image and n.image.filepath and n.image.filepath != "" and n.image.filepath != "//":
  830. imgpath, data = gettexture(n.image.filepath, use_inline)
  831. if imgpath != "":
  832. s = uniquedict(strs, imgpath)
  833. if use_inline and len(data) > 8:
  834. uniquedict(inlined, [s, data])
  835. props[128] = [128, s]
  836. break
  837. # otherwise properly parse material if blender can convert it into PrincipledBSDF
  838. mat_wrap = node_shader_utils.PrincipledBSDFWrapper(mat)
  839. if mat_wrap:
  840. for key, mat_wrap_key in mat_property_map.items():
  841. if key == 0:
  842. # Kd
  843. if mat_wrap.alpha != 0.0 and mat_wrap.alpha != 1.0:
  844. d = mat_wrap.alpha
  845. elif mat_wrap.base_color and len(mat_wrap.base_color) > 3:
  846. d = mat_wrap.base_color[3]
  847. else:
  848. d = 0.0
  849. if d != 0.0:
  850. props[0] = [0, uniquedict(cmap, [mat_wrap.base_color[0], mat_wrap.base_color[1], mat_wrap.base_color[2], d])]
  851. elif key == 8:
  852. # il
  853. il = 0
  854. if mat_wrap.specular == 0:
  855. il = 1
  856. elif mat_wrap.metallic != 0.0:
  857. if d != 1.0:
  858. il = 6
  859. else:
  860. il = 3
  861. elif d != 1.0:
  862. il = 9
  863. else:
  864. il = 2
  865. if il != 0:
  866. props[8] = [8, il]
  867. elif mat_wrap_key[0][0:2] == "//":
  868. continue
  869. try:
  870. val = getattr(mat_wrap, mat_wrap_key[1], None)
  871. except:
  872. continue
  873. if val is None:
  874. continue
  875. if key >= 128:
  876. # according to the doc, texture material attributes should always have val.image
  877. # but sometimes they don't... And sometimes filename is "//" for whatever reason...
  878. if val.image is None or val.image.filepath is None or val.image.filepath == "" or val.image.filepath == "//":
  879. continue
  880. imgpath, data = gettexture(val.image.filepath, use_inline)
  881. if imgpath == "":
  882. continue
  883. s = uniquedict(strs, imgpath)
  884. props[key] = [key, s]
  885. if use_inline and len(data) > 8:
  886. uniquedict(inlined, [s, data])
  887. elif mat_wrap_key[0] == "gscale" and val != 0.0:
  888. props[key] = [key, uniquedict(cmap, [val, val, val, 1.0])]
  889. elif mat_wrap_key[0] == "color" and len(val) == 3:
  890. props[key] = [key, uniquedict(cmap, [val[0], val[1], val[2], 1.0])]
  891. elif mat_wrap_key[0] == "color" and len(val) == 4:
  892. props[key] = [key, uniquedict(cmap, val)]
  893. elif mat_wrap_key[0] == "float" and val != 0.0:
  894. props[key] = [key, val]
  895. elif (mat_wrap_key[0] == "byte" or mat_wrap_key[0] == "int") and val != 0:
  896. props[key] = [key, val]
  897. else:
  898. report({"ERROR"}, "Material '" + mat.name + "' does not use PrincipledBSDF surface, not parsing.")
  899. # append material if it has at least one property
  900. if len(props) > 0:
  901. ni = uniquedict(strs, safestr(mat.name))
  902. matopa[ni] = 255 - int(255.0 * d)
  903. materials.append([ni, props])
  904. progress.step()
  905. progress.leave_substeps()
  906. # sort faces by opacity and material index
  907. for i,v in enumerate(faces):
  908. try:
  909. faces[i][4] = matopa[faces[i][0]]
  910. except:
  911. faces[i][4] = 255
  912. faces.sort(key=itemgetter(4,0))
  913. else:
  914. # sort faces by material index only
  915. faces.sort(key=itemgetter(0))
  916. ### Actions ###
  917. if use_animation:
  918. progress.step("Exporting Animations")
  919. if use_skeleton and len(bones) > 0:
  920. mpf = 1000.0/use_fps # msec per frame
  921. acts = []
  922. nf = 0 # number of total frames
  923. # collect actions from timeline markers, otherwise use actions
  924. if use_markers == True:
  925. if len(scene.timeline_markers) > 0:
  926. tlm = sorted(scene.timeline_markers, key=lambda tl: tl.frame)
  927. for i,t in enumerate(tlm):
  928. if i + 1 >= len(tlm):
  929. et = scene.frame_end
  930. else:
  931. et = tlm[i+1].frame - 1
  932. if et > t.frame:
  933. acts.append([safestr(t.name), -1, t.frame, et])
  934. nf = nf + et - t.frame
  935. del tlm
  936. else:
  937. for i,a in enumerate(bpy.data.actions):
  938. # pre 3.0 blender used frame_range for the animation's length
  939. # but post 3.0 blender uses that for a manually selected range
  940. st = et = 0
  941. try:
  942. st = int(a.curve_frame_range[0])
  943. et = int(a.curve_frame_range[1])
  944. except:
  945. st = int(a.frame_range[0])
  946. et = int(a.frame_range[1])
  947. if et > 0:
  948. acts.append([safestr(a.name), i, st, et])
  949. nf += et - st
  950. if nf == 0:
  951. # no actions nor markers, one big happy animation only
  952. acts.append(["Anim", -1, scene.frame_start, scene.frame_end])
  953. nf = scene.frame_end - scene.frame_start
  954. # ok, now 'acts' is an array of [action name, action pose index, start frame, end frame]
  955. progress.enter_substeps(nf + 1)
  956. for a in acts:
  957. # set action pose
  958. scene.frame_set(0, subframe=0.0)
  959. for i,ob_main in enumerate(objects):
  960. if ob_main.type != "ARMATURE":
  961. continue
  962. if a[1] != -1:
  963. ob_main.animation_data.action = bpy.data.actions[a[1]]
  964. ob_main.data.pose_position = "POSE"
  965. ob_main.data.update_tag()
  966. lf = 0
  967. frames = [] # collect frame with changed bones for this action
  968. lastpose = {} # fill up with bind pose on start
  969. for n,b in bones.items():
  970. lastpose[n] = [b[1][2], b[1][3]]
  971. # iterate through each frame, and set anim pose for the armature
  972. for frame in range(a[2], a[3] + 1):
  973. scene.frame_set(frame, subframe=0.0)
  974. # walk through the bones in anim pose, collect which one changed
  975. changed = []
  976. for i,ob_main in enumerate(objects):
  977. if ob_main.type != "ARMATURE":
  978. continue
  979. for i, b in enumerate(ob_main.pose.bones):
  980. try:
  981. idx = bones[b.name][0]
  982. except:
  983. report({"ERROR"}, "Animated bone name '" + b.name + "' does not match any bind-pose bone???")
  984. break;
  985. # we need model-space p,q only for bones without parents
  986. m = matnorm(global_matrix @ ob_main.matrix_world @ b.matrix)
  987. if use_relbones == True and b.parent:
  988. p = matnorm(global_matrix @ ob_main.matrix_world @ b.parent.matrix)
  989. m = p.inverted() @ m
  990. p = m.to_translation()
  991. q = m.to_quaternion()
  992. q.normalize()
  993. # differerent?
  994. pos = uniquedict(verts, vert(
  995. round(p[0], digits),
  996. round(p[1], digits),
  997. round(p[2], digits), 1.0, 0, -1))
  998. ori = uniquedict(verts, vert(
  999. round(q.x, digits),
  1000. round(q.y, digits),
  1001. round(q.z, digits),
  1002. round(q.w, digits), 0, -2))
  1003. if lastpose[b.name][0] != pos or lastpose[b.name][1] != ori:
  1004. changed.append([idx, pos, ori])
  1005. lastpose[b.name][0] = pos
  1006. lastpose[b.name][1] = ori
  1007. # do we have changed bones on this frame?
  1008. if len(changed) > 0:
  1009. if len(frames) < 1:
  1010. a[2] = frame
  1011. frames.append([int((frame-a[2]) * mpf), changed])
  1012. lf = frame
  1013. if len(changed) > fi_m:
  1014. fi_m = len(changed)
  1015. progress.step()
  1016. # if the action has at least one frame, save it
  1017. if len(frames) > 0:
  1018. actions.append([uniquedict(strs, safestr(a[0])), int((lf-a[2]+1) * mpf), frames])
  1019. progress.leave_substeps()
  1020. else:
  1021. report({"ERROR"}, "Trying to export animations without armature and skin")
  1022. # restore original armature
  1023. for i,ob_main in enumerate(objects):
  1024. if ob_main.type == "ARMATURE":
  1025. if oldaction != None and ob_main.animation_data:
  1026. try:
  1027. ob_main.animation_data.action = oldaction
  1028. except:
  1029. continue
  1030. ob_main.data.pose_position = oldpose[i]
  1031. ob_main.data.update_tag()
  1032. context.scene.frame_set(oldframe)
  1033. # we need lists, but creating unique lists in python is impossible, so we
  1034. # have used dictionaries. Let's convert those into lists now
  1035. cmap = dict2list(cmap)
  1036. strs = dict2list(strs)
  1037. verts = dict2list(verts)
  1038. tmaps = dict2list(tmaps)
  1039. bones = dict2list(bones)
  1040. skins = dict2list(skins)
  1041. inlined = dict2list(inlined)
  1042. # ----------------- End of Blender Specific Stuff ---------------------
  1043. # Now we should have:
  1044. # cmap = array of [r, g, b, a]
  1045. # strs = array of unique strings
  1046. # verts = array of [x, y, z, w, color, skinid]
  1047. # tmaps = array of [u, v]
  1048. # faces = array of [material strid, [3] vertexids, [3] normalvertexids, [3] tmapids }
  1049. # shapes =
  1050. # labels =
  1051. # materials = array of [material strid, dict of [property type, property value]]
  1052. # bones = array of [parent, name strid, pos vertexid, ori vertexid]
  1053. # skins = array of [[boneid, weight] * 8]
  1054. # actions = array of [action name strid, durationmsec, array of animation frames]
  1055. # anim frame = [timestampmsec, array of [boneid, pos vertexid, ori vertexid]]
  1056. # inlined = array of [name strid, bytes data]
  1057. # extras = array of [bytes[4] magic, bytes data]
  1058. #print("----------------------------------------------")
  1059. #print(cmap)
  1060. #print(strs)
  1061. #print(verts)
  1062. #print(tmaps)
  1063. #print(faces)
  1064. #print(shapes)
  1065. #print(labels)
  1066. #print(materials)
  1067. #print(bones)
  1068. #print(skins)
  1069. #print(actions)
  1070. #print(inlined)
  1071. #print(extras)
  1072. #print("----------------------------------------------")
  1073. # normalize coordinates
  1074. if use_gridcompress == True:
  1075. min_x = min_y = min_z = 1e10
  1076. max_x = max_y = max_z = -1e10
  1077. for v in verts:
  1078. if v[0] < min_x:
  1079. min_x = v[0]
  1080. if v[0] > max_x:
  1081. max_x = v[0]
  1082. if v[1] < min_y:
  1083. min_y = v[1]
  1084. if v[1] > max_y:
  1085. max_y = v[1]
  1086. if v[2] < min_z:
  1087. min_z = v[2]
  1088. if v[2] > max_z:
  1089. max_z = v[2]
  1090. s = max(abs(min_x), abs(max_x), abs(min_y), abs(max_y), abs(min_z), abs(max_z))
  1091. if s != 1.0 and s != 0.0:
  1092. for i,v in enumerate(verts):
  1093. if verts[i][5] != -2:
  1094. verts[i][0] = round(verts[i][0] / s, digits)
  1095. verts[i][1] = round(verts[i][1] / s, digits)
  1096. verts[i][2] = round(verts[i][2] / s, digits)
  1097. if use_scale <= 0.0:
  1098. use_scale = s
  1099. if use_scale <= 0.0:
  1100. use_scale = 1.0
  1101. # Construct chunks buffer from lists
  1102. progress.leave_substeps()
  1103. progress.step("Compressing output")
  1104. print(len(verts), "verts,", len(faces), "faces,", len(tmaps), "UVs", len(materials), "materials,", len(bones), "bones,", len(skins), "skins,", len(actions), "actions")
  1105. progress.enter_substeps(len(verts) + len(faces) + len(tmaps) + len(materials) + len(bones) + len(skins) + len(actions) + len(inlined))
  1106. # create string table and calculate string offsets
  1107. if use_author is None or use_author == "":
  1108. use_author = os.getenv("LOGNAME", "")
  1109. if use_ascii == True:
  1110. # save Model 3D ASCII variant
  1111. s = "3dmodel " + str(use_scale) + "\r\n"
  1112. s += safestr(use_name, 2) + "\r\n"
  1113. s += safestr(use_license, 2) + "\r\n"
  1114. s += safestr(use_author, 2) + "\r\n"
  1115. s += safestr(use_comment, 1) + "\r\n\r\n"
  1116. # materials
  1117. if len(materials) > 0:
  1118. print("Materials")
  1119. for m in materials:
  1120. progress.step()
  1121. s += "Material " + strs[m[0]] + "\r\n"
  1122. for pi,p in m[1].items():
  1123. t = mat_property_map[p[0]]
  1124. s += t[2] + " "
  1125. if t[0] == "color" or t[0] == "gscale":
  1126. s += "#"
  1127. for i in range(0, 4):
  1128. s += "%02x" % (int(cmap[p[1]][3 - i] * 255.0))
  1129. elif t[0] == "float":
  1130. s += str(round(p[1], digits))
  1131. elif p[0] >= 128:
  1132. s += strs[p[1]]
  1133. else:
  1134. s += str(p[1])
  1135. s += "\r\n"
  1136. s += "\r\n"
  1137. # texture map
  1138. if len(tmaps) > 0:
  1139. print("Texture map")
  1140. s += "Textmap\r\n"
  1141. r = True
  1142. for t in tmaps:
  1143. progress.step()
  1144. # failsafes
  1145. if t[0] < 0.0 or t[0] > 1.0 or t[1] < 0.0 or t[1] > 1.0:
  1146. if r:
  1147. r = False
  1148. report({"ERROR"}, "Texture UV's are out of 0..1 range")
  1149. if t[0] > 1.0:
  1150. t[0] = 1.0
  1151. if t[0] < 0.0:
  1152. t[0] = 0.0
  1153. if t[1] > 1.0:
  1154. t[1] = 1.0
  1155. if t[1] < 0.0:
  1156. t[1] = 0.0
  1157. s += str(round(t[0], digits)) + " " + str(round(t[1], digits)) + "\r\n"
  1158. s += "\r\n"
  1159. # vertex list
  1160. if len(verts) > 0:
  1161. print("Vertex")
  1162. s += "Vertex\r\n"
  1163. for v in verts:
  1164. progress.step()
  1165. s += str(v[0]) + " " + str(v[1]) + " " + str(v[2]) + " " + str(v[3])
  1166. if v[4] >= 0 and v[4] < len(cmap):
  1167. s += " #"
  1168. for i in range(0, 4):
  1169. s += "%02x" % (int(cmap[v[4]][3 - i] * 255.0))
  1170. elif v[5] >= 0 and v[5] < len(skins):
  1171. s += " #ffffffff"
  1172. if v[5] >= 0 and v[5] < len(skins):
  1173. for i in range(0, min(len(skins[v[5]]), 8)):
  1174. if skins[v[5]][i][0] != -1 and skins[v[5]][i][1] != 0:
  1175. s += " " + str(skins[v[5]][i][0]) + ":" + str(round(float(skins[v[5]][i][1]) / 255.0, 4))
  1176. s += "\r\n"
  1177. s += "\r\n"
  1178. # triangle mesh
  1179. if len(faces) > 0:
  1180. print("Faces")
  1181. s += "Mesh\r\n"
  1182. l = -1
  1183. for f in faces:
  1184. progress.step()
  1185. if l != f[0]:
  1186. l = f[0]
  1187. if l == -1:
  1188. s += "use\r\n"
  1189. else:
  1190. s += "use " + strs[l] + "\r\n"
  1191. for i,v in enumerate(f[1]):
  1192. if i != 0:
  1193. s += " "
  1194. s += str(v) + "/"
  1195. if use_uvs:
  1196. s += str(f[2][i])
  1197. s += "/"
  1198. if use_normals:
  1199. s += str(f[3][i])
  1200. s += "\r\n"
  1201. s += "\r\n"
  1202. # skeleton
  1203. if len(bones) > 0 or len(skins) > 0:
  1204. print("Bones")
  1205. s += "Bones\r\n"
  1206. s += bonestr(strs, bones, -1, 0)
  1207. s += "\r\n"
  1208. # actions (animations)
  1209. if len(actions) > 0:
  1210. print("Actions")
  1211. for a in actions:
  1212. progress.step()
  1213. if len(a[2]) < 1:
  1214. continue
  1215. s += "Action " + str(a[1]) + " " + strs[a[0]] + "\r\n"
  1216. for f in a[2]:
  1217. s += "frame " + str(f[0]) + "\r\n"
  1218. for t in f[1]:
  1219. s += str(t[0]) + " " + str(t[1]) + " " + str(t[2]) + "\r\n"
  1220. s += "\r\n"
  1221. # inlined assets
  1222. if len(inlined) > 0:
  1223. print("Inlined assets")
  1224. s += "Assets\r\n"
  1225. for i in inlined:
  1226. progress.step()
  1227. s += strs[i[0]] + ".png\r\n"
  1228. s += "\r\n"
  1229. # write out file
  1230. filepath = filepath[:len(filepath)-4] + ".a3d"
  1231. if use_strmcompress:
  1232. import gzip
  1233. print("Zlib compress")
  1234. # could have use gzip.open, but we need the compressed size too
  1235. s = gzip.compress(bytes(s, 'utf-8'), 9)
  1236. filepath += ".gz"
  1237. f = open(filepath, 'wb')
  1238. else:
  1239. f = open(filepath, 'w')
  1240. f.write(s)
  1241. f.close()
  1242. s = len(s)
  1243. else:
  1244. # save Model 3D binary variant
  1245. stridx = [0] * (len(strs))
  1246. st = bytes(safestr(use_name, 2), 'utf-8') + pack("<b", 0)
  1247. st = st + bytes(safestr(use_license, 2), 'utf-8') + pack("<b", 0)
  1248. st = st + bytes(safestr(use_author, 2), 'utf-8') + pack("<b", 0)
  1249. st = st + bytes(safestr(use_comment, 1), 'utf-8') + pack("<b", 0)
  1250. o = len(st)
  1251. for i, s in enumerate(strs):
  1252. s = bytes(s, 'utf-8') + pack("<b", 0)
  1253. st = st + s
  1254. stridx[i] = o
  1255. o = o + len(s)
  1256. # construct model header chunk
  1257. ci_s = idxsize(len(cmap))
  1258. ti_s = idxsize(len(tmaps))
  1259. vi_s = idxsize(len(verts))
  1260. si_s = idxsize(o)
  1261. bi_s = idxsize(len(bones))
  1262. sk_s = idxsize(len(skins))
  1263. hi_s = idxsize(len(shapes))
  1264. fi_s = idxsize(len(faces))
  1265. if nb_m < 2:
  1266. nb_s = 0
  1267. elif nb_m == 2:
  1268. nb_s = 1
  1269. elif nb_m <= 4:
  1270. nb_s = 2
  1271. else:
  1272. nb_s = 3
  1273. fc_s = idxsize(fi_m)
  1274. flags = (use_quality << 0) | (vi_s << 2) | (si_s << 4) | (ci_s << 6) | (ti_s << 8) | (bi_s << 10) | (nb_s << 12)
  1275. flags |= (sk_s << 14) | (fc_s << 16) | (hi_s << 18) | (fi_s << 20)
  1276. buf = pack("<f", use_scale) + pack("<I", flags) + st
  1277. buf = b'HEAD' + pack("<I",len(buf) + 8) + buf
  1278. # color map
  1279. if len(cmap) > 0 and ci_s < 4:
  1280. print("Color map")
  1281. buf = buf + b'CMAP' + pack("<I", len(cmap) * 4 + 8)
  1282. for col in cmap:
  1283. for i in range(0, 4):
  1284. buf = buf + pack("<B", int(col[i] * 255))
  1285. # texture map
  1286. if len(tmaps) > 0:
  1287. print("Texture map")
  1288. buf = buf + b'TMAP' + pack("<I", len(tmaps) * 2 * (1 << use_quality) + 8)
  1289. r = True
  1290. for t in tmaps:
  1291. progress.step()
  1292. # failsafes
  1293. if t[0] < 0.0 or t[0] > 1.0 or t[1] < 0.0 or t[1] > 1.0:
  1294. if r:
  1295. r = False
  1296. report({"ERROR"}, "Texture UV's are out of 0..1 range")
  1297. #print("Eeeeeek texture UV's are out of 0..1 range? Should never happen!", t)
  1298. if t[0] > 1.0:
  1299. t[0] = 1.0
  1300. if t[0] < 0.0:
  1301. t[0] = 0.0
  1302. if t[1] > 1.0:
  1303. t[1] = 1.0
  1304. if t[1] < 0.0:
  1305. t[1] = 0.0
  1306. if use_quality == 0:
  1307. buf = buf + pack("<BB", int(t[0] * 255), int(t[1] * 255))
  1308. elif use_quality == 1:
  1309. buf = buf + pack("<HH", int(t[0] * 65535), int(t[1] * 65535))
  1310. elif use_quality == 3:
  1311. buf = buf + pack("<dd", t[0], t[1])
  1312. else:
  1313. buf = buf + pack("<ff", t[0], t[1])
  1314. # vertex list
  1315. if len(verts) > 0:
  1316. print("Vertex")
  1317. o = b''
  1318. for v in verts:
  1319. progress.step()
  1320. for i in range(0, 4):
  1321. if use_quality == 0:
  1322. o = o + pack("<b", int(v[i] * 127))
  1323. elif use_quality == 1:
  1324. o = o + pack("<h", int(v[i] * 32767))
  1325. elif use_quality == 3:
  1326. o = o + pack("<d", v[i])
  1327. else:
  1328. o = o + pack("<f", v[i])
  1329. if ci_s < 4:
  1330. o = o + addidx(ci_s, v[4])
  1331. else:
  1332. o = o + pack("<I", cmap[v[4]])
  1333. o = o + addidx(sk_s, v[5])
  1334. buf = buf + b'VRTS' + pack("<I", len(o) + 8) + o
  1335. # skeleton
  1336. if len(bones) > 0 or len(skins) > 0:
  1337. print("Bones")
  1338. o = addidx(bi_s, len(bones)) + addidx(sk_s, len(skins))
  1339. for b in bones:
  1340. progress.step()
  1341. o = o + addidx(bi_s, b[0]) + addidx(si_s, stridx[b[1]]) + addidx(vi_s, b[2]) + addidx(vi_s, b[3])
  1342. print("Skins")
  1343. for s in skins:
  1344. progress.step()
  1345. if nb_s > 0:
  1346. for i in range(0, 1 << nb_s):
  1347. if i >= len(s):
  1348. o = o + pack("<B", 0)
  1349. else:
  1350. o = o + pack("<B", s[i][1])
  1351. for i in range(0, min(len(s), 1 << nb_s)):
  1352. if s[i][1] != 0:
  1353. o = o + addidx(bi_s, s[i][0])
  1354. buf = buf + b'BONE' + pack("<I", len(o) + 8) + o
  1355. # materials
  1356. if len(materials) > 0:
  1357. print("Materials")
  1358. for m in materials:
  1359. progress.step()
  1360. o = addidx(si_s, stridx[m[0]])
  1361. for pi,p in m[1].items():
  1362. o = o + pack("<B", p[0])
  1363. t = mat_property_map[p[0]]
  1364. if t[0] == "color" or t[0] == "gscale":
  1365. if ci_s < 4:
  1366. o = o + addidx(ci_s, p[1])
  1367. else:
  1368. o = o + pack("<I", cmap[p[1]])
  1369. elif t[0] == "byte" or t[0] == "//byte":
  1370. o = o + pack("<B", p[1])
  1371. elif p[0] >= 128:
  1372. o = o + addidx(si_s, stridx[p[1]])
  1373. else:
  1374. o = o + pack("<f", p[1])
  1375. buf = buf + b'MTRL' + pack("<I", len(o) + 8) + o
  1376. # triangle mesh
  1377. if len(faces) > 0:
  1378. print("Faces")
  1379. l = -1
  1380. o = b''
  1381. for f in faces:
  1382. progress.step()
  1383. if l != f[0]:
  1384. l = f[0]
  1385. o = o + pack("<b", 0) + addidx(si_s, stridx[l])
  1386. o = o + pack("<b", (len(f[1]) << 4) | (use_uvs) | (use_normals << 1))
  1387. for i,v in enumerate(f[1]):
  1388. o = o + addidx(vi_s, v)
  1389. if use_uvs:
  1390. o = o + addidx(ti_s, f[2][i])
  1391. if use_normals:
  1392. o = o + addidx(vi_s, f[3][i])
  1393. buf = buf + b'MESH' + pack("<I", len(o) + 8) + o
  1394. # shapes
  1395. if len(shapes) > 0:
  1396. print("Shapes")
  1397. l = -1
  1398. o = b''
  1399. for f in shapes:
  1400. o = o + b''
  1401. buf = buf + b'SHPE' + pack("<I", len(o) + 8) + o
  1402. # labels
  1403. if len(labels) > 0:
  1404. print("Labels")
  1405. l = -1
  1406. o = b''
  1407. for f in labels:
  1408. o = o + b''
  1409. buf = buf + b'LBLS' + pack("<I", len(o) + 8) + o
  1410. # actions (animations)
  1411. if len(actions) > 0:
  1412. print("Actions")
  1413. for a in actions:
  1414. progress.step()
  1415. if len(a[2]) < 1:
  1416. continue
  1417. o = addidx(si_s, stridx[a[0]]) + pack("<H", len(a[2])) + pack("<I", a[1])
  1418. for f in a[2]:
  1419. o = o + pack("<I", f[0]) + addidx(fc_s, len(f[1]))
  1420. for t in f[1]:
  1421. o = o + addidx(bi_s, t[0]) + addidx(vi_s, t[1]) + addidx(vi_s, t[2])
  1422. buf = buf + b'ACTN' + pack("<I", len(o) + 8) + o
  1423. # inlined assets
  1424. if len(inlined) > 0:
  1425. print("Inlined assets")
  1426. for i in inlined:
  1427. progress.step()
  1428. o = addidx(si_s, stridx[i[0]]) + i[1]
  1429. buf = buf + b'ASET' + pack("<I", len(o) + 8) + o
  1430. # extra chunks
  1431. if len(extras) > 0:
  1432. print("Extras")
  1433. for e in extras:
  1434. buf = buf + e[0][0:3] + pack("<I", len(e[1]) + 8) + e[1]
  1435. # End chunk
  1436. buf = buf + b'OMD3';
  1437. if use_strmcompress:
  1438. import zlib
  1439. print("Zlib compress")
  1440. buf = zlib.compress(buf, 9)
  1441. # add file header and write out file
  1442. f = open(filepath, 'wb')
  1443. s = len(buf) + 8
  1444. f.write(b'3DMO' + pack("<L", s) + buf)
  1445. f.close()
  1446. print("Finished!")
  1447. progress.leave_substeps("Finished!")
  1448. report({"INFO"}, "Model 3D " + filepath + " (" + str(s) + " bytes) exported.")
  1449. return {'FINISHED'}
  1450. # -----------------------------------------------------------------------------
  1451. # Blender integration
  1452. import bpy
  1453. from bpy.props import (
  1454. BoolProperty,
  1455. FloatProperty,
  1456. StringProperty,
  1457. IntProperty,
  1458. EnumProperty,
  1459. )
  1460. from bpy_extras.io_utils import (
  1461. ExportHelper,
  1462. ImportHelper,
  1463. axis_conversion,
  1464. )
  1465. class ImportM3D(bpy.types.Operator, ImportHelper):
  1466. """Load a Model 3D File (.m3d)"""
  1467. bl_idname = "import_scene.m3d"
  1468. bl_label = 'Import M3D'
  1469. bl_options = {'PRESET'}
  1470. filename_ext = ".m3d"
  1471. filter_glob: StringProperty(
  1472. default="*.m3d;*.a3d;*.a3d.gz",
  1473. options={'HIDDEN'},
  1474. )
  1475. def execute(self, context):
  1476. return read_m3d(context, self.filepath, self.report)
  1477. class ExportM3D(bpy.types.Operator, ExportHelper):
  1478. """Save a Model 3D File (.m3d)"""
  1479. bl_idname = "export_scene.m3d"
  1480. bl_label = 'Export M3D'
  1481. bl_options = {'PRESET'}
  1482. filename_ext = ".m3d"
  1483. filter_glob: StringProperty(
  1484. default="*.m3d",
  1485. options={'HIDDEN'},
  1486. )
  1487. # model properties
  1488. use_name: StringProperty(
  1489. name="Model Name",
  1490. description="Name of the exported model",
  1491. default="",
  1492. )
  1493. use_license: StringProperty(
  1494. name="License",
  1495. description="Licensing, copyright notice",
  1496. default="MIT",
  1497. )
  1498. use_author: StringProperty(
  1499. name="Author",
  1500. description="Your name and contact (email, git repo url etc.)",
  1501. default="",
  1502. )
  1503. use_comment: StringProperty(
  1504. name="Comment",
  1505. description="Any description or comment on the model",
  1506. default="",
  1507. )
  1508. use_scale: FloatProperty(
  1509. name="Scale (meter)",
  1510. description="Specify model space 1.0 in SI meters (use 0.0 to calculate)",
  1511. min=0.0, max=1000.0,
  1512. default=1.0,
  1513. )
  1514. # import range
  1515. use_selection: BoolProperty(
  1516. name="Selection Only",
  1517. description="Export selected objects only",
  1518. default=False,
  1519. )
  1520. use_mesh_modifiers: BoolProperty(
  1521. name="Apply Modifiers",
  1522. description="Apply modifiers",
  1523. default=True,
  1524. )
  1525. # export properties
  1526. use_normals: BoolProperty(
  1527. name="Include Normals",
  1528. description="Export one normal per vertex and per face, to represent flat faces and sharp edges",
  1529. default=True,
  1530. )
  1531. use_uvs: BoolProperty(
  1532. name="Include UVs",
  1533. description="Write out the active UV coordinates",
  1534. default=True,
  1535. )
  1536. use_colors: BoolProperty(
  1537. name="Include Vertex Colors",
  1538. description="Write out individual vertex colors (independent to material colors)",
  1539. default=True,
  1540. )
  1541. use_shapes: BoolProperty(
  1542. name="Write Shapes",
  1543. description="Write out as parameterized shapes",
  1544. default=False,
  1545. )
  1546. use_materials: BoolProperty(
  1547. name="Write Materials",
  1548. description="Write out the materials",
  1549. default=True,
  1550. )
  1551. use_skeleton: BoolProperty(
  1552. name="Write Armature",
  1553. description="Write out armature (bones hiearachy and skin)",
  1554. default=True,
  1555. )
  1556. use_animation: BoolProperty(
  1557. name="Write Animation",
  1558. description="Write out actions (implies armature)",
  1559. default=True,
  1560. )
  1561. use_markers: BoolProperty(
  1562. name="Use Markers",
  1563. description="Use timeline markers for animations instead of actions",
  1564. default=False,
  1565. )
  1566. use_fps: IntProperty(
  1567. name="FPS",
  1568. description="Specify frame per second. Blender only nows about frames",
  1569. min=1, max=120,
  1570. default=25,
  1571. )
  1572. use_quality: EnumProperty(
  1573. name="Precision",
  1574. items=(('-1','auto', 'choose depending on the number of polygons'),
  1575. ('0', '8 bits (int8)', '1/256 coordinate unit (for low poly models)'),
  1576. ('1','16 bits (int16)', '1/65536 coordinate unit (more than enough in most cases)'),
  1577. ('2','32 bits (float)', 'float precision coordinates (used by most other binary formats)'),
  1578. ('3','64 bits (double)', 'double precision coordinates (rarely needed)'),
  1579. ),
  1580. description="Coordinate grid system's size and precision",
  1581. default='-1',
  1582. )
  1583. use_inline: BoolProperty(
  1584. name="Embed Assets",
  1585. description="Inline assets (like textures) into output, create a single file that contains everything",
  1586. default=False,
  1587. )
  1588. use_gridcompress: BoolProperty(
  1589. name="Use Gridcompression",
  1590. description="Use lossy compression, achieve much smaller files by sacrificing a little bit of model quality",
  1591. default=True,
  1592. )
  1593. use_strmcompress: BoolProperty(
  1594. name="Use Streamcompression",
  1595. description="Use lossless deflate on binary data. Unless you're writing your own M3D parser, keep it checked",
  1596. default=True,
  1597. )
  1598. use_ascii: BoolProperty(
  1599. name="Use ASCII variant",
  1600. description="Use plain text variant of Model 3D for output",
  1601. default=False,
  1602. )
  1603. def execute(self, context):
  1604. # Exit edit mode before exporting, so current object states are exported properly.
  1605. if bpy.ops.object.mode_set.poll():
  1606. bpy.ops.object.mode_set(mode='OBJECT')
  1607. keywords = self.as_keywords(ignore=("filepath", "filter_glob"))
  1608. return write_m3d(context, self.filepath, self.report, **keywords)
  1609. def menu_func_export(self, context):
  1610. self.layout.operator(ExportM3D.bl_idname, text="Model 3D (.m3d)")
  1611. def menu_func_import(self, context):
  1612. self.layout.operator(ImportM3D.bl_idname, text="Model 3D (.m3d/.a3d)")
  1613. def register():
  1614. bpy.utils.register_class(ExportM3D)
  1615. bpy.utils.register_class(ImportM3D)
  1616. bpy.types.TOPBAR_MT_file_export.append(menu_func_export)
  1617. # bpy.types.TOPBAR_MT_file_import.append(menu_func_import)
  1618. def unregister():
  1619. bpy.types.TOPBAR_MT_file_export.remove(menu_func_export)
  1620. # bpy.types.TOPBAR_MT_file_import.remove(menu_func_import)
  1621. bpy.utils.unregister_class(ExportM3D)
  1622. bpy.utils.unregister_class(ImportM3D)
  1623. if __name__ == "__main__":
  1624. register()