export_godot.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. # ##### BEGIN GPL LICENSE BLOCK #####
  2. #
  3. # This program is free software; you can redistribute it and/or
  4. # modify it under the terms of the GNU General Public License
  5. # as published by the Free Software Foundation; either version 2
  6. # of the License, or (at your option) any later version.
  7. #
  8. # This program is distributed in the hope that it will be useful,
  9. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. # GNU General Public License for more details.
  12. #
  13. # You should have received a copy of the GNU General Public License
  14. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  15. #
  16. # ##### END GPL LICENSE BLOCK #####
  17. # Script copyright (C) Juan Linietsky
  18. # Contact Info: [email protected]
  19. """
  20. This script is an exporter to Godot Engine
  21. http://www.godotengine.org
  22. """
  23. import os
  24. import collections
  25. import functools
  26. import logging
  27. import bpy
  28. from . import structures
  29. from . import converters
  30. from .structures import (_AXIS_CORRECT, NodePath)
  31. logging.basicConfig(level=logging.INFO, format="[%(levelname)s]: %(message)s")
  32. @functools.lru_cache(maxsize=1) # Cache it so we don't search lots of times
  33. def find_godot_project_dir(export_path):
  34. """Finds the project.godot file assuming that the export path
  35. is inside a project (looks for a project.godot file)"""
  36. project_dir = export_path
  37. # Search up until we get to the top, which is "/" in *nix.
  38. # Standard Windows ends up as, e.g., "C:\", and independent of what else is
  39. # in the world, we can at least watch for repeats, because that's bad.
  40. last = None
  41. while not os.path.isfile(os.path.join(project_dir, "project.godot")):
  42. project_dir = os.path.split(project_dir)[0]
  43. if project_dir in ("/", last):
  44. raise structures.ValidationError(
  45. "Unable to find Godot project file"
  46. )
  47. last = project_dir
  48. logging.info("Found Godot project directory at %s", project_dir)
  49. return project_dir
  50. class ExporterLogHandler(logging.Handler):
  51. """Custom handler for exporter, would report logging message
  52. to GUI"""
  53. def __init__(self, operator):
  54. super().__init__()
  55. self.setLevel(logging.WARNING)
  56. self.setFormatter(logging.Formatter("%(message)s"))
  57. self.blender_op = operator
  58. def emit(self, record):
  59. if record.levelno == logging.WARNING:
  60. self.blender_op.report({'WARNING'}, record.message)
  61. else:
  62. self.blender_op.report({'ERROR'}, record.message)
  63. class GodotExporter:
  64. """Handles picking what nodes to export and kicks off the export process"""
  65. def export_object(self, obj, parent_gd_node):
  66. """Recursively export a object. It calls the export_object function on
  67. all of the objects children. If you have heirarchies more than 1000
  68. objects deep, this will fail with a recursion error"""
  69. if obj not in self.valid_objects:
  70. return
  71. logging.info("Exporting Blender object: %s", obj.name)
  72. prev_node = bpy.context.view_layer.objects.active
  73. bpy.context.view_layer.objects.active = obj
  74. # Figure out what function will perform the export of this object
  75. if obj.type not in converters.BLENDER_TYPE_TO_EXPORTER:
  76. logging.warning(
  77. "Unknown object type. Treating as empty: %s", obj.name
  78. )
  79. elif obj in self.exporting_objects:
  80. exporter = converters.BLENDER_TYPE_TO_EXPORTER[obj.type]
  81. else:
  82. logging.warning(
  83. "Object is parent of exported objects. "
  84. "Treating as empty: %s", obj.name
  85. )
  86. exporter = converters.BLENDER_TYPE_TO_EXPORTER["EMPTY"]
  87. is_bone_attachment = False
  88. if ("ARMATURE" in self.config['object_types'] and
  89. obj.parent and obj.parent_bone != ''):
  90. is_bone_attachment = True
  91. parent_gd_node = converters.BONE_ATTACHMENT_EXPORTER(
  92. self.escn_file,
  93. self.config,
  94. obj,
  95. parent_gd_node
  96. )
  97. if ("PARTICLE" in self.config['object_types'] and
  98. converters.has_particle(obj)):
  99. converters.MULTIMESH_EXPORTER(
  100. self.escn_file,
  101. self.config,
  102. obj,
  103. parent_gd_node
  104. )
  105. # Perform the export, note that `exported_node.parent` not
  106. # always the same as `parent_gd_node`, as sometimes, one
  107. # blender node exported as two parented node
  108. exported_node = exporter(self.escn_file, self.config, obj,
  109. parent_gd_node)
  110. self.bl_object_gd_node_map[obj] = exported_node
  111. if is_bone_attachment:
  112. for child in parent_gd_node.children:
  113. child['transform'] = structures.fix_bone_attachment_transform(
  114. obj, child['transform']
  115. )
  116. # CollisionShape node has different direction in blender
  117. # and godot, so it has a -90 rotation around X axis,
  118. # here rotate its children back
  119. if (hasattr(exported_node, "parent") and
  120. exported_node.parent.get_type() == 'CollisionShape'):
  121. exported_node['transform'] = (
  122. _AXIS_CORRECT.inverted() @
  123. exported_node['transform'])
  124. # if the blender node is exported and it has animation data
  125. if exported_node != parent_gd_node:
  126. converters.ANIMATION_DATA_EXPORTER(
  127. self.escn_file,
  128. self.config,
  129. exported_node,
  130. obj,
  131. "transform"
  132. )
  133. for child in obj.children:
  134. self.export_object(child, exported_node)
  135. bpy.context.view_layer.objects.active = prev_node
  136. def should_export_object(self, obj):
  137. """Checks if a node should be exported:"""
  138. if obj.type not in self.config["object_types"]:
  139. return False
  140. if self.config["use_included_in_render"] and obj.hide_render:
  141. return False
  142. if self.config["use_visible_objects"]:
  143. view_layer = bpy.context.view_layer
  144. if obj.name not in view_layer.objects:
  145. return False
  146. if not obj.visible_get():
  147. return False
  148. if self.config["use_export_selected"] and not obj.select_get():
  149. return False
  150. return True
  151. def export_scene(self):
  152. # pylint: disable-msg=too-many-branches
  153. """Decide what objects to export, and export them!"""
  154. logging.info("Exporting scene: %s", self.scene.name)
  155. in_edit_mode = False
  156. if bpy.context.object and bpy.context.object.mode == "EDIT":
  157. in_edit_mode = True
  158. bpy.ops.object.editmode_toggle()
  159. # Decide what objects to export
  160. for obj in self.scene.objects:
  161. if obj in self.exporting_objects:
  162. continue
  163. if self.should_export_object(obj):
  164. self.exporting_objects.add(obj)
  165. # Ensure parents of current valid object is
  166. # going to the exporting recursion
  167. tmp = obj
  168. while tmp is not None:
  169. if tmp not in self.valid_objects:
  170. self.valid_objects.add(tmp)
  171. else:
  172. break
  173. tmp = tmp.parent
  174. logging.info("Exporting %d objects", len(self.valid_objects))
  175. # Scene root
  176. root_gd_node = structures.NodeTemplate(
  177. self.scene.name,
  178. "Spatial",
  179. None
  180. )
  181. self.escn_file.add_node(root_gd_node)
  182. for obj in self.scene.objects:
  183. if obj in self.valid_objects and obj.parent is None:
  184. # recursive exporting on root object
  185. self.export_object(obj, root_gd_node)
  186. if "ARMATURE" in self.config['object_types']:
  187. for bl_obj in self.bl_object_gd_node_map:
  188. for mod in bl_obj.modifiers:
  189. if mod.type == "ARMATURE":
  190. mesh_node = self.bl_object_gd_node_map[bl_obj]
  191. skeleton_node = self.bl_object_gd_node_map[mod.object]
  192. mesh_node['skeleton'] = NodePath(
  193. mesh_node.get_path(), skeleton_node.get_path())
  194. if in_edit_mode:
  195. bpy.ops.object.editmode_toggle()
  196. def load_supported_features(self):
  197. """According to `project.godot`, determine all new feature supported
  198. by that godot version"""
  199. project_dir = ""
  200. try:
  201. project_dir = self.config["project_path_func"]()
  202. except structures.ValidationError:
  203. project_dir = False
  204. logging.warning(
  205. "Not export to Godot project dir, disable all beta features.")
  206. # minimal supported version
  207. conf_versiton = 3
  208. if project_dir:
  209. project_file_path = os.path.join(project_dir, "project.godot")
  210. with open(project_file_path, "r") as proj_f:
  211. for line in proj_f:
  212. if not line.startswith("config_version"):
  213. continue
  214. _, version_str = tuple(line.split("="))
  215. conf_versiton = int(version_str)
  216. break
  217. if conf_versiton < 2:
  218. logging.error(
  219. "Godot version smaller than 3.0, not supported by this addon")
  220. if conf_versiton >= 4:
  221. # godot >=3.1
  222. self.config["feature_bezier_track"] = True
  223. def export(self):
  224. """Begin the export"""
  225. self.escn_file = structures.ESCNFile(structures.FileEntry(
  226. "gd_scene",
  227. collections.OrderedDict((
  228. ("load_steps", 1),
  229. ("format", 2)
  230. ))
  231. ))
  232. self.export_scene()
  233. self.escn_file.fix_paths(self.config)
  234. with open(self.path, 'w') as out_file:
  235. out_file.write(self.escn_file.to_string())
  236. return True
  237. def __init__(self, path, kwargs, operator):
  238. self.path = path
  239. self.operator = operator
  240. self.scene = bpy.context.scene
  241. self.config = kwargs
  242. self.config["path"] = path
  243. self.config["project_path_func"] = functools.partial(
  244. find_godot_project_dir, path
  245. )
  246. # valid object would contain object should be exported
  247. # and their parents to retain the hierarchy
  248. self.valid_objects = set()
  249. # exporting objects would only contain objects need
  250. # to be exported
  251. self.exporting_objects = set()
  252. # optional features
  253. self.config["feature_bezier_track"] = False
  254. if self.config["use_beta_features"]:
  255. self.load_supported_features()
  256. self.escn_file = None
  257. self.bl_object_gd_node_map = {}
  258. def __enter__(self):
  259. return self
  260. def __exit__(self, *exc):
  261. pass
  262. def save(operator, context, filepath="", **kwargs):
  263. """Begin the export"""
  264. object_types = kwargs["object_types"]
  265. # GEOMETRY isn't an object type so replace it with all valid geometry based
  266. # object types
  267. if "GEOMETRY" in object_types:
  268. object_types.remove("GEOMETRY")
  269. object_types |= {"MESH", "CURVE", "SURFACE", "META", "FONT"}
  270. with GodotExporter(filepath, kwargs, operator) as exp:
  271. exp.export()
  272. return {"FINISHED"}