io_export_arm.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495
  1. """Armory Mesh Exporter"""
  2. #
  3. # Based on Open Game Engine Exchange
  4. # https://opengex.org/
  5. # Export plugin for Blender by Eric Lengyel
  6. # Copyright 2015, Terathon Software LLC
  7. #
  8. # This software is licensed under the Creative Commons
  9. # Attribution-ShareAlike 3.0 Unported License:
  10. # http://creativecommons.org/licenses/by-sa/3.0/deed.en_US
  11. import io
  12. import os
  13. import struct
  14. import time
  15. import bpy
  16. from bpy_extras.io_utils import ExportHelper
  17. from mathutils import Vector
  18. import numpy as np
  19. bl_info = {
  20. "name": "Armory Mesh Exporter",
  21. "category": "Import-Export",
  22. "location": "File -> Export",
  23. "description": "Armory mesh data",
  24. "author": "Armory3D.org",
  25. "version": (2025, 8, 0),
  26. "blender": (4, 3, 1),
  27. "doc_url": "",
  28. "tracker_url": "",
  29. }
  30. NodeTypeMesh = 1
  31. structIdentifier = ["object", "mesh_object"]
  32. class ArmoryExporter(bpy.types.Operator, ExportHelper):
  33. """Export to Armory format"""
  34. bl_idname = "export_scene.arm"
  35. bl_label = "Export Armory"
  36. filename_ext = ".arm"
  37. def execute(self, context):
  38. profile_time = time.time()
  39. current_frame = context.scene.frame_current
  40. current_subframe = context.scene.frame_subframe
  41. self.scene = context.scene
  42. self.output = {}
  43. self.bobjectArray = {}
  44. self.meshArray = {}
  45. self.depsgraph = context.evaluated_depsgraph_get()
  46. scene_objects = self.scene.collection.all_objects
  47. for bobject in scene_objects:
  48. if not bobject.parent:
  49. self.process_bobject(bobject)
  50. self.output["name"] = self.scene.name
  51. self.output["objects"] = []
  52. for bo in scene_objects:
  53. if not bo.parent:
  54. self.export_object(bo, self.scene)
  55. self.output["mesh_datas"] = []
  56. for o in self.meshArray.items():
  57. self.export_mesh(o)
  58. self.output["camera_datas"] = None
  59. self.output["camera_ref"] = None
  60. self.output["material_datas"] = None
  61. self.output["shader_datas"] = None
  62. self.output["world_datas"] = None
  63. self.output["world_ref"] = None
  64. self.output["speaker_datas"] = None
  65. self.output["embedded_datas"] = None
  66. self.write_arm(self.filepath, self.output)
  67. self.scene.frame_set(current_frame, subframe=current_subframe)
  68. print(f"Scene exported in {str(time.time() - profile_time)}")
  69. return {"FINISHED"}
  70. def write_arm(self, filepath, output):
  71. with open(filepath, "wb") as f:
  72. f.write(packb(output))
  73. def write_matrix(self, matrix):
  74. return [
  75. matrix[0][0],
  76. matrix[0][1],
  77. matrix[0][2],
  78. matrix[0][3],
  79. matrix[1][0],
  80. matrix[1][1],
  81. matrix[1][2],
  82. matrix[1][3],
  83. matrix[2][0],
  84. matrix[2][1],
  85. matrix[2][2],
  86. matrix[2][3],
  87. matrix[3][0],
  88. matrix[3][1],
  89. matrix[3][2],
  90. matrix[3][3],
  91. ]
  92. def process_bobject(self, bobject):
  93. if bobject.type not in ["MESH"]:
  94. return
  95. btype = NodeTypeMesh if bobject.type == "MESH" else 0
  96. self.bobjectArray[bobject] = {"objectType": btype, "structName": bobject.name}
  97. for subbobject in bobject.children:
  98. self.process_bobject(subbobject)
  99. def export_object(self, bobject, scene, parento=None):
  100. if bobjectRef := self.bobjectArray.get(bobject):
  101. o = {}
  102. o["name"] = bobjectRef["structName"]
  103. o["type"] = structIdentifier[bobjectRef["objectType"]]
  104. o["data_ref"] = None
  105. o["transform"] = self.write_matrix(bobject.matrix_local)
  106. o["dimensions"] = None
  107. o["visible"] = True
  108. o["spawn"] = True
  109. o["anim"] = None
  110. o["material_refs"] = None
  111. o["children"] = None
  112. if bobjectRef["objectType"] == NodeTypeMesh:
  113. objref = bobject.data
  114. if objref not in self.meshArray:
  115. self.meshArray[objref] = {
  116. "structName": objref.name,
  117. "objectTable": [bobject],
  118. }
  119. else:
  120. self.meshArray[objref]["objectTable"].append(bobject)
  121. oid = self.meshArray[objref]["structName"]
  122. o["data_ref"] = oid
  123. o["dimensions"] = self.calc_aabb(bobject)
  124. if parento is None:
  125. self.output["objects"].append(o)
  126. else:
  127. parento["children"].append(o)
  128. if not hasattr(o, "children") and len(bobject.children) > 0:
  129. o["children"] = []
  130. for subbobject in bobject.children:
  131. self.export_object(subbobject, scene, o)
  132. def calc_aabb(self, bobject):
  133. aabb_center = 0.125 * sum((Vector(b) for b in bobject.bound_box), Vector())
  134. return [
  135. abs(
  136. (bobject.bound_box[6][0] - bobject.bound_box[0][0]) / 2
  137. + abs(aabb_center[0])
  138. )
  139. * 2,
  140. abs(
  141. (bobject.bound_box[6][1] - bobject.bound_box[0][1]) / 2
  142. + abs(aabb_center[1])
  143. )
  144. * 2,
  145. abs(
  146. (bobject.bound_box[6][2] - bobject.bound_box[0][2]) / 2
  147. + abs(aabb_center[2])
  148. )
  149. * 2,
  150. ]
  151. def export_mesh_data(self, exportMesh, bobject, o, has_armature=False):
  152. exportMesh.calc_loop_triangles()
  153. loops = exportMesh.loops
  154. num_verts = len(loops)
  155. num_uv_layers = len(exportMesh.uv_layers)
  156. num_colors = len(exportMesh.vertex_colors)
  157. has_tex = num_uv_layers > 0
  158. has_tex1 = num_uv_layers > 1
  159. has_col = num_colors > 0
  160. has_tang = False
  161. # Scale for packed coords
  162. aabb = self.calc_aabb(bobject)
  163. maxdim = max(aabb[0], max(aabb[1], aabb[2]))
  164. if maxdim > 2:
  165. o["scale_pos"] = maxdim / 2
  166. else:
  167. o["scale_pos"] = 1.0
  168. pdata = np.empty(num_verts * 4, dtype="<f4") # p.xyz, n.z
  169. ndata = np.empty(num_verts * 2, dtype="<f4") # n.xy
  170. if has_tex:
  171. t0map = 0 # Get active uvmap
  172. t0data = np.empty(num_verts * 2, dtype="<f4")
  173. uv_layers = exportMesh.uv_layers
  174. if uv_layers is not None:
  175. for i in range(0, len(uv_layers)):
  176. if uv_layers[i].active_render:
  177. t0map = i
  178. break
  179. if has_tex1:
  180. t1map = 1 if t0map == 0 else 0
  181. t1data = np.empty(num_verts * 2, dtype="<f4")
  182. # Scale for packed coords
  183. maxdim = 1.0
  184. lay0 = uv_layers[t0map]
  185. for v in lay0.data:
  186. if abs(v.uv[0]) > maxdim:
  187. maxdim = abs(v.uv[0])
  188. if abs(v.uv[1]) > maxdim:
  189. maxdim = abs(v.uv[1])
  190. if has_tex1:
  191. lay1 = uv_layers[t1map]
  192. for v in lay1.data:
  193. if abs(v.uv[0]) > maxdim:
  194. maxdim = abs(v.uv[0])
  195. if abs(v.uv[1]) > maxdim:
  196. maxdim = abs(v.uv[1])
  197. if maxdim > 1:
  198. o["scale_tex"] = maxdim
  199. invscale_tex = (1 / o["scale_tex"]) * 32767
  200. else:
  201. o["scale_tex"] = 1.0
  202. invscale_tex = 1 * 32767
  203. if has_tang:
  204. exportMesh.calc_tangents(uvmap=lay0.name)
  205. tangdata = np.empty(num_verts * 4, dtype="<f4")
  206. if has_col:
  207. cdata = np.empty(num_verts * 4, dtype="<f4")
  208. scale_pos = o["scale_pos"]
  209. invscale_pos = (1 / scale_pos) * 32767
  210. verts = exportMesh.vertices
  211. if has_tex:
  212. lay0 = exportMesh.uv_layers[t0map]
  213. if has_tex1:
  214. lay1 = exportMesh.uv_layers[t1map]
  215. if has_col:
  216. vcol0 = exportMesh.vertex_colors[0].data
  217. for i, loop in enumerate(loops):
  218. v = verts[loop.vertex_index]
  219. co = v.co
  220. normal = loop.normal
  221. tang = loop.tangent
  222. i4 = i * 4
  223. i2 = i * 2
  224. pdata[i4] = co[0]
  225. pdata[i4 + 1] = co[1]
  226. pdata[i4 + 2] = co[2]
  227. pdata[i4 + 3] = normal[2] * scale_pos # Cancel scale
  228. ndata[i2] = normal[0]
  229. ndata[i2 + 1] = normal[1]
  230. if has_tex:
  231. uv = lay0.data[loop.index].uv
  232. t0data[i2] = uv[0]
  233. t0data[i2 + 1] = 1.0 - uv[1] # Reverse Y
  234. if has_tex1:
  235. uv = lay1.data[loop.index].uv
  236. t1data[i2] = uv[0]
  237. t1data[i2 + 1] = 1.0 - uv[1]
  238. if has_tang:
  239. i4 = i * 4
  240. tangdata[i4] = tang[0]
  241. tangdata[i4 + 1] = tang[1]
  242. tangdata[i4 + 2] = tang[2]
  243. if has_col:
  244. col = vcol0[loop.index].color
  245. i4 = i * 4
  246. cdata[i4] = col[0]
  247. cdata[i4 + 1] = col[1]
  248. cdata[i4 + 2] = col[2]
  249. cdata[i4 + 3] = col[3]
  250. # Pack
  251. pdata *= invscale_pos
  252. ndata *= 32767
  253. pdata = np.array(pdata, dtype="<i2")
  254. ndata = np.array(ndata, dtype="<i2")
  255. if has_tex:
  256. t0data *= invscale_tex
  257. t0data = np.array(t0data, dtype="<i2")
  258. if has_tex1:
  259. t1data *= invscale_tex
  260. t1data = np.array(t1data, dtype="<i2")
  261. if has_col:
  262. cdata *= 32767
  263. cdata = np.array(cdata, dtype="<i2")
  264. if has_tang:
  265. tangdata *= 32767
  266. tangdata = np.array(tangdata, dtype="<i2")
  267. # Output
  268. o["vertex_arrays"] = []
  269. o["vertex_arrays"].append(
  270. {"attrib": "pos", "data": "short4norm", "values": pdata}
  271. )
  272. o["vertex_arrays"].append(
  273. {"attrib": "nor", "data": "short2norm", "values": ndata}
  274. )
  275. if has_tex:
  276. o["vertex_arrays"].append(
  277. {"attrib": "tex", "data": "short2norm", "values": t0data}
  278. )
  279. if has_tex1:
  280. o["vertex_arrays"].append(
  281. {"attrib": "tex1", "data": "short2norm", "values": t1data}
  282. )
  283. if has_col:
  284. o["vertex_arrays"].append(
  285. {"attrib": "col", "data": "short4norm", "values": cdata}
  286. )
  287. if has_tang:
  288. o["vertex_arrays"].append(
  289. {
  290. "attrib": "tang",
  291. "data": "short4norm",
  292. "values": tangdata,
  293. }
  294. )
  295. mats = exportMesh.materials
  296. poly_map = []
  297. for i in range(max(len(mats), 1)):
  298. poly_map.append([])
  299. for poly in exportMesh.polygons:
  300. poly_map[poly.material_index].append(poly)
  301. # map polygon indices to triangle loops
  302. tri_loops = {}
  303. for loop in exportMesh.loop_triangles:
  304. if loop.polygon_index not in tri_loops:
  305. tri_loops[loop.polygon_index] = []
  306. tri_loops[loop.polygon_index].append(loop)
  307. for index, polys in enumerate(poly_map):
  308. tris = 0
  309. for poly in polys:
  310. tris += poly.loop_total - 2
  311. if tris == 0: # No face assigned
  312. continue
  313. prim = np.empty(tris * 3, dtype="<i4")
  314. i = 0
  315. for poly in polys:
  316. for loop in tri_loops[poly.index]:
  317. prim[i] = loops[loop.loops[0]].index
  318. prim[i + 1] = loops[loop.loops[1]].index
  319. prim[i + 2] = loops[loop.loops[2]].index
  320. i += 3
  321. o["index_array"] = prim
  322. def export_mesh(self, objectRef):
  323. # This function exports a single mesh object
  324. table = objectRef[1]["objectTable"]
  325. bobject = table[0]
  326. oid = objectRef[1]["structName"]
  327. o = {}
  328. o["name"] = oid
  329. armature = bobject.find_armature()
  330. apply_modifiers = not armature
  331. bobject_eval = (
  332. bobject.evaluated_get(self.depsgraph) if apply_modifiers else bobject
  333. )
  334. exportMesh = bobject_eval.to_mesh()
  335. self.export_mesh_data(exportMesh, bobject, o, has_armature=armature is not None)
  336. self.output["mesh_datas"].append(o)
  337. bobject_eval.to_mesh_clear()
  338. def menu_func(self, context):
  339. self.layout.operator(ArmoryExporter.bl_idname, text="Armory (.arm)")
  340. def register():
  341. bpy.utils.register_class(ArmoryExporter)
  342. bpy.types.TOPBAR_MT_file_export.append(menu_func)
  343. def unregister():
  344. bpy.types.TOPBAR_MT_file_export.remove(menu_func)
  345. bpy.utils.unregister_class(ArmoryExporter)
  346. if __name__ == "__main__":
  347. register()
  348. # Msgpack parser with typed arrays
  349. # Based on u-msgpack-python v2.4.1 - v at sergeev.io
  350. # https://github.com/vsergeev/u-msgpack-python
  351. #
  352. # Permission is hereby granted, free of charge, to any person obtaining a copy
  353. # of this software and associated documentation files (the "Software"), to deal
  354. # in the Software without restriction, including without limitation the rights
  355. # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  356. # copies of the Software, and to permit persons to whom the Software is
  357. # furnished to do so, subject to the following conditions:
  358. #
  359. # The above copyright notice and this permission notice shall be included in
  360. # all copies or substantial portions of the Software.
  361. #
  362. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  363. # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  364. # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  365. # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  366. # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  367. # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  368. # THE SOFTWARE.
  369. def _pack_integer(obj, fp):
  370. fp.write(b"\xd2" + struct.pack("<i", obj))
  371. def _pack_nil(fp):
  372. fp.write(b"\xc0")
  373. def _pack_boolean(obj, fp):
  374. fp.write(b"\xc3" if obj else b"\xc2")
  375. def _pack_float(obj, fp):
  376. fp.write(b"\xca" + struct.pack("<f", obj))
  377. def _pack_string(obj, fp):
  378. obj = obj.encode("utf-8")
  379. fp.write(b"\xdb" + struct.pack("<I", len(obj)) + obj)
  380. def _pack_binary(obj, fp):
  381. fp.write(b"\xc6" + struct.pack("<I", len(obj)) + obj)
  382. def _pack_array(obj, fp):
  383. fp.write(b"\xdd" + struct.pack("<I", len(obj)))
  384. if len(obj) > 0 and isinstance(obj[0], float):
  385. fp.write(b"\xca")
  386. for e in obj:
  387. fp.write(struct.pack("<f", e))
  388. elif len(obj) > 0 and isinstance(obj[0], bool):
  389. for e in obj:
  390. pack(e, fp)
  391. elif len(obj) > 0 and isinstance(obj[0], int):
  392. fp.write(b"\xd2")
  393. for e in obj:
  394. fp.write(struct.pack("<i", e))
  395. # Float32
  396. elif len(obj) > 0 and isinstance(obj[0], np.float32):
  397. fp.write(b"\xca")
  398. fp.write(obj.tobytes())
  399. # Int32
  400. elif len(obj) > 0 and isinstance(obj[0], np.int32):
  401. fp.write(b"\xd2")
  402. fp.write(obj.tobytes())
  403. # Int16
  404. elif len(obj) > 0 and isinstance(obj[0], np.int16):
  405. fp.write(b"\xd1")
  406. fp.write(obj.tobytes())
  407. # Regular
  408. else:
  409. for e in obj:
  410. pack(e, fp)
  411. def _pack_map(obj, fp):
  412. fp.write(b"\xdf" + struct.pack("<I", len(obj)))
  413. for k, v in obj.items():
  414. pack(k, fp)
  415. pack(v, fp)
  416. def pack(obj, fp):
  417. if obj is None:
  418. _pack_nil(fp)
  419. elif isinstance(obj, bool):
  420. _pack_boolean(obj, fp)
  421. elif isinstance(obj, int):
  422. _pack_integer(obj, fp)
  423. elif isinstance(obj, float):
  424. _pack_float(obj, fp)
  425. elif isinstance(obj, str):
  426. _pack_string(obj, fp)
  427. elif isinstance(obj, bytes):
  428. _pack_binary(obj, fp)
  429. elif isinstance(obj, (list, np.ndarray, tuple)):
  430. _pack_array(obj, fp)
  431. elif isinstance(obj, dict):
  432. _pack_map(obj, fp)
  433. def packb(obj):
  434. fp = io.BytesIO()
  435. pack(obj, fp)
  436. return fp.getvalue()