Last active
February 6, 2019 06:05
-
-
Save tamask/a6b1649a2d3e952bfe53229f93d06eca to your computer and use it in GitHub Desktop.
Autoexport to an FBX target file (Blender 2.80)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import os | |
import bpy | |
import bpy_extras | |
import mathutils as mu | |
from io_scene_fbx import ExportFBX | |
bl_info = { | |
'name': 'Autoexport FBX Target (.fbx)', | |
'author': 'Tamas Kemenczy', | |
'blender': (2, 80, 0), | |
'version': (0, 10), | |
'description': 'Autoexport an FBX file to a specified target path', | |
'category': 'Import-Export', | |
} | |
PRESETS = { | |
'UNITY': { | |
'global_scale': 0.01, | |
'axis_up': 'Y', | |
'axis_forward': '-Z', | |
'use_tspace': False, | |
'use_armature_deform_only': True, | |
'bake_anim_use_all_bones': False, | |
'bake_anim_use_nla_strips': False, | |
'bake_anim_use_all_actions': False, | |
'bake_anim_force_startend_keying': False, | |
} | |
} | |
# awful but necessary workaround because writing to ID objects in | |
# certain contexts is 'not allowed' | |
__LAYOUT__ = None | |
class PseudoOperator: | |
# 2.80 fbx save_single function expects an operator to allow reporting | |
def report(self, *args, **kwargs): | |
pass | |
def filepath_update_fn(self, context): | |
if self.filepath and not self.filepath.endswith('.fbx'): | |
self.filepath += '.fbx' | |
if self.filepath: | |
install_autoexport_handler() | |
else: | |
remove_autoexport_handler(); | |
def autoexport_update_fn(self, context): | |
install_autoexport_handler() | |
def preset_update_fn(self, context): | |
bpy.ops.export_scene.fbx_target_preset() | |
class FbxTargetSettings(bpy.types.PropertyGroup): | |
filepath : bpy.props.StringProperty( | |
name='Filepath', | |
subtype='FILE_PATH', | |
update=filepath_update_fn) | |
autoexport : bpy.props.BoolProperty( | |
name='Autoexport', | |
default=True, | |
update=autoexport_update_fn) | |
preset: bpy.props.EnumProperty( | |
items=( | |
('NONE', 'Blender', 'Use Blender\'s defaults'), | |
('UNITY', 'Unity', 'Common settings for Unity 3D'), | |
), | |
update=preset_update_fn, | |
name='Preset') | |
show_export_options : bpy.props.BoolProperty(name='Show Export Options') | |
@property | |
def layout(self): | |
global __LAYOUT__ | |
return __LAYOUT__ | |
def as_keywords(self, *args, **kwargs): | |
keys = set(self.__annotations__.keys()) | |
keys.difference_update(set(kwargs.get('ignore', tuple()))) | |
keywords = {} | |
for key in list(keys): | |
keywords[key] = getattr(self, key) | |
return keywords | |
def get_defaults(self, *args, **kwargs): | |
keys = set(self.__annotations__.keys()) | |
keys.difference_update(set(kwargs.get('ignore', tuple()))) | |
keywords = {} | |
for key in list(keys): | |
props = self.__annotations__[key][1] | |
if 'default' in props: | |
keywords[key] = props['default'] | |
return keywords | |
FbxTargetSettings.__annotations__.update(ExportFBX.__annotations__) | |
class FbxTargetObject(bpy.types.PropertyGroup): | |
export : bpy.props.BoolProperty( | |
name='Export', default=True, | |
description='Include object in FBX target') | |
PROP_FBX_TARGET_SETTINGS = bpy.props.PointerProperty(type=FbxTargetSettings) | |
PROP_FBX_TARGET_OBJECT = bpy.props.PointerProperty(type=FbxTargetObject) | |
class RENDER_PT_fbx_target(bpy.types.Panel): | |
bl_label = 'FBX Target' | |
bl_space_type = 'PROPERTIES' | |
bl_region_type = 'WINDOW' | |
bl_context = 'output' | |
def draw(self, context): | |
fbx = context.scene.fbx_target | |
layout = self.layout | |
layout.use_property_split = False | |
layout.use_property_decorate = False # No animation. | |
layout.prop(fbx, 'filepath', text='') | |
have_path = len(fbx.filepath) > 0 | |
layout.use_property_split = True | |
flow = layout.grid_flow(row_major=True, columns=0, even_columns=True, even_rows=False, align=False) | |
col = flow.column() | |
col.enabled = have_path | |
col.prop(fbx, 'autoexport', text='Autoexport on save') | |
row = layout.row(align=True) | |
row.enabled = have_path | |
row.prop(fbx, 'preset', text='') | |
row = layout.row() | |
row.enabled = have_path | |
row.operator('export_scene.fbx_target', text='Export Now') | |
if have_path: | |
filepath = get_abs_path(fbx.filepath) | |
dirpath = os.path.dirname(filepath) | |
if not os.path.exists(dirpath): | |
box = layout.box() | |
box.label(text='Path does not exist, it will be created', icon='ERROR') | |
layout.use_property_split = False | |
layout.prop(fbx, 'show_export_options', toggle=True) | |
if fbx.show_export_options: | |
global __LAYOUT__ | |
__LAYOUT__ = self.layout | |
ExportFBX.draw(fbx, context) | |
__LAYOUT__ = None | |
class OBJECT_PT_fbx_target(bpy.types.Panel): | |
bl_label = 'FBX Target' | |
bl_space_type = 'PROPERTIES' | |
bl_region_type = 'WINDOW' | |
bl_context = 'object' | |
def draw(self, context): | |
fbx = context.active_object.fbx_target | |
layout = self.layout | |
layout.use_property_split = False | |
layout.use_property_decorate = False # No animation. | |
layout.use_property_split = True | |
flow = layout.grid_flow(row_major=True, columns=0, even_columns=True, even_rows=False, align=False) | |
col = flow.column() | |
col.prop(fbx, 'export', text='Include') | |
def get_abs_path(filepath): | |
if filepath.startswith('//'): | |
filepath = os.path.join(os.path.dirname(bpy.data.filepath), filepath[2:]) | |
return filepath | |
def save_fbx_target(operator, context, scene): | |
fbx = scene.fbx_target | |
context_objects = [o for o in scene.objects if o.fbx_target.export] | |
if not context_objects: | |
operator.report({'INFO'}, 'Nothing to export') | |
return {'CANCELLED'} | |
global_matrix = bpy_extras.io_utils.axis_conversion( | |
to_forward=fbx.axis_forward, | |
to_up=fbx.axis_up, | |
).to_4x4() | |
keywords = fbx.as_keywords(ignore=( | |
'filepath', 'autoexport', 'show_export_options', 'preset', | |
'batch_mode', 'check_existing', 'filter_glob', 'ui_tab',)) | |
keywords.update({ | |
'global_matrix': global_matrix, | |
'context_objects': context_objects, | |
}) | |
filepath = get_abs_path(fbx.filepath) | |
dirpath = os.path.dirname(filepath) | |
if not os.path.exists(dirpath): | |
os.makedirs(dirpath) | |
from io_scene_fbx import export_fbx_bin | |
return export_fbx_bin.save_single(operator, scene, context.depsgraph, filepath=filepath, **keywords) | |
class EXPORT_SCENE_OT_fbx_target(bpy.types.Operator): | |
'''Export an FBX file of the scene a target path.''' | |
bl_idname = 'export_scene.fbx_target' | |
bl_label = 'Autoexport FBX Target' | |
def execute(self, context): | |
try: | |
ret = save_fbx_target(self, context, context.scene) | |
filepath = get_abs_path(context.scene.fbx_target.filepath) | |
basename = os.path.basename(filepath) | |
self.report({'INFO'}, 'Saved %s' % basename) | |
return ret | |
except FileNotFoundError as e: | |
self.report({'ERROR'}, e.strerror) | |
return {'CANCELLED'} | |
class EXPORT_SCENE_OT_fbx_target_preset(bpy.types.Operator): | |
'''Apply an export preset to the FBX target.''' | |
bl_idname = 'export_scene.fbx_target_preset' | |
bl_label = 'Apply FBX Target Preset' | |
def execute(self, context): | |
fbx = context.scene.fbx_target | |
if fbx.preset == 'NONE': | |
preset = fbx.get_defaults(ignore=( | |
'filepath', 'autoexport', 'show_export_options', 'preset', | |
'batch_mode', 'check_existing', 'filter_glob', 'ui_tab',)) | |
else: | |
preset = PRESETS[fbx.preset] | |
for key, value in preset.items(): | |
setattr(fbx, key, value) | |
return {'FINISHED'} | |
_handler_installed = False | |
def on_save_post(scene): | |
for scene in bpy.data.scenes: | |
if scene.fbx_target.filepath and scene.fbx_target.autoexport: | |
save_fbx_target(PseudoOperator(), bpy.context, scene) | |
def install_autoexport_handler(): | |
global _handler_installed | |
if not _handler_installed: | |
add_handler = False | |
for scene in bpy.data.scenes: | |
if scene.fbx_target.filepath and scene.fbx_target.autoexport: | |
add_handler = True | |
break | |
if add_handler: | |
bpy.app.handlers.save_post.append(on_save_post) | |
_handler_installed = True | |
def remove_autoexport_handler(): | |
global _handler_installed | |
if _handler_installed: | |
bpy.app.handlers.save_post.remove(on_save_post) | |
_handler_installed = False | |
@bpy.app.handlers.persistent | |
def on_load_post(scene): | |
global _handler_installed | |
_handler_installed = False | |
install_autoexport_handler() | |
def register(): | |
for obj in __REGISTER__: | |
if hasattr(obj, 'count'): | |
cls, prop, value = obj | |
setattr(cls, prop, value) | |
else: | |
bpy.utils.register_class(obj) | |
bpy.app.handlers.load_post.append(on_load_post) | |
def unregister(): | |
for obj in __REGISTER__: | |
if hasattr(obj, 'count'): | |
cls, prop, value = obj | |
if hasattr(cls, prop): | |
delattr(cls, prop) | |
else: | |
bpy.utils.unregister_class(obj) | |
bpy.app.handlers.load_post.remove(on_load_post) | |
__REGISTER__ = ( | |
FbxTargetSettings, | |
FbxTargetObject, | |
(bpy.types.Scene, 'fbx_target', PROP_FBX_TARGET_SETTINGS), | |
(bpy.types.Object, 'fbx_target', PROP_FBX_TARGET_OBJECT), | |
RENDER_PT_fbx_target, | |
OBJECT_PT_fbx_target, | |
EXPORT_SCENE_OT_fbx_target, | |
EXPORT_SCENE_OT_fbx_target_preset, | |
) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment