diff --git a/src/addons/send2ue/core/io/fbx_b3.py b/src/addons/send2ue/core/io/fbx_b3.py index b1ce3c7a..8f88dbb9 100644 --- a/src/addons/send2ue/core/io/fbx_b3.py +++ b/src/addons/send2ue/core/io/fbx_b3.py @@ -348,7 +348,10 @@ def fbx_data_object_elements(root, ob_obj, scene_data): obj_type = b"Camera" if ob_obj.type == 'ARMATURE': - if bpy.context.scene.send2ue.export_object_name_as_root: + if bpy.context.scene.send2ue.export_custom_root_name: + # if the user has provided a custom name for a root bone, use this directly + ob_obj.name = bpy.context.scene.send2ue.export_custom_root_name + elif bpy.context.scene.send2ue.export_object_name_as_root: # if the object is already named armature this forces the object name to root if 'armature' == ob_obj.name.lower(): ob_obj.name = 'root' diff --git a/src/addons/send2ue/core/io/fbx_b4.py b/src/addons/send2ue/core/io/fbx_b4.py index 05d97ab2..ecca5a86 100644 --- a/src/addons/send2ue/core/io/fbx_b4.py +++ b/src/addons/send2ue/core/io/fbx_b4.py @@ -434,7 +434,10 @@ def fbx_data_object_elements(root, ob_obj, scene_data): obj_type = b"Camera" if ob_obj.type == 'ARMATURE': - if bpy.context.scene.send2ue.export_object_name_as_root: + if bpy.context.scene.send2ue.export_custom_root_name: + # if the user has provided a custom name for a root bone, use this directly + ob_obj.name = bpy.context.scene.send2ue.export_custom_root_name + elif bpy.context.scene.send2ue.export_object_name_as_root: # if the object is already named armature this forces the object name to root if 'armature' == ob_obj.name.lower(): ob_obj.name = 'root' diff --git a/src/addons/send2ue/properties.py b/src/addons/send2ue/properties.py index 696238f9..fd915808 100644 --- a/src/addons/send2ue/properties.py +++ b/src/addons/send2ue/properties.py @@ -286,6 +286,14 @@ class Send2UeSceneProperties(property_class): "the first bone in the armature hierarchy is used as the root bone in unreal." ) ) + export_custom_root_name: bpy.props.StringProperty( + name="Custom root bone name", + default="", + description=( + "If specified, this adds a root bone by this name in Unreal. This overrides the " + "\"Export object name as root bone\" setting." + ) + ) export_custom_property_fcurves: bpy.props.BoolProperty( name="Export custom property fcurves", default=True, diff --git a/src/addons/send2ue/ui/dialog.py b/src/addons/send2ue/ui/dialog.py index 126e0556..2ab6a2ea 100644 --- a/src/addons/send2ue/ui/dialog.py +++ b/src/addons/send2ue/ui/dialog.py @@ -63,6 +63,7 @@ def draw_export_tab(self, layout): properties = bpy.context.scene.send2ue self.draw_property(properties, layout, 'use_object_origin') self.draw_property(properties, layout, 'export_object_name_as_root') + self.draw_property(properties, layout, 'export_custom_root_name', enabled=not properties.export_object_name_as_root) # animation settings box self.draw_expanding_section( diff --git a/tests/test_send2ue_cubes.py b/tests/test_send2ue_cubes.py index f6a10bdc..1b71eb96 100644 --- a/tests/test_send2ue_cubes.py +++ b/tests/test_send2ue_cubes.py @@ -23,6 +23,10 @@ def test_auto_stash_active_action_option(self): def test_export_object_name_as_root_option(self): pass + @unittest.skip + def test_custom_root_bone_name(self): + pass + @unittest.skip def test_export_custom_property_fcurves_option(self): pass diff --git a/tests/test_send2ue_mannequins.py b/tests/test_send2ue_mannequins.py index 80731c78..8a8ad504 100644 --- a/tests/test_send2ue_mannequins.py +++ b/tests/test_send2ue_mannequins.py @@ -199,6 +199,19 @@ def test_export_object_name_as_root_option(self): 'frames': [2, 6, 11] }}) + def test_custom_root_bone_name(self): + """ + Tests custom root bone name option. + """ + self.run_custom_root_bone_name_option_tests({ + 'SK_Mannequin_Female': { + 'rig': 'female_root', + 'animations': ['third_person_walk_01', 'third_person_run_01'], + 'bones': ['spine_02', 'calf_l', 'lowerarm_r'], + 'frames': [2, 6, 11], + 'custom_name': 'my_test_root_bone', + }}) + def test_export_custom_property_fcurves_option(self): """ Tests export custom property fcurves option. diff --git a/tests/utils/base_test_case.py b/tests/utils/base_test_case.py index 3f7fc981..5968b00f 100644 --- a/tests/utils/base_test_case.py +++ b/tests/utils/base_test_case.py @@ -691,7 +691,7 @@ def assert_curve(self, animation_name, curve_name, exists=True): else: self.assertFalse(result, f'Curve "{curve_name}" exists on animation "{animation_name}" when it should not!') - def assert_animation_hierarchy(self, rig_name, animation_name, bone_name, include_object=True): + def assert_animation_hierarchy(self, rig_name, animation_name, bone_name, include_object=True, custom_root_name=None): self.log( f'Checking the bone hierarchy of "{animation_name}" to see if "{bone_name}" has the same path ' f'to the root bone...' @@ -702,6 +702,9 @@ def assert_animation_hierarchy(self, rig_name, animation_name, bone_name, includ unreal_bone_path = self.unreal.get_bone_path_to_root(asset_path, bone_name) blender_bone_path = self.blender.get_bone_path_to_root(rig_name, bone_name, include_object) + if custom_root_name: + blender_bone_path.append(custom_root_name) + self.assertEqual( collections.Counter(blender_bone_path), collections.Counter(unreal_bone_path), @@ -1084,6 +1087,29 @@ def run_export_object_name_as_root_option_tests(self, objects_and_animations): for frame in frames: self.assert_animation_translation(rig_name, animation_name, bone_name, frame) + def run_custom_root_bone_name_option_tests(self, objects_and_animations): + self.blender.set_addon_property('scene', 'send2ue', 'export_all_actions', True) + self.blender.set_addon_property('scene', 'send2ue', 'import_animations', True) + # This option should be ignored as we're setting a custom name below. + self.blender.set_addon_property('scene', 'send2ue', 'export_object_name_as_root', True) + + for object_name, data in objects_and_animations.items(): + rig_name = data.get('rig') + animation_names = data.get('animations') + bone_names = data.get('bones') + frames = data.get('frames') + custom_root_bone_name = data.get('custom_name') + self.blender.set_addon_property('scene', 'send2ue', 'export_custom_root_name', custom_root_bone_name) + self.move_to_collection([object_name, rig_name], 'Export') + self.send2ue_operation() + self.assert_mesh_import(object_name) + # check that the animations are as expected + for animation_name in animation_names: + for bone_name in bone_names: + self.assert_animation_hierarchy(rig_name, animation_name, bone_name, include_object=False, custom_root_name=custom_root_bone_name) + for frame in frames: + self.assert_animation_translation(rig_name, animation_name, bone_name, frame) + def run_export_custom_property_fcurves_option_tests(self, objects_and_animations): self.blender.set_addon_property('scene', 'send2ue', 'export_all_actions', True) self.blender.set_addon_property('scene', 'send2ue', 'import_animations', True) @@ -1168,6 +1194,9 @@ def test_export_object_name_as_root_option(self): """ raise NotImplementedError('This test case must be implemented or skipped') + def test_custom_root_bone_name(self): + raise NotImplementedError('This test case must be implemented or skipped') + def test_export_custom_property_fcurves_option(self): """ Tests export custom property fcurves option. @@ -1235,6 +1264,10 @@ def test_auto_stash_active_action_option(self): def test_export_object_name_as_root_option(self): pass + @unittest.skip + def test_custom_root_bone_name(self): + pass + @unittest.skip def test_use_object_origin_option(self): pass