Browse Source

Merge pull request #11885 from aws-lumberyard-dev/codex-shawstar

Codex shawstar
Jonny Galloway 3 years ago
parent
commit
dcd61f1b64

+ 16 - 6
Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/AddOns/SceneExporter/__init__.py

@@ -36,7 +36,7 @@
 bl_info = {
 bl_info = {
     "name": "O3DE_DCCSI_BLENDER_SCENE_EXPORTER",
     "name": "O3DE_DCCSI_BLENDER_SCENE_EXPORTER",
     "author": "shawstar@amazon",
     "author": "shawstar@amazon",
-    "version": (1, 4),
+    "version": (1, 5),
     "blender": (3, 00, 0),
     "blender": (3, 00, 0),
     "location": "",
     "location": "",
     "description": "Export Scene Assets to O3DE",
     "description": "Export Scene Assets to O3DE",
@@ -59,17 +59,19 @@ from . import constants
 
 
 
 
 def register():
 def register():
-    """! This is the function that will register Classes and Global Vars for this plugin
+    """! 
+    This is the function that will register Classes and Global Vars for this plugin
     """
     """
     bpy.types.Scene.plugin_directory = str(directory)
     bpy.types.Scene.plugin_directory = str(directory)
     bpy.utils.register_class(ui.O3deTools)
     bpy.utils.register_class(ui.O3deTools)
     bpy.utils.register_class(ui.MessageBox)
     bpy.utils.register_class(ui.MessageBox)
     bpy.utils.register_class(ui.MessageBoxConfirm)
     bpy.utils.register_class(ui.MessageBoxConfirm)
+    bpy.utils.register_class(ui.ReportCard)
+    bpy.utils.register_class(ui.ReportCardButton)
     bpy.utils.register_class(ui.WikiButton)
     bpy.utils.register_class(ui.WikiButton)
     bpy.utils.register_class(ui.CustomProjectPath)
     bpy.utils.register_class(ui.CustomProjectPath)
     bpy.utils.register_class(ui.AddColliderMesh)
     bpy.utils.register_class(ui.AddColliderMesh)
     bpy.utils.register_class(ui.AddLODMesh)
     bpy.utils.register_class(ui.AddLODMesh)
-    bpy.utils.register_class(ui.ExportFiles)
     bpy.utils.register_class(ui.ProjectsListDropDown)
     bpy.utils.register_class(ui.ProjectsListDropDown)
     bpy.utils.register_class(ui.SceneExporterFileMenu)
     bpy.utils.register_class(ui.SceneExporterFileMenu)
     bpy.utils.register_class(ui.ExportOptionsListDropDown)
     bpy.utils.register_class(ui.ExportOptionsListDropDown)
@@ -85,6 +87,7 @@ def register():
     bpy.types.Scene.pop_up_confirm_label = ''
     bpy.types.Scene.pop_up_confirm_label = ''
     bpy.types.Scene.pop_up_question_label = ''
     bpy.types.Scene.pop_up_question_label = ''
     bpy.types.Scene.pop_up_question_bool = False
     bpy.types.Scene.pop_up_question_bool = False
+    bpy.types.Scene.pop_up_type = ''
     bpy.types.Scene.udp_type = ''
     bpy.types.Scene.udp_type = ''
     bpy.types.Scene.export_textures_folder = True
     bpy.types.Scene.export_textures_folder = True
     bpy.types.Scene.animation_export = constants.NO_ANIMATION
     bpy.types.Scene.animation_export = constants.NO_ANIMATION
@@ -96,7 +99,10 @@ def register():
     bpy.types.Scene.texture_options_list = EnumProperty(items=constants.EXPORT_LIST_OPTIONS, name='', default='0')
     bpy.types.Scene.texture_options_list = EnumProperty(items=constants.EXPORT_LIST_OPTIONS, name='', default='0')
     bpy.types.Scene.animation_options_list = EnumProperty(items=constants.ANIMATION_LIST_OPTIONS, name='', default='0')
     bpy.types.Scene.animation_options_list = EnumProperty(items=constants.ANIMATION_LIST_OPTIONS, name='', default='0')
     bpy.types.WindowManager.multi_file_export_toggle = bpy.props.BoolProperty()
     bpy.types.WindowManager.multi_file_export_toggle = bpy.props.BoolProperty()
-
+    bpy.types.WindowManager.mesh_triangle_export_toggle = bpy.props.BoolProperty()
+    bpy.types.Scene.export_option_gui = False
+    bpy.types.Scene.convert_mesh_to_triangles = True
+    
 def unregister():
 def unregister():
     """! This is the function that will unregister Classes and Global Vars for this plugin
     """! This is the function that will unregister Classes and Global Vars for this plugin
     """
     """
@@ -104,11 +110,12 @@ def unregister():
     bpy.utils.unregister_class(ui.O3deTools)
     bpy.utils.unregister_class(ui.O3deTools)
     bpy.utils.unregister_class(ui.MessageBox)
     bpy.utils.unregister_class(ui.MessageBox)
     bpy.utils.unregister_class(ui.MessageBoxConfirm)
     bpy.utils.unregister_class(ui.MessageBoxConfirm)
+    bpy.utils.unregister_class(ui.ReportCard)
+    bpy.utils.unregister_class(ui.ReportCardButton)
     bpy.utils.unregister_class(ui.WikiButton)
     bpy.utils.unregister_class(ui.WikiButton)
     bpy.utils.unregister_class(ui.CustomProjectPath)
     bpy.utils.unregister_class(ui.CustomProjectPath)
     bpy.utils.unregister_class(ui.AddColliderMesh)
     bpy.utils.unregister_class(ui.AddColliderMesh)
     bpy.utils.unregister_class(ui.AddLODMesh)
     bpy.utils.unregister_class(ui.AddLODMesh)
-    bpy.utils.unregister_class(ui.ExportFiles)
     bpy.utils.unregister_class(ui.ProjectsListDropDown)
     bpy.utils.unregister_class(ui.ProjectsListDropDown)
     bpy.utils.unregister_class(ui.SceneExporterFileMenu)
     bpy.utils.unregister_class(ui.SceneExporterFileMenu)
     bpy.utils.unregister_class(ui.ExportOptionsListDropDown)
     bpy.utils.unregister_class(ui.ExportOptionsListDropDown)
@@ -119,6 +126,7 @@ def unregister():
     del bpy.types.Scene.pop_up_confirm_label
     del bpy.types.Scene.pop_up_confirm_label
     del bpy.types.Scene.pop_up_question_label
     del bpy.types.Scene.pop_up_question_label
     del bpy.types.Scene.pop_up_question_bool
     del bpy.types.Scene.pop_up_question_bool
+    del bpy.types.Scene.pop_up_type
     del bpy.types.Scene.udp_type
     del bpy.types.Scene.udp_type
     del bpy.types.Scene.selected_o3de_project_path
     del bpy.types.Scene.selected_o3de_project_path
     del bpy.types.Scene.o3de_projects_list
     del bpy.types.Scene.o3de_projects_list
@@ -131,7 +139,9 @@ def unregister():
     del bpy.types.Scene.file_menu_animation_export
     del bpy.types.Scene.file_menu_animation_export
     del bpy.types.Scene.stored_image_source_paths
     del bpy.types.Scene.stored_image_source_paths
     del bpy.types.WindowManager.multi_file_export_toggle
     del bpy.types.WindowManager.multi_file_export_toggle
-
+    del bpy.types.Scene.export_option_gui
+    del bpy.types.WindowManager.mesh_triangle_export_toggle
+    del bpy.types.Scene.convert_mesh_to_triangles
 
 
 if __name__ == "__main__":
 if __name__ == "__main__":
     register()
     register()

+ 1 - 1
Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/AddOns/SceneExporter/constants.py

@@ -34,4 +34,4 @@ IMAGE_EXT = ('', '.jpg', '.png', '.JPG', '.PNG')
 USER_HOME = Path.home()
 USER_HOME = Path.home()
 DEFAULT_SDK_MANIFEST_PATH = Path.home().joinpath(f'{TAG_O3DE}','o3de_manifest.json')
 DEFAULT_SDK_MANIFEST_PATH = Path.home().joinpath(f'{TAG_O3DE}','o3de_manifest.json')
 WIKI_URL = 'https://github.com/o3de/o3de/blob/development/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/AddOns/SceneExporter/README.md'
 WIKI_URL = 'https://github.com/o3de/o3de/blob/development/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/AddOns/SceneExporter/README.md'
-PLUGIN_VERSION = '1.4'
+PLUGIN_VERSION = '1.5'

+ 9 - 4
Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/AddOns/SceneExporter/fbx_exporter.py

@@ -119,9 +119,12 @@ def fbx_file_exporter(fbx_file_path, file_name):
             file_menu_export = True
             file_menu_export = True
             if not bpy.types.Scene.export_textures_folder is None:
             if not bpy.types.Scene.export_textures_folder is None:
                 utils.clone_repath_images(file_menu_export, source_file_path, o3de_utils.build_projects_list())
                 utils.clone_repath_images(file_menu_export, source_file_path, o3de_utils.build_projects_list())
-
+                # Currently the Blender FBX Export API is not support use_triangles, we will need to use a modifier at export then remove when done.
+        if bpy.types.Scene.convert_mesh_to_triangles:
+            utils.add_remove_modifier("TRIANGULATE", True)
+        # Lets get the Animation Options
         bake_anim_option, bake_anim_use_all_bones, bake_anim_use_nla_strips_option, bake_anim_use_all_actions_option, bake_anim_force_startend_keying_option = amimation_export_options()
         bake_anim_option, bake_anim_use_all_bones, bake_anim_use_nla_strips_option, bake_anim_use_all_actions_option, bake_anim_force_startend_keying_option = amimation_export_options()
-
+        # Main Blender FBX API exporter
         bpy.ops.export_scene.fbx(
         bpy.ops.export_scene.fbx(
             filepath=str(export_file_path),
             filepath=str(export_file_path),
             check_existing=False,
             check_existing=False,
@@ -161,9 +164,11 @@ def fbx_file_exporter(fbx_file_path, file_name):
             axis_forward='-Z',
             axis_forward='-Z',
             axis_up='Y')
             axis_up='Y')
         
         
-        transforms_status = utils.check_selected_transforms()
+        # If we added a Triangulate modifier, lets remove it now.
+        if bpy.types.Scene.convert_mesh_to_triangles:
+            utils.add_remove_modifier("Triangulate", False)
         # Show export status
         # Show export status
-        bpy.types.Scene.pop_up_notes = f'{file_name} Exported! Freeze Transforms: {transforms_status}'
+        bpy.types.Scene.pop_up_notes = f'{file_name} Exported!'
         bpy.ops.message.popup('INVOKE_DEFAULT')
         bpy.ops.message.popup('INVOKE_DEFAULT')
         if not bpy.types.Scene.export_textures_folder is None:
         if not bpy.types.Scene.export_textures_folder is None:
             utils.replace_stored_paths()
             utils.replace_stored_paths()

+ 254 - 99
Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/AddOns/SceneExporter/ui.py

@@ -8,6 +8,7 @@
 #
 #
 #
 #
 # -------------------------------------------------------------------------
 # -------------------------------------------------------------------------
+from multiprocessing import context
 import bpy
 import bpy
 from pathlib import Path
 from pathlib import Path
 import webbrowser
 import webbrowser
@@ -90,11 +91,176 @@ class MessageBoxConfirm(bpy.types.Operator):
     def execute(self, context):
     def execute(self, context):
         """!
         """!
         This will update the UI with the current o3de Project Title.
         This will update the UI with the current o3de Project Title.
+        There also can be special types of functions that can be called in this execute method.
+        Example: pop_up_type == "UDP"
         """
         """
-        utils.create_udp()
+        if bpy.types.Scene.pop_up_type == "UDP":
+            utils.create_udp()
+
         self.report({'INFO'}, "OKAY")
         self.report({'INFO'}, "OKAY")
         return {'FINISHED'}
         return {'FINISHED'}
 
 
+class ReportCard(bpy.types.Operator):
+    """!
+    This Class is for the UI Report Card Pop-Up.
+    """
+    bl_idname = "report_card.popup"
+    bl_label = "O3DE Scene Exporter"
+    bl_options = {'REGISTER', 'INTERNAL'}
+    
+    @classmethod
+    def poll(cls, context):
+        return True
+
+    def invoke(self, context, event):
+        return context.window_manager.invoke_props_dialog(self)
+
+    def draw(self, context):
+        layout = self.layout
+        col = layout.column()
+
+        # Check seleted Status
+        name_selections = [obj.name for obj in bpy.context.selected_objects]
+        # Check Transforms Status
+        transforms_status, world_location = utils.check_selected_transforms()
+        # Check Selected UVs
+        selected_uvs_check = utils.check_selected_uvs()
+        # Validate selection bone names if any
+        invalid_bone_names, bone_are_in_selected = utils.check_selected_bone_names()
+        # Check to see which Animation Action is active
+        action_name, action_count = utils.check_for_animation_actions()
+        # Check the texture export option
+        if bpy.types.Scene.export_textures_folder:
+            texture_option = "Textures in texture folder."
+        elif not bpy.types.Scene.export_textures_folder:
+            texture_option = "Textures with Model(s)"
+        else:
+            texture_option = "No texture export."
+        # Check for UDPs on selected
+        lods, colliders = utils.check_for_udp()
+
+        # Get Selected Stats
+        vert_count, edge_count, polygon_count, material_count = utils.check_selected_stats_count()
+        # Get filename
+        file_name = re.sub(r'\W+', '', bpy.context.scene.export_file_name_o3de)
+
+        # Start GUI Layout
+        issues = col.box()
+        issues.box()
+        issues.label(text="PREFLIGHT REPORT CARD", icon="COLLECTION_COLOR_04")
+        issues.label(text=f"Issues Found:")
+
+        self.reported_issues_int = 0
+
+        if not transforms_status:
+            issues.alert = True
+            issues.label(text=f"Warning, Your Selected Object(s)" , icon="SEQUENCE_COLOR_03")
+            issues.label(text=f"Transforms have not been Applied (Frozen).")
+            self.reported_issues_int += 1
+        if not world_location == [0.0, 0.0, 0.0]:
+            issues.alert = True
+            issues.label(text=f"Warning, Your Selected Object(s)" , icon="SEQUENCE_COLOR_03")
+            issues.label(text=f"Are not at world orgin {world_location}")
+            self.reported_issues_int += 1
+        if not selected_uvs_check:
+            issues.alert = True
+            issues.label(text=f"Warning, Your Selected Object(s)" , icon="SEQUENCE_COLOR_03")
+            issues.label(text=f"Are missing UV's")
+            self.reported_issues_int += 1
+        if invalid_bone_names:
+            issues.alert = True
+            issues.label(text=f"Warning, Your Selected Object(s)", icon="SEQUENCE_COLOR_03")
+            issues.label(text=f"Have Invalid Bone Names for O3DE.")
+            issues.label(text=f"Please do not use these types of Characters")
+            issues.label(text=f"in Bone names <>[\]~.`")
+            self.reported_issues_int += 1
+        if not bpy.context.scene.unit_settings.length_unit == 'METERS':
+            issues.alert = True
+            issues.label(text=f"Warning, Your Scene is set to", icon="SEQUENCE_COLOR_03")
+            issues.label(text=f"{bpy.context.scene.unit_settings.length_unit}, however, O3DE units are in Meters.")
+            self.reported_issues_int += 1
+        issues.label(text=f"({self.reported_issues_int}) Issues Found.")
+        if self.reported_issues_int == 0:
+            issues.alert = False
+            issues.label(text="No issues found.", icon="SEQUENCE_COLOR_04")
+
+        # General information
+        mesh_info = col.box()
+        mesh_info.box()
+        mesh_info.label(text="EXPORT FILE INFORMATION", icon="COLLECTION_COLOR_05")
+        mesh_info.label(text=f"Issues Found:")
+        mesh_info.label(text=f"Selected({len(name_selections)}): {name_selections}")
+        mesh_info.label(text=f"File Name: {file_name}")
+        mesh_info.label(text=f"Vert Count: {vert_count}")
+        mesh_info.label(text=f"Edge Count: {edge_count}")
+        mesh_info.label(text=f"Polygon Count: {polygon_count}")
+        mesh_info.label(text=f"Material Count: {material_count}")
+        mesh_info.label(text=f"Scene Units are in: {bpy.context.scene.unit_settings.length_unit}")
+        mesh_info.label(text=f"Transforms are applied: {transforms_status}")
+        mesh_info.label(text=f"Export as Triangles: {bpy.types.Scene.convert_mesh_to_triangles}")
+        mesh_info.label(text=f"Animation Options: {bpy.types.Scene.animation_export}")
+        mesh_info.label(text=f"Animation Action({action_count}) {action_name}")
+        mesh_info.label(text=f"Texture Options: {texture_option}")
+        mesh_info.label(text=f"Physics Collider: {colliders}")
+        mesh_info.label(text=f"LODS: {lods}")
+        mesh_info.label(text=f"Export to Path: {bpy.types.Scene.selected_o3de_project_path}")
+    
+    def export_files(self, file_name):
+        """!
+        This function will export the selected as an .fbx to the current project path.
+        @param file_name is the file name selected or string in export_file_name_o3de
+        """
+        # Add file ext
+        file_name = f'{file_name}.fbx'
+        fbx_exporter.fbx_file_exporter('', file_name)
+    
+    def execute(self, context):
+        """!
+        This function will check the current selected count and multi_file_export_o3de bool is True or False.
+        If multi_file_export_o3de is True it will export each selected object as an .fbx file, if False it will
+        export all objects selected as one .fbx.
+        @param context defualt for this blender class
+        """
+        # Validate a selection
+        valid_selection, selected_name = utils.check_selected()
+        # Check if there are multi selections
+        if len(selected_name) > 1:
+            if bpy.types.Scene.multi_file_export_o3de:
+                for obj_name in selected_name:
+                    # Deselect all or will just keep adding to selection
+                    bpy.ops.object.select_all(action='DESELECT')
+                    # Select a mesh in the loop
+                    bpy.data.objects[obj_name].select_set(True)
+                    # Remove some nasty invalid char
+                    file_name = re.sub(r'\W+', '', obj_name)
+                    # Export file
+                    self.export_files(file_name)
+            else:
+                # Remove some nasty invalid char
+                file_name = re.sub(r'\W+', '', bpy.context.scene.export_file_name_o3de)
+                # Export file
+                self.export_files(file_name)
+        else:
+            # Remove some nasty invalid char
+            file_name = re.sub(r'\W+', '', bpy.context.scene.export_file_name_o3de)
+            # Export file
+            self.export_files(file_name)
+        return{'FINISHED'}
+
+class ReportCardButton(bpy.types.Operator):
+    """!
+    This Class is for the UI Report Card Button
+    """
+    bl_idname = "vin.report_card_button"
+    bl_label = "O3DE Report Card Button"
+
+    def execute(self, context):
+        """!
+        This function will open report card window.
+        """
+        bpy.ops.report_card.popup('INVOKE_DEFAULT')
+        return{'FINISHED'}
+
 class WikiButton(bpy.types.Operator):
 class WikiButton(bpy.types.Operator):
     """!
     """!
     This Class is for the UI Wiki Button
     This Class is for the UI Wiki Button
@@ -108,6 +274,7 @@ class WikiButton(bpy.types.Operator):
         """
         """
         webbrowser.open(constants.WIKI_URL)
         webbrowser.open(constants.WIKI_URL)
         return{'FINISHED'}
         return{'FINISHED'}
+
 class CustomProjectPath(bpy.types.Operator, ImportHelper):
 class CustomProjectPath(bpy.types.Operator, ImportHelper):
     """!
     """!
     This Class is for setting a custom project path
     This Class is for setting a custom project path
@@ -153,6 +320,7 @@ class AddColliderMesh(bpy.types.Operator):
         bpy.types.Scene.pop_up_confirm_label = 'Adding UDP Type: o3de_atom_phys to mesh.'
         bpy.types.Scene.pop_up_confirm_label = 'Adding UDP Type: o3de_atom_phys to mesh.'
         bpy.types.Scene.pop_up_question_label = 'Add UDP to a Duplicate mesh?'
         bpy.types.Scene.pop_up_question_label = 'Add UDP to a Duplicate mesh?'
         bpy.types.Scene.udp_type = constants.UDP.get('o3de_atom_phys')
         bpy.types.Scene.udp_type = constants.UDP.get('o3de_atom_phys')
+        bpy.types.Scene.pop_up_type = "UDP"
         bpy.ops.message_confirm.popup('INVOKE_DEFAULT')
         bpy.ops.message_confirm.popup('INVOKE_DEFAULT')
         return{'FINISHED'}
         return{'FINISHED'}
 
 
@@ -171,58 +339,10 @@ class AddLODMesh(bpy.types.Operator):
         bpy.types.Scene.pop_up_confirm_label = 'Adding UDP Type: o3de_atom_lod to mesh.'
         bpy.types.Scene.pop_up_confirm_label = 'Adding UDP Type: o3de_atom_lod to mesh.'
         bpy.types.Scene.pop_up_question_label = 'Add UDP to a Duplicate mesh?'
         bpy.types.Scene.pop_up_question_label = 'Add UDP to a Duplicate mesh?'
         bpy.types.Scene.udp_type = constants.UDP.get('o3de_atom_lod')
         bpy.types.Scene.udp_type = constants.UDP.get('o3de_atom_lod')
+        bpy.types.Scene.pop_up_type = "UDP"
         bpy.ops.message_confirm.popup('INVOKE_DEFAULT')
         bpy.ops.message_confirm.popup('INVOKE_DEFAULT')
         return{'FINISHED'}
         return{'FINISHED'}
 
 
-class ExportFiles(bpy.types.Operator):
-    """!
-    This function will send to selected mesh to an O3DE Project Path.
-    """
-    bl_idname = "vin.o3defilexport"
-    bl_label = "SENDFILES"
-
-    def export_files(self, file_name):
-        """!
-        This function will export the selected as an .fbx to the current project path.
-        @param file_name is the file name selected or string in export_file_name_o3de
-        """
-        # Add file ext
-        file_name = f'{file_name}.fbx'
-        fbx_exporter.fbx_file_exporter('', file_name)
-
-    def execute(self, context):
-        """!
-        This function will check the current selected count and multi_file_export_o3de bool is True or False.
-        If multi_file_export_o3de is True it will export each selected object as an .fbx file, if False it will
-        export all objects selected as one .fbx.
-        @param context defualt for this blender class
-        """
-        # Validate a selection
-        valid_selection, selected_name = utils.check_selected()
-        # Check if there are multi selections
-        if len(selected_name) > 1:
-            if bpy.types.Scene.multi_file_export_o3de:
-                for obj_name in selected_name:
-                    # Deselect all or will just keep adding to selection
-                    bpy.ops.object.select_all(action='DESELECT')
-                    # Select a mesh in the loop
-                    bpy.data.objects[obj_name].select_set(True)
-                    # Remove some nasty invalid char
-                    file_name = re.sub(r'\W+', '', obj_name)
-                    # Export file
-                    self.export_files(file_name)
-            else:
-                # Remove some nasty invalid char
-                file_name = re.sub(r'\W+', '', bpy.context.scene.export_file_name_o3de)
-                # Export file
-                self.export_files(file_name)
-        else:
-            # Remove some nasty invalid char
-            file_name = re.sub(r'\W+', '', bpy.context.scene.export_file_name_o3de)
-            # Export file
-            self.export_files(file_name)
-        return{'FINISHED'}
-
 class ProjectsListDropDown(bpy.types.Operator):
 class ProjectsListDropDown(bpy.types.Operator):
     """!
     """!
     This Class is for the O3DE Projects UI List Drop Down.
     This Class is for the O3DE Projects UI List Drop Down.
@@ -274,6 +394,10 @@ class SceneExporterFileMenu(Operator, ExportHelper):
         description="Export Textures in textures folder",
         description="Export Textures in textures folder",
         default=True,
         default=True,
     )
     )
+    export_mesh_as_triangles : BoolProperty(
+        name="Export Mesh as Triangles",
+        description="Export Mesh as Triangles"
+    )
     # Add animation to export
     # Add animation to export
     export_animation : BoolProperty(
     export_animation : BoolProperty(
         name="Keyframe Animation",
         name="Keyframe Animation",
@@ -287,6 +411,7 @@ class SceneExporterFileMenu(Operator, ExportHelper):
         """
         """
         bpy.types.Scene.export_textures_folder = self.export_textures_folder
         bpy.types.Scene.export_textures_folder = self.export_textures_folder
         bpy.types.Scene.file_menu_animation_export = self.export_animation
         bpy.types.Scene.file_menu_animation_export = self.export_animation
+        bpy.types.Scene.convert_mesh_to_triangles = self.export_mesh_as_triangles
         if self.export_animation:
         if self.export_animation:
             bpy.types.Scene.animation_export = constants.KEY_FRAME_ANIMATION
             bpy.types.Scene.animation_export = constants.KEY_FRAME_ANIMATION
             utils.valid_animation_selection()
             utils.valid_animation_selection()
@@ -325,7 +450,7 @@ class ExportOptionsListDropDown(bpy.types.Operator):
         else:
         else:
             bpy.types.Scene.export_textures_folder = None
             bpy.types.Scene.export_textures_folder = None
         return {'FINISHED'}
         return {'FINISHED'}
-
+        
 class AnimationOptionsListDropDown(bpy.types.Operator):
 class AnimationOptionsListDropDown(bpy.types.Operator):
     """!
     """!
     This Class is for the O3DE Export Animations UI List Drop Down
     This Class is for the O3DE Export Animations UI List Drop Down
@@ -388,8 +513,9 @@ class O3deTools(Panel):
         """
         """
         layout = self.layout
         layout = self.layout
         selected_objects = context.object
         selected_objects = context.object
-        row = layout.row()
         wm = context.window_manager
         wm = context.window_manager
+        row = layout.row()
+
         # Look at the o3de Engine Manifest
         # Look at the o3de Engine Manifest
         o3de_projects, engine_is_installed = o3de_utils.look_at_engine_manifest()
         o3de_projects, engine_is_installed = o3de_utils.look_at_engine_manifest()
         
         
@@ -397,37 +523,31 @@ class O3deTools(Panel):
         valid_selection, selected_name = utils.check_selected()
         valid_selection, selected_name = utils.check_selected()
         
         
         # Validate selection bone names if any
         # Validate selection bone names if any
-        bone_names = utils.check_selected_bone_names()
-
-        if engine_is_installed: # Checks to see if O3DE is installed
-            row.operator("vin.wiki", text='O3DE Tools Wiki', icon="WORLD_DATA")
-            # Let user choose a custom project path
-            local_project_path = layout.row()
-            local_project_path.operator("project.open_filebrowser", text="Add Custom Project Path", icon="OUTLINER_OB_GROUP_INSTANCE")
+        invalid_bone_names, bone_are_in_selected = utils.check_selected_bone_names()
+
+        # Get the names of current selected
+        name_selections = [obj.name for obj in bpy.context.selected_objects]
+
+        # Checks to see if O3DE is installed
+        help_info_row = layout.box()
+        help_info_row.label(text="HELP / INFORMATION", icon="EVENT_A")
+        # Checks to see if O3DE is installed
+        if engine_is_installed:
+            wiki_row = layout.row()
+            wiki_row.operator("vin.wiki", text='O3DE Tools Wiki', icon="WORLD_DATA")
             # Heads up of current selected objects and options
             # Heads up of current selected objects and options
-            current_selected_options_row = layout.row()
+            current_selected_options_row = layout.box()
             current_selected_options_row.label(text="CURRENT SELECTED", icon="OUTLINER_DATA_GP_LAYER")
             current_selected_options_row.label(text="CURRENT SELECTED", icon="OUTLINER_DATA_GP_LAYER")
             # Let user know how many objects are selected
             # Let user know how many objects are selected
             installed_lable = layout.row()
             installed_lable = layout.row()
-            selections = [obj.name for obj in bpy.context.selected_objects]
-            if len(selections) == 0: 
-                installed_lable.label(text='SELECTED: None')
+            
+            # Check if there are any selections
+            if len(name_selections) == 0: 
+                installed_lable.label(text='Selected: None')
             else:
             else:
                 installed_lable.label(text=f'({len(selected_name)}) Selected: {selected_objects.name}')
                 installed_lable.label(text=f'({len(selected_name)}) Selected: {selected_objects.name}')
 
 
-            # If more than one object is selected, let user choose export options
-            if len(selections) > 1:
-                multi_file_export_label = "Export Multi-Files" if wm.multi_file_export_toggle else "Export a Single File"
-                multi_file_export_button = layout.row()
-                multi_file_export_button.prop(wm, "multi_file_export_toggle", text=multi_file_export_label, toggle=True)
-                if wm.multi_file_export_toggle:
-                    bpy.types.Scene.multi_file_export_o3de = True
-                else:
-                    bpy.types.Scene.multi_file_export_o3de = False
-            else:
-                wm.multi_file_export_toggle = False
-                bpy.types.Scene.multi_file_export_o3de = False
-            
+            # Show which project path is the current
             project_path_lable = layout.row()
             project_path_lable = layout.row()
             if not bpy.types.Scene.selected_o3de_project_path == '':
             if not bpy.types.Scene.selected_o3de_project_path == '':
                 project = Path(bpy.types.Scene.selected_o3de_project_path).name
                 project = Path(bpy.types.Scene.selected_o3de_project_path).name
@@ -435,13 +555,22 @@ class O3deTools(Panel):
             else:
             else:
                 project_path_lable.label(text='Project: None')
                 project_path_lable.label(text='Project: None')
             
             
+            # Check to see which Animation Action is active
+            action_name, action_count = utils.check_for_animation_actions()
+            
             # Let user choose animation export options
             # Let user choose animation export options
-            animation_export_lable = layout.row()
-            if not bpy.types.Scene.animation_export == constants.NO_ANIMATION:
-                animation_export_lable.label(text=f'Animation: {bpy.types.Scene.animation_export}')
+            if bone_are_in_selected:
+                animation_action_lable = layout.row()
+                animation_export_lable = layout.row()
+                if bpy.types.Scene.animation_export == constants.NO_ANIMATION:
+                    animation_action_lable.label(text=f'Action: ')
+                    animation_export_lable.label(text=f'Animation: {bpy.types.Scene.animation_export}')
+                else:
+                    animation_action_lable.label(text=f'Action: {action_name} ({action_count})')
+                    animation_export_lable.label(text=f'Animation: {bpy.types.Scene.animation_export}')
             else:
             else:
-                animation_export_lable.label(text=f'Animation: {bpy.types.Scene.animation_export}')
-            
+                bpy.types.Scene.animation_export = constants.NO_ANIMATION
+
             # This is the UI Export Texture Option Label
             # This is the UI Export Texture Option Label
             texture_export_option_label = layout.row()
             texture_export_option_label = layout.row()
             if bpy.types.Scene.export_textures_folder:
             if bpy.types.Scene.export_textures_folder:
@@ -451,9 +580,21 @@ class O3deTools(Panel):
             elif bpy.types.Scene.export_textures_folder is None:
             elif bpy.types.Scene.export_textures_folder is None:
                 texture_export_option_label.label(text='Texture Export: Off')
                 texture_export_option_label.label(text='Texture Export: Off')
             
             
+            # User selects projects folder path
+            o3de_projects_row = layout.box()
+            o3de_projects_row.label(text="PROJECTS", icon="EVENT_B")
+
+            # This is the UI Porjects List
+            o3de_projects_panel = layout.row()
+            o3de_projects_panel.operator('wm.projectlist', text='O3DE Projects', icon="OUTLINER")
+
+            # Let user choose a custom project path
+            local_project_path = layout.row()
+            local_project_path.operator("project.open_filebrowser", text="Add Custom Project Path", icon="OUTLINER_OB_GROUP_INSTANCE")
+
             # User can add UDP
             # User can add UDP
-            user_defined_properties_row = layout.row()
-            user_defined_properties_row.label(text="USER DEFINED (UDP)", icon="NODETREE")
+            user_defined_properties_row = layout.box()
+            user_defined_properties_row.label(text="USER DEFINED (UDP)", icon="EVENT_C")
             
             
             # Let user create a O3DE Collider Mesh for Physx
             # Let user create a O3DE Collider Mesh for Physx
             create_collider_button = layout.row()
             create_collider_button = layout.row()
@@ -462,18 +603,10 @@ class O3deTools(Panel):
             # Let user create a O3DE LOD Mesh
             # Let user create a O3DE LOD Mesh
             create_lod_button = layout.row()
             create_lod_button = layout.row()
             create_lod_button.operator("vin.lod", text='Create LOD', icon="MOD_REMESH")
             create_lod_button.operator("vin.lod", text='Create LOD', icon="MOD_REMESH")
-
-            # User selects projects folder path
-            o3de_projects_row = layout.row()
-            o3de_projects_row.label(text="PROJECTS", icon="OUTLINER")
-
-            # This is the UI Porjects List
-            o3de_projects_panel = layout.row()
-            o3de_projects_panel.operator('wm.projectlist', text='O3DE Projects', icon="OUTLINER")
             
             
             # User selected export options
             # User selected export options
-            o3de_projects_row = layout.row()
-            o3de_projects_row.label(text="EXPORT OPTIONS", icon="ZOOM_ALL")
+            o3de_projects_row = layout.box()
+            o3de_projects_row.label(text="EXPORT OPTIONS", icon="EVENT_D")
 
 
             # This is the UI Texture Export Options List
             # This is the UI Texture Export Options List
             texture_export_options_panel = layout.row()
             texture_export_options_panel = layout.row()
@@ -482,19 +615,41 @@ class O3deTools(Panel):
             # This is the UI Animation Export Options List
             # This is the UI Animation Export Options List
             animation_export_options_panel = layout.row()
             animation_export_options_panel = layout.row()
             animation_export_options_panel.operator('wm.animationoptions', text='Animation Export Options', icon="POSE_HLT")
             animation_export_options_panel.operator('wm.animationoptions', text='Animation Export Options', icon="POSE_HLT")
+
+            # If more than one object is selected, let user choose export options
+            if len(name_selections) > 1:
+                multi_file_export_label = "Export Multi-Files" if wm.multi_file_export_toggle else "Export a Single File"
+                multi_file_export_button = layout.row()
+                multi_file_export_button.prop(wm, "multi_file_export_toggle", text=multi_file_export_label, toggle=True)
+                if wm.multi_file_export_toggle:
+                    bpy.types.Scene.multi_file_export_o3de = True
+                else:
+                    bpy.types.Scene.multi_file_export_o3de = False
+            else:
+                wm.multi_file_export_toggle = False
+                bpy.types.Scene.multi_file_export_o3de = False
+            # Export mesh options
+            export_mesh_triangles_quads_label = "Exporting as Triangles" if wm.mesh_triangle_export_toggle else "Exporting as Quads"
+            export_mesh_triangles_quads_button = layout.row()
+            export_mesh_triangles_quads_button.prop(wm, "mesh_triangle_export_toggle", text=export_mesh_triangles_quads_label, toggle=True)
+            if wm.mesh_triangle_export_toggle:
+                bpy.types.Scene.convert_mesh_to_triangles = True
+            else:
+                bpy.types.Scene.convert_mesh_to_triangles = False
             
             
             # This checks to see if we should enable the export button
             # This checks to see if we should enable the export button
-            export_row = layout.row()
-            export_row.label(text="EXPORT FILE", icon="TEXT")
+            export_row = layout.box()
+            export_row.label(text="EXPORT FILE", icon="EVENT_E")
             # Let user choose a custom file name
             # Let user choose a custom file name
             file_name_lable = layout.row()
             file_name_lable = layout.row()
             if not wm.multi_file_export_toggle:
             if not wm.multi_file_export_toggle:
                 file_name_lable.label(text='Export File Name')
                 file_name_lable.label(text='Export File Name')
                 file_name_export_input = self.layout.column(align = True)
                 file_name_export_input = self.layout.column(align = True)
                 file_name_export_input.prop(context.scene, "export_file_name_o3de")
                 file_name_export_input.prop(context.scene, "export_file_name_o3de")
-            
+            # Export File
             export_files_row = layout.row()
             export_files_row = layout.row()
             export_ready_row = layout.row()
             export_ready_row = layout.row()
+            # Enable Rows on and off
             if bpy.types.Scene.selected_o3de_project_path == '':
             if bpy.types.Scene.selected_o3de_project_path == '':
                 export_files_row.enabled = False
                 export_files_row.enabled = False
                 export_ready_row.label(text='No Project Selected.')
                 export_ready_row.label(text='No Project Selected.')
@@ -507,13 +662,13 @@ class O3deTools(Panel):
             elif not valid_selection:
             elif not valid_selection:
                 export_files_row.enabled = False
                 export_files_row.enabled = False
                 export_ready_row.label(text='Nothing Selected.')
                 export_ready_row.label(text='Nothing Selected.')
-            elif bone_names == False:
+            elif invalid_bone_names == False:
                 export_files_row.enabled = False
                 export_files_row.enabled = False
                 export_ready_row.label(text='Invalid Char in Bone Names.')
                 export_ready_row.label(text='Invalid Char in Bone Names.')
             else:
             else:
                 export_files_row.enabled = True
                 export_files_row.enabled = True
-            # This is the export button
-            export_files_row.operator('vin.o3defilexport', text='EXPORT TO O3DE', icon="BLENDER")
+            # Final Export Files Button
+            export_files_row.operator('vin.report_card_button', text='EXPORT TO O3DE', icon="BLENDER")
         else:
         else:
             # If O3DE is not installed we tell the user
             # If O3DE is not installed we tell the user
             row.operator("vin.wiki", text='O3DE Tools Wiki', icon="WORLD_DATA")
             row.operator("vin.wiki", text='O3DE Tools Wiki', icon="WORLD_DATA")

+ 154 - 9
Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/AddOns/SceneExporter/utils.py

@@ -18,6 +18,14 @@ from . import constants
 from . import ui
 from . import ui
 from . import o3de_utils
 from . import o3de_utils
 
 
+def select_object(obj):
+    """!
+    This function will select just one object
+    """
+    bpy.ops.object.select_all(action='DESELECT')
+    bpy.context.view_layer.objects.active = obj
+    obj.select_set(True)
+
 def check_selected():
 def check_selected():
     """!
     """!
     This function will check to see if the user has selected a object
     This function will check to see if the user has selected a object
@@ -35,18 +43,23 @@ def check_selected_bone_names():
     context = bpy.context
     context = bpy.context
     obj = context.object
     obj = context.object
 
 
+    invalid_bone_names = None
+    bone_are_in_selected = None
+
     for selected_obj in context.selected_objects:
     for selected_obj in context.selected_objects:
         if selected_obj is not []:
         if selected_obj is not []:
             if selected_obj.type == "ARMATURE":
             if selected_obj.type == "ARMATURE":
+                bone_are_in_selected = True
                 try:
                 try:
                     for pb in obj.pose.bones:
                     for pb in obj.pose.bones:
                         # Validate all chars in string.
                         # Validate all chars in string.
                         check_re =  re.compile(r"^[^<>/{}[\]~.`]*$")
                         check_re =  re.compile(r"^[^<>/{}[\]~.`]*$")
                         if not check_re.match(pb.name):
                         if not check_re.match(pb.name):
                             # If any in the ARMATURE are named invalid return will fail.
                             # If any in the ARMATURE are named invalid return will fail.
-                            return False
+                            invalid_bone_names = False
                 except AttributeError:
                 except AttributeError:
                     pass
                     pass
+    return invalid_bone_names, bone_are_in_selected
 
 
 def selected_hierarchy_and_rig_animation():
 def selected_hierarchy_and_rig_animation():
     """!
     """!
@@ -93,6 +106,28 @@ def valid_animation_selection():
             else:
             else:
                 selected_hierarchy_and_rig_animation()
                 selected_hierarchy_and_rig_animation()
 
 
+def check_for_animation_actions():
+    """!
+    This function will check to see if the current scene has animation actions
+    return actions[0] is the top level animation action
+    return len(actions) is the number of available actions
+    """
+    obj = bpy.context.object
+    actions = []
+    active_action_name = ""
+    # Look for animation actions available
+    for animations in bpy.data.actions:
+        actions.append(animations.name)
+    # Look for Active Animation Action
+    try:
+        if not obj.animation_data == None:
+            active_action_name = obj.animation_data.action.name
+        else:
+            active_action_name = ""
+    except AttributeError:
+        pass
+    return active_action_name, len(actions)
+
 def check_if_valid_path(file_path):
 def check_if_valid_path(file_path):
     """!
     """!
     This function will check to see if a file path exist and return a bool
     This function will check to see if a file path exist and return a bool
@@ -300,6 +335,29 @@ def check_for_blender_int_ext(duplicated_node_name):
         name = duplicated_node_name
         name = duplicated_node_name
     return name
     return name
 
 
+def check_for_udp():
+    """!
+    This function will check if selected has and custom properties 
+    Example: print( f"Value: {obj_upd_keys} Key: {bpy.data.objects[selected_obj.name][obj_upd_keys] } ")
+    """
+    context = bpy.context
+    # Create list to store selected meshs udps
+    lod_list = []
+    colliders_list = []
+
+    for selected_obj in context.selected_objects:
+        if selected_obj is not []:
+            if selected_obj.type == "MESH":
+                for obj_upd_keys in bpy.data.objects[selected_obj.name].keys():
+                    if obj_upd_keys not in '_RNA_UI':
+                        if obj_upd_keys == "o3de_atom_lod":
+                            lod_list.append(True)
+                        if obj_upd_keys == "o3de_atom_phys":
+                            colliders_list.append(True)
+    lods = any(lod_list)
+    colliders = any(colliders_list)      
+    return lods, colliders
+
 def create_udp():
 def create_udp():
     """!
     """!
     This function will create a copy of a selected mesh and create an o3de PhysX collider PhysX Mesh that will
     This function will create a copy of a selected mesh and create an o3de PhysX collider PhysX Mesh that will
@@ -330,11 +388,11 @@ def create_udp():
         if bpy.types.Scene.udp_type == "_phys":
         if bpy.types.Scene.udp_type == "_phys":
             duplicate_object = duplicate_selected(selected_objects, f"{selected_objects.name}_phys")
             duplicate_object = duplicate_selected(selected_objects, f"{selected_objects.name}_phys")
             duplicate_object["o3de_atom_phys"] = "_phys"
             duplicate_object["o3de_atom_phys"] = "_phys"
-        # Set the copy active
-        bpy.context.view_layer.objects.active = duplicate_object
-        # Add the Decimate Modifier on for user.
-        bpy.ops.object.modifier_add(type="DECIMATE")
-        bpy.context.object.modifiers["Decimate"].ratio = 0.5
+        # Select the duplicated object only
+        select_object(duplicate_object)
+        # Add the Decimate Modifier on for user. Since both _lod and _phys will need this modifier
+        #  for now we will have it as a default.
+        add_remove_modifier("DECIMATE", True)
     else:
     else:
         if bpy.types.Scene.udp_type == "_lod":
         if bpy.types.Scene.udp_type == "_lod":
                 selected_objects['o3de_atom_lod'] = "_lod0"
                 selected_objects['o3de_atom_lod'] = "_lod0"
@@ -352,12 +410,99 @@ def compair_set(list_a, list_b):
     else:
     else:
         return False
         return False
 
 
+def add_remove_modifier(modifier_name, add):
+    """!
+    This function will add or remove a modifier to selected
+    @param modifier_name is the name of the modifier you wish to add or remove
+    @param add if Bool True will add the modifier, if False will remove 
+    """
+    context = bpy.context
+
+    for selected_obj in context.selected_objects:
+        if selected_obj is not []:
+            if selected_obj.type == "MESH":
+                if add:
+                    # Set the mesh active
+                    bpy.context.view_layer.objects.active = selected_obj
+                    # Add Modifier
+                    bpy.ops.object.modifier_add(type=modifier_name)
+                    if modifier_name == "TRIANGULATE":
+                        bpy.context.object.modifiers["Triangulate"].keep_custom_normals = True
+                else:
+                    # Set the mesh active
+                    bpy.context.view_layer.objects.active = selected_obj
+                    # Remove Modifier
+                    bpy.ops.object.modifier_remove(modifier=modifier_name)
+
+def check_selected_stats_count():
+    """!
+    This function will check selected and get poly counts
+    """
+    # We will make list for each selection
+    mesh_vertices_list = []
+    mesh_edges_list = []
+    mesh_polygons_list = []
+    mesh_material_list = []
+    
+    context = bpy.context
+    
+    for selected_obj in context.selected_objects:
+        if selected_obj is not []:
+            if selected_obj.type == "MESH":
+                mesh_stats = selected_obj.data
+                # Vert, Edge, Poly Counts
+                mesh_vertices = len(mesh_stats.vertices)
+                mesh_edges = len(mesh_stats.edges)
+                mesh_polygons = len(mesh_stats.polygons)
+                mesh_materials = len(mesh_stats.materials)
+                mesh_vertices_list.append(mesh_vertices)
+                mesh_edges_list.append(mesh_edges)
+                mesh_polygons_list.append(mesh_polygons)
+                # Material Count
+                mesh_material_list.append(mesh_materials)
+
+    vert_count = sum(mesh_vertices_list)
+    edge_count = sum(mesh_edges_list)
+    polygon_count = sum(mesh_polygons_list)
+    material_count = sum(mesh_material_list)
+    # Reset the list
+    mesh_vertices_list = []
+    mesh_edges_list = []
+    mesh_polygons_list = []
+    mesh_material_list = []
+    return vert_count, edge_count, polygon_count, material_count
+
+def check_selected_uvs():
+    """!
+    This function will check to see if there are UVs
+    """
+    context = bpy.context
+    # We will make list for each selection and compair if there are UVs.
+    mesh_uv_list = []
+    
+    for selected_obj in context.selected_objects:
+        if selected_obj is not []:
+            if selected_obj.type == "MESH":
+                if selected_obj.data.uv_layers:
+                    mesh_uv_list.append(True)
+                else:
+                    mesh_uv_list.append(False)
+    object_uv_list = all(mesh_uv_list)
+    # If all objects have UVs
+    if object_uv_list:
+        # Clear UV List
+        mesh_uv_list = []
+        return True
+    else:
+        # Clear UV List
+        mesh_uv_list = []
+        return False
+
 def check_selected_transforms():
 def check_selected_transforms():
     """!
     """!
     This function will check to see if there are unfrozen transfors and to warn the artist before export.
     This function will check to see if there are unfrozen transfors and to warn the artist before export.
     """
     """
     context = bpy.context
     context = bpy.context
-    ob = context.object
     # We will make list for each selection and compair to a frozzen transfrom.
     # We will make list for each selection and compair to a frozzen transfrom.
     location_list = []
     location_list = []
     location_source = [0.0, 0.0, 0.0]
     location_source = [0.0, 0.0, 0.0]
@@ -401,9 +546,9 @@ def check_selected_transforms():
             # Check if all are true or false
             # Check if all are true or false
             check_transfroms_bools = [location_good, rotation_good, scale_good]
             check_transfroms_bools = [location_good, rotation_good, scale_good]
             if all(check_transfroms_bools):
             if all(check_transfroms_bools):
-                return True
+                return True, location_list
             else:
             else:
-                return False
+                return False, location_list
         # Reset the list
         # Reset the list
         location_list = []
         location_list = []
         rotation_list = []
         rotation_list = []