Explorar o código

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

Codex shawstar
Jonny Galloway %!s(int64=3) %!d(string=hai) anos
pai
achega
dcd61f1b64

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

@@ -36,7 +36,7 @@
 bl_info = {
     "name": "O3DE_DCCSI_BLENDER_SCENE_EXPORTER",
     "author": "shawstar@amazon",
-    "version": (1, 4),
+    "version": (1, 5),
     "blender": (3, 00, 0),
     "location": "",
     "description": "Export Scene Assets to O3DE",
@@ -59,17 +59,19 @@ from . import constants
 
 
 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.utils.register_class(ui.O3deTools)
     bpy.utils.register_class(ui.MessageBox)
     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.CustomProjectPath)
     bpy.utils.register_class(ui.AddColliderMesh)
     bpy.utils.register_class(ui.AddLODMesh)
-    bpy.utils.register_class(ui.ExportFiles)
     bpy.utils.register_class(ui.ProjectsListDropDown)
     bpy.utils.register_class(ui.SceneExporterFileMenu)
     bpy.utils.register_class(ui.ExportOptionsListDropDown)
@@ -85,6 +87,7 @@ def register():
     bpy.types.Scene.pop_up_confirm_label = ''
     bpy.types.Scene.pop_up_question_label = ''
     bpy.types.Scene.pop_up_question_bool = False
+    bpy.types.Scene.pop_up_type = ''
     bpy.types.Scene.udp_type = ''
     bpy.types.Scene.export_textures_folder = True
     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.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.mesh_triangle_export_toggle = bpy.props.BoolProperty()
+    bpy.types.Scene.export_option_gui = False
+    bpy.types.Scene.convert_mesh_to_triangles = True
+    
 def unregister():
     """! 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.MessageBox)
     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.CustomProjectPath)
     bpy.utils.unregister_class(ui.AddColliderMesh)
     bpy.utils.unregister_class(ui.AddLODMesh)
-    bpy.utils.unregister_class(ui.ExportFiles)
     bpy.utils.unregister_class(ui.ProjectsListDropDown)
     bpy.utils.unregister_class(ui.SceneExporterFileMenu)
     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_question_label
     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.selected_o3de_project_path
     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.stored_image_source_paths
     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__":
     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()
 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'
-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
             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())
-
+                # 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()
-
+        # Main Blender FBX API exporter
         bpy.ops.export_scene.fbx(
             filepath=str(export_file_path),
             check_existing=False,
@@ -161,9 +164,11 @@ def fbx_file_exporter(fbx_file_path, file_name):
             axis_forward='-Z',
             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
-        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')
         if not bpy.types.Scene.export_textures_folder is None:
             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
 from pathlib import Path
 import webbrowser
@@ -90,11 +91,176 @@ class MessageBoxConfirm(bpy.types.Operator):
     def execute(self, context):
         """!
         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")
         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):
     """!
     This Class is for the UI Wiki Button
@@ -108,6 +274,7 @@ class WikiButton(bpy.types.Operator):
         """
         webbrowser.open(constants.WIKI_URL)
         return{'FINISHED'}
+
 class CustomProjectPath(bpy.types.Operator, ImportHelper):
     """!
     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_question_label = 'Add UDP to a Duplicate mesh?'
         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')
         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_question_label = 'Add UDP to a Duplicate mesh?'
         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')
         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):
     """!
     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",
         default=True,
     )
+    export_mesh_as_triangles : BoolProperty(
+        name="Export Mesh as Triangles",
+        description="Export Mesh as Triangles"
+    )
     # Add animation to export
     export_animation : BoolProperty(
         name="Keyframe Animation",
@@ -287,6 +411,7 @@ class SceneExporterFileMenu(Operator, ExportHelper):
         """
         bpy.types.Scene.export_textures_folder = self.export_textures_folder
         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:
             bpy.types.Scene.animation_export = constants.KEY_FRAME_ANIMATION
             utils.valid_animation_selection()
@@ -325,7 +450,7 @@ class ExportOptionsListDropDown(bpy.types.Operator):
         else:
             bpy.types.Scene.export_textures_folder = None
         return {'FINISHED'}
-
+        
 class AnimationOptionsListDropDown(bpy.types.Operator):
     """!
     This Class is for the O3DE Export Animations UI List Drop Down
@@ -388,8 +513,9 @@ class O3deTools(Panel):
         """
         layout = self.layout
         selected_objects = context.object
-        row = layout.row()
         wm = context.window_manager
+        row = layout.row()
+
         # Look at the o3de 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()
         
         # 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
-            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")
             # Let user know how many objects are selected
             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:
                 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()
             if not bpy.types.Scene.selected_o3de_project_path == '':
                 project = Path(bpy.types.Scene.selected_o3de_project_path).name
@@ -435,13 +555,22 @@ class O3deTools(Panel):
             else:
                 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
-            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:
-                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
             texture_export_option_label = layout.row()
             if bpy.types.Scene.export_textures_folder:
@@ -451,9 +580,21 @@ class O3deTools(Panel):
             elif bpy.types.Scene.export_textures_folder is None:
                 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_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
             create_collider_button = layout.row()
@@ -462,18 +603,10 @@ class O3deTools(Panel):
             # Let user create a O3DE LOD Mesh
             create_lod_button = layout.row()
             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
-            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
             texture_export_options_panel = layout.row()
@@ -482,19 +615,41 @@ class O3deTools(Panel):
             # This is the UI Animation Export Options List
             animation_export_options_panel = layout.row()
             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
-            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
             file_name_lable = layout.row()
             if not wm.multi_file_export_toggle:
                 file_name_lable.label(text='Export File Name')
                 file_name_export_input = self.layout.column(align = True)
                 file_name_export_input.prop(context.scene, "export_file_name_o3de")
-            
+            # Export File
             export_files_row = layout.row()
             export_ready_row = layout.row()
+            # Enable Rows on and off
             if bpy.types.Scene.selected_o3de_project_path == '':
                 export_files_row.enabled = False
                 export_ready_row.label(text='No Project Selected.')
@@ -507,13 +662,13 @@ class O3deTools(Panel):
             elif not valid_selection:
                 export_files_row.enabled = False
                 export_ready_row.label(text='Nothing Selected.')
-            elif bone_names == False:
+            elif invalid_bone_names == False:
                 export_files_row.enabled = False
                 export_ready_row.label(text='Invalid Char in Bone Names.')
             else:
                 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:
             # If O3DE is not installed we tell the user
             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 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():
     """!
     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
     obj = context.object
 
+    invalid_bone_names = None
+    bone_are_in_selected = None
+
     for selected_obj in context.selected_objects:
         if selected_obj is not []:
             if selected_obj.type == "ARMATURE":
+                bone_are_in_selected = True
                 try:
                     for pb in obj.pose.bones:
                         # Validate all chars in string.
                         check_re =  re.compile(r"^[^<>/{}[\]~.`]*$")
                         if not check_re.match(pb.name):
                             # If any in the ARMATURE are named invalid return will fail.
-                            return False
+                            invalid_bone_names = False
                 except AttributeError:
                     pass
+    return invalid_bone_names, bone_are_in_selected
 
 def selected_hierarchy_and_rig_animation():
     """!
@@ -93,6 +106,28 @@ def valid_animation_selection():
             else:
                 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):
     """!
     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
     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():
     """!
     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":
             duplicate_object = duplicate_selected(selected_objects, f"{selected_objects.name}_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:
         if bpy.types.Scene.udp_type == "_lod":
                 selected_objects['o3de_atom_lod'] = "_lod0"
@@ -352,12 +410,99 @@ def compair_set(list_a, list_b):
     else:
         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():
     """!
     This function will check to see if there are unfrozen transfors and to warn the artist before export.
     """
     context = bpy.context
-    ob = context.object
     # We will make list for each selection and compair to a frozzen transfrom.
     location_list = []
     location_source = [0.0, 0.0, 0.0]
@@ -401,9 +546,9 @@ def check_selected_transforms():
             # Check if all are true or false
             check_transfroms_bools = [location_good, rotation_good, scale_good]
             if all(check_transfroms_bools):
-                return True
+                return True, location_list
             else:
-                return False
+                return False, location_list
         # Reset the list
         location_list = []
         rotation_list = []