123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327 |
- # ##### BEGIN GPL LICENSE BLOCK #####
- #
- # This program is free software; you can redistribute it and/or
- # modify it under the terms of the GNU General Public License
- # as published by the Free Software Foundation; either version 2
- # of the License, or (at your option) any later version.
- #
- # This program is distributed in the hope that it will be useful,
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- # GNU General Public License for more details.
- #
- # You should have received a copy of the GNU General Public License
- # along with this program. If not, see <http://www.gnu.org/licenses/>.
- #
- # ##### END GPL LICENSE BLOCK #####
- # Script copyright (C) Juan Linietsky
- # Contact Info: [email protected]
- """
- This script is an exporter to Godot Engine
- http://www.godotengine.org
- """
- import os
- import collections
- import functools
- import logging
- import bpy
- from . import structures
- from . import converters
- from .structures import (_AXIS_CORRECT, NodePath)
- logging.basicConfig(level=logging.INFO, format="[%(levelname)s]: %(message)s")
- @functools.lru_cache(maxsize=1) # Cache it so we don't search lots of times
- def find_godot_project_dir(export_path):
- """Finds the project.godot file assuming that the export path
- is inside a project (looks for a project.godot file)"""
- project_dir = export_path
- # Search up until we get to the top, which is "/" in *nix.
- # Standard Windows ends up as, e.g., "C:\", and independent of what else is
- # in the world, we can at least watch for repeats, because that's bad.
- last = None
- while not os.path.isfile(os.path.join(project_dir, "project.godot")):
- project_dir = os.path.split(project_dir)[0]
- if project_dir in ("/", last):
- raise structures.ValidationError(
- "Unable to find Godot project file"
- )
- last = project_dir
- logging.info("Found Godot project directory at %s", project_dir)
- return project_dir
- class ExporterLogHandler(logging.Handler):
- """Custom handler for exporter, would report logging message
- to GUI"""
- def __init__(self, operator):
- super().__init__()
- self.setLevel(logging.WARNING)
- self.setFormatter(logging.Formatter("%(message)s"))
- self.blender_op = operator
- def emit(self, record):
- if record.levelno == logging.WARNING:
- self.blender_op.report({'WARNING'}, record.message)
- else:
- self.blender_op.report({'ERROR'}, record.message)
- class GodotExporter:
- """Handles picking what nodes to export and kicks off the export process"""
- def export_object(self, obj, parent_gd_node):
- """Recursively export a object. It calls the export_object function on
- all of the objects children. If you have heirarchies more than 1000
- objects deep, this will fail with a recursion error"""
- if obj not in self.valid_objects:
- return
- logging.info("Exporting Blender object: %s", obj.name)
- prev_node = bpy.context.view_layer.objects.active
- bpy.context.view_layer.objects.active = obj
- # Figure out what function will perform the export of this object
- if obj.type not in converters.BLENDER_TYPE_TO_EXPORTER:
- logging.warning(
- "Unknown object type. Treating as empty: %s", obj.name
- )
- elif obj in self.exporting_objects:
- exporter = converters.BLENDER_TYPE_TO_EXPORTER[obj.type]
- else:
- logging.warning(
- "Object is parent of exported objects. "
- "Treating as empty: %s", obj.name
- )
- exporter = converters.BLENDER_TYPE_TO_EXPORTER["EMPTY"]
- is_bone_attachment = False
- if ("ARMATURE" in self.config['object_types'] and
- obj.parent and obj.parent_bone != ''):
- is_bone_attachment = True
- parent_gd_node = converters.BONE_ATTACHMENT_EXPORTER(
- self.escn_file,
- self.config,
- obj,
- parent_gd_node
- )
- if ("PARTICLE" in self.config['object_types'] and
- converters.has_particle(obj)):
- converters.MULTIMESH_EXPORTER(
- self.escn_file,
- self.config,
- obj,
- parent_gd_node
- )
- # Perform the export, note that `exported_node.parent` not
- # always the same as `parent_gd_node`, as sometimes, one
- # blender node exported as two parented node
- exported_node = exporter(self.escn_file, self.config, obj,
- parent_gd_node)
- self.bl_object_gd_node_map[obj] = exported_node
- if is_bone_attachment:
- for child in parent_gd_node.children:
- child['transform'] = structures.fix_bone_attachment_transform(
- obj, child['transform']
- )
- # CollisionShape node has different direction in blender
- # and godot, so it has a -90 rotation around X axis,
- # here rotate its children back
- if (hasattr(exported_node, "parent") and
- exported_node.parent.get_type() == 'CollisionShape'):
- exported_node['transform'] = (
- _AXIS_CORRECT.inverted() @
- exported_node['transform'])
- # if the blender node is exported and it has animation data
- if exported_node != parent_gd_node:
- converters.ANIMATION_DATA_EXPORTER(
- self.escn_file,
- self.config,
- exported_node,
- obj,
- "transform"
- )
- for child in obj.children:
- self.export_object(child, exported_node)
- bpy.context.view_layer.objects.active = prev_node
- def should_export_object(self, obj):
- """Checks if a node should be exported:"""
- if obj.type not in self.config["object_types"]:
- return False
- if self.config["use_included_in_render"] and obj.hide_render:
- return False
- if self.config["use_visible_objects"]:
- view_layer = bpy.context.view_layer
- if obj.name not in view_layer.objects:
- return False
- if not obj.visible_get():
- return False
- if self.config["use_export_selected"] and not obj.select_get():
- return False
- return True
- def export_scene(self):
- # pylint: disable-msg=too-many-branches
- """Decide what objects to export, and export them!"""
- logging.info("Exporting scene: %s", self.scene.name)
- in_edit_mode = False
- if bpy.context.object and bpy.context.object.mode == "EDIT":
- in_edit_mode = True
- bpy.ops.object.editmode_toggle()
- # Decide what objects to export
- for obj in self.scene.objects:
- if obj in self.exporting_objects:
- continue
- if self.should_export_object(obj):
- self.exporting_objects.add(obj)
- # Ensure parents of current valid object is
- # going to the exporting recursion
- tmp = obj
- while tmp is not None:
- if tmp not in self.valid_objects:
- self.valid_objects.add(tmp)
- else:
- break
- tmp = tmp.parent
- logging.info("Exporting %d objects", len(self.valid_objects))
- # Scene root
- root_gd_node = structures.NodeTemplate(
- self.scene.name,
- "Spatial",
- None
- )
- self.escn_file.add_node(root_gd_node)
- for obj in self.scene.objects:
- if obj in self.valid_objects and obj.parent is None:
- # recursive exporting on root object
- self.export_object(obj, root_gd_node)
- if "ARMATURE" in self.config['object_types']:
- for bl_obj in self.bl_object_gd_node_map:
- for mod in bl_obj.modifiers:
- if mod.type == "ARMATURE":
- mesh_node = self.bl_object_gd_node_map[bl_obj]
- skeleton_node = self.bl_object_gd_node_map[mod.object]
- mesh_node['skeleton'] = NodePath(
- mesh_node.get_path(), skeleton_node.get_path())
- if in_edit_mode:
- bpy.ops.object.editmode_toggle()
- def load_supported_features(self):
- """According to `project.godot`, determine all new feature supported
- by that godot version"""
- project_dir = ""
- try:
- project_dir = self.config["project_path_func"]()
- except structures.ValidationError:
- project_dir = False
- logging.warning(
- "Not export to Godot project dir, disable all beta features.")
- # minimal supported version
- conf_versiton = 3
- if project_dir:
- project_file_path = os.path.join(project_dir, "project.godot")
- with open(project_file_path, "r") as proj_f:
- for line in proj_f:
- if not line.startswith("config_version"):
- continue
- _, version_str = tuple(line.split("="))
- conf_versiton = int(version_str)
- break
- if conf_versiton < 2:
- logging.error(
- "Godot version smaller than 3.0, not supported by this addon")
- if conf_versiton >= 4:
- # godot >=3.1
- self.config["feature_bezier_track"] = True
- def export(self):
- """Begin the export"""
- self.escn_file = structures.ESCNFile(structures.FileEntry(
- "gd_scene",
- collections.OrderedDict((
- ("load_steps", 1),
- ("format", 2)
- ))
- ))
- self.export_scene()
- self.escn_file.fix_paths(self.config)
- with open(self.path, 'w') as out_file:
- out_file.write(self.escn_file.to_string())
- return True
- def __init__(self, path, kwargs, operator):
- self.path = path
- self.operator = operator
- self.scene = bpy.context.scene
- self.config = kwargs
- self.config["path"] = path
- self.config["project_path_func"] = functools.partial(
- find_godot_project_dir, path
- )
- # valid object would contain object should be exported
- # and their parents to retain the hierarchy
- self.valid_objects = set()
- # exporting objects would only contain objects need
- # to be exported
- self.exporting_objects = set()
- # optional features
- self.config["feature_bezier_track"] = False
- if self.config["use_beta_features"]:
- self.load_supported_features()
- self.escn_file = None
- self.bl_object_gd_node_map = {}
- def __enter__(self):
- return self
- def __exit__(self, *exc):
- pass
- def save(operator, context, filepath="", **kwargs):
- """Begin the export"""
- object_types = kwargs["object_types"]
- # GEOMETRY isn't an object type so replace it with all valid geometry based
- # object types
- if "GEOMETRY" in object_types:
- object_types.remove("GEOMETRY")
- object_types |= {"MESH", "CURVE", "SURFACE", "META", "FONT"}
- with GodotExporter(filepath, kwargs, operator) as exp:
- exp.export()
- return {"FINISHED"}
|