export_godot.py 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  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 math
  28. import bpy
  29. import mathutils
  30. from . import structures
  31. from . import converters
  32. logging.basicConfig(level=logging.INFO, format="[%(levelname)s]: %(message)s")
  33. @functools.lru_cache(maxsize=1) # Cache it so we don't search lots of times
  34. def find_godot_project_dir(export_path):
  35. """Finds the project.godot file assuming that the export path
  36. is inside a project (looks for a project.godot file)"""
  37. project_dir = export_path
  38. # Search up until we get to the top, which is "/" in *nix.
  39. # Standard Windows ends up as, e.g., "C:\", and independent of what else is
  40. # in the world, we can at least watch for repeats, because that's bad.
  41. last = None
  42. while not os.path.isfile(os.path.join(project_dir, "project.godot")):
  43. project_dir = os.path.split(project_dir)[0]
  44. if project_dir in ("/", last):
  45. raise structures.ValidationError(
  46. "Unable to find godot project file"
  47. )
  48. last = project_dir
  49. logging.info("Found godot project directory at %s", project_dir)
  50. return project_dir
  51. class ExporterLogHandler(logging.Handler):
  52. """Custom handler for exporter, would report logging message
  53. to GUI"""
  54. def __init__(self, operator):
  55. super().__init__()
  56. self.setLevel(logging.WARNING)
  57. self.setFormatter(logging.Formatter("%(message)s"))
  58. self.blender_op = operator
  59. def emit(self, record):
  60. if record.levelno == logging.WARNING:
  61. self.blender_op.report({'WARNING'}, record.message)
  62. else:
  63. self.blender_op.report({'ERROR'}, record.message)
  64. class GodotExporter:
  65. """Handles picking what nodes to export and kicks off the export process"""
  66. def export_object(self, obj, parent_gd_node):
  67. """Recursively export a object. It calls the export_object function on
  68. all of the objects children. If you have heirarchies more than 1000
  69. objects deep, this will fail with a recursion error"""
  70. if obj not in self.valid_objects:
  71. return
  72. logging.info("Exporting Blender Object: %s", obj.name)
  73. prev_node = bpy.context.view_layer.objects.active
  74. bpy.context.view_layer.objects.active = obj
  75. # Figure out what function will perform the export of this object
  76. if (obj.type in converters.BLENDER_TYPE_TO_EXPORTER and
  77. obj in self.exporting_objects):
  78. exporter = converters.BLENDER_TYPE_TO_EXPORTER[obj.type]
  79. else:
  80. logging.warning(
  81. "Unknown object type. Treating as empty: %s", obj.name
  82. )
  83. exporter = converters.BLENDER_TYPE_TO_EXPORTER["EMPTY"]
  84. is_bone_attachment = False
  85. if ("ARMATURE" in self.config['object_types'] and
  86. obj.parent_bone != ''):
  87. is_bone_attachment = True
  88. parent_gd_node = converters.BONE_ATTACHMENT_EXPORTER(
  89. self.escn_file,
  90. obj,
  91. parent_gd_node
  92. )
  93. # Perform the export, note that `exported_node.parent` not
  94. # always the same as `parent_gd_node`, as sometimes, one
  95. # blender node exported as two parented node
  96. exported_node = exporter(self.escn_file, self.config, obj,
  97. parent_gd_node)
  98. if is_bone_attachment:
  99. for child in parent_gd_node.children:
  100. child['transform'] = structures.fix_bone_attachment_transform(
  101. obj, child['transform']
  102. )
  103. # CollisionShape node has different direction in blender
  104. # and godot, so it has a -90 rotation around X axis,
  105. # here rotate its children back
  106. if (exported_node.parent is not None and
  107. exported_node.parent.get_type() == 'CollisionShape'):
  108. exported_node['transform'] = (
  109. mathutils.Matrix.Rotation(math.radians(90), 4, 'X') @
  110. exported_node['transform'])
  111. # if the blender node is exported and it has animation data
  112. if exported_node != parent_gd_node:
  113. converters.ANIMATION_DATA_EXPORTER(
  114. self.escn_file,
  115. self.config,
  116. exported_node,
  117. obj,
  118. "transform"
  119. )
  120. for child in obj.children:
  121. self.export_object(child, exported_node)
  122. bpy.context.view_layer.objects.active = prev_node
  123. def should_export_object(self, obj):
  124. """Checks if a node should be exported:"""
  125. if obj.type not in self.config["object_types"]:
  126. return False
  127. if self.config["use_visible_objects"]:
  128. view_layer = bpy.context.view_layer
  129. if obj.name not in view_layer.objects:
  130. return False
  131. if not obj.visible_get():
  132. return False
  133. if self.config["use_export_selected"] and not obj.select:
  134. return False
  135. self.exporting_objects.add(obj)
  136. return True
  137. def export_scene(self):
  138. """Decide what objects to export, and export them!"""
  139. logging.info("Exporting scene: %s", self.scene.name)
  140. # Decide what objects to export
  141. for obj in self.scene.objects:
  142. if obj in self.valid_objects:
  143. continue
  144. if self.should_export_object(obj):
  145. # Ensure parents of current valid object is
  146. # going to the exporting recursion
  147. tmp = obj
  148. while tmp is not None:
  149. if tmp not in self.valid_objects:
  150. self.valid_objects.add(tmp)
  151. else:
  152. break
  153. tmp = tmp.parent
  154. logging.info("Exporting %d objects", len(self.valid_objects))
  155. # Scene root
  156. root_gd_node = structures.NodeTemplate(
  157. self.scene.name,
  158. "Spatial",
  159. None
  160. )
  161. self.escn_file.add_node(root_gd_node)
  162. for obj in self.scene.objects:
  163. if obj in self.valid_objects and obj.parent is None:
  164. # recursive exporting on root object
  165. self.export_object(obj, root_gd_node)
  166. def export(self):
  167. """Begin the export"""
  168. self.escn_file = structures.ESCNFile(structures.FileEntry(
  169. "gd_scene",
  170. collections.OrderedDict((
  171. ("load_steps", 1),
  172. ("format", 2)
  173. ))
  174. ))
  175. self.export_scene()
  176. self.escn_file.fix_paths(self.config)
  177. with open(self.path, 'w') as out_file:
  178. out_file.write(self.escn_file.to_string())
  179. return True
  180. def __init__(self, path, kwargs, operator):
  181. self.path = path
  182. self.operator = operator
  183. self.scene = bpy.context.scene
  184. self.config = kwargs
  185. self.config["path"] = path
  186. self.config["project_path_func"] = functools.partial(
  187. find_godot_project_dir, path
  188. )
  189. # valid object would contain object should be exported
  190. # and their parents to retain the hierarchy
  191. self.valid_objects = set()
  192. self.exporting_objects = set()
  193. self.escn_file = None
  194. def __enter__(self):
  195. return self
  196. def __exit__(self, *exc):
  197. pass
  198. def save(operator, context, filepath="", **kwargs):
  199. """Begin the export"""
  200. exporter_log_handler = ExporterLogHandler(operator)
  201. logging.getLogger().addHandler(exporter_log_handler)
  202. with GodotExporter(filepath, kwargs, operator) as exp:
  203. exp.export()
  204. logging.getLogger().removeHandler(exporter_log_handler)
  205. return {"FINISHED"}