Преглед изворни кода

Merge branch 'stabilization/2110' into Prism/FixGemCart

AMZN-nggieber пре 3 година
родитељ
комит
7de4628747
100 измењених фајлова са 1197 додато и 562 уклоњено
  1. 54 0
      AutomatedTesting/Gem/PythonTests/AWS/Windows/aws_metrics/aws_metrics_automation_test.py
  2. 36 0
      AutomatedTesting/Gem/PythonTests/AWS/Windows/client_auth/aws_client_auth_automation_test.py
  3. 49 0
      AutomatedTesting/Gem/PythonTests/AWS/Windows/core/test_aws_resource_interaction.py
  4. 14 0
      AutomatedTesting/Gem/PythonTests/AWS/common/resource_mappings.py
  5. 1 2
      AutomatedTesting/Gem/PythonTests/Atom/atom_utils/atom_component_helper.py
  6. 8 0
      AutomatedTesting/Gem/PythonTests/Atom/atom_utils/atom_constants.py
  7. 1 1
      AutomatedTesting/Gem/PythonTests/Atom/golden_images/AreaLight_1.ppm
  8. 1 1
      AutomatedTesting/Gem/PythonTests/Atom/golden_images/AreaLight_2.ppm
  9. 1 1
      AutomatedTesting/Gem/PythonTests/Atom/golden_images/AreaLight_3.ppm
  10. 1 1
      AutomatedTesting/Gem/PythonTests/Atom/golden_images/AreaLight_4.ppm
  11. 1 1
      AutomatedTesting/Gem/PythonTests/Atom/golden_images/AreaLight_5.ppm
  12. 1 1
      AutomatedTesting/Gem/PythonTests/Atom/golden_images/AtomBasicLevelSetup.ppm
  13. 1 1
      AutomatedTesting/Gem/PythonTests/Atom/golden_images/SpotLight_1.ppm
  14. 1 1
      AutomatedTesting/Gem/PythonTests/Atom/golden_images/SpotLight_2.ppm
  15. 1 1
      AutomatedTesting/Gem/PythonTests/Atom/golden_images/SpotLight_3.ppm
  16. 1 1
      AutomatedTesting/Gem/PythonTests/Atom/golden_images/SpotLight_4.ppm
  17. 1 1
      AutomatedTesting/Gem/PythonTests/Atom/golden_images/SpotLight_5.ppm
  18. 1 1
      AutomatedTesting/Gem/PythonTests/Atom/golden_images/SpotLight_6.ppm
  19. 2 4
      AutomatedTesting/Gem/PythonTests/Atom/tests/hydra_AtomEditorComponents_GlobalSkylightIBLAdded.py
  20. 149 112
      AutomatedTesting/Gem/PythonTests/Atom/tests/hydra_AtomGPU_BasicLevelSetup.py
  21. 1 2
      AutomatedTesting/Gem/PythonTests/Atom/tests/hydra_GPUTest_BasicLevelSetup.py
  22. 4 0
      AutomatedTesting/Gem/PythonTests/EditorPythonTestTools/editor_python_test_tools/hydra_editor_utils.py
  23. 46 34
      AutomatedTesting/Gem/PythonTests/PythonAssetBuilder/AssetBuilder_test_case.py
  24. 0 7
      Code/Editor/2DViewport.cpp
  25. 0 2
      Code/Editor/2DViewport.h
  26. 1 1
      Code/Editor/AboutDialog.ui
  27. 2 0
      Code/Editor/CryEdit.cpp
  28. 21 2
      Code/Editor/EditorModularViewportCameraComposer.cpp
  29. 16 72
      Code/Editor/EditorViewportWidget.cpp
  30. 2 4
      Code/Editor/EditorViewportWidget.h
  31. 0 1
      Code/Editor/Include/IDisplayViewport.h
  32. 98 50
      Code/Editor/Lib/Tests/Camera/test_EditorCamera.cpp
  33. 12 3
      Code/Editor/Lib/Tests/test_ModularViewportCameraController.cpp
  34. 6 0
      Code/Editor/Plugins/ComponentEntityEditorPlugin/SandboxIntegration.cpp
  35. 1 1
      Code/Editor/StartupLogoDialog.ui
  36. 21 16
      Code/Editor/ViewportManipulatorController.cpp
  37. 5 1
      Code/Framework/AzCore/AzCore/Asset/AssetManager.cpp
  38. 4 3
      Code/Framework/AzCore/AzCore/Jobs/JobManagerComponent.cpp
  39. 2 0
      Code/Framework/AzCore/AzCore/Serialization/Json/JsonSystemComponent.cpp
  40. 7 0
      Code/Framework/AzCore/AzCore/Serialization/Json/UnsupportedTypesSerializer.cpp
  41. 10 0
      Code/Framework/AzCore/AzCore/Serialization/Json/UnsupportedTypesSerializer.h
  42. 6 1
      Code/Framework/AzCore/AzCore/Task/TaskGraphSystemComponent.cpp
  43. 0 1
      Code/Framework/AzCore/Platform/Android/AzCore/AzCore_Traits_Android.h
  44. 0 1
      Code/Framework/AzCore/Platform/Linux/AzCore/AzCore_Traits_Linux.h
  45. 0 1
      Code/Framework/AzCore/Platform/Mac/AzCore/AzCore_Traits_Mac.h
  46. 0 1
      Code/Framework/AzCore/Platform/Windows/AzCore/AzCore_Traits_Windows.h
  47. 0 1
      Code/Framework/AzCore/Platform/iOS/AzCore/AzCore_Traits_iOS.h
  48. 3 3
      Code/Framework/AzCore/Tests/Asset/AssetManagerLoadingTests.cpp
  49. 4 0
      Code/Framework/AzFramework/AzFramework/Archive/Archive.cpp
  50. 53 28
      Code/Framework/AzFramework/AzFramework/Viewport/CameraInput.cpp
  51. 14 0
      Code/Framework/AzFramework/AzFramework/Viewport/CameraInput.h
  52. 32 18
      Code/Framework/AzFramework/AzFramework/Viewport/CameraState.cpp
  53. 6 2
      Code/Framework/AzFramework/AzFramework/Viewport/CameraState.h
  54. 4 0
      Code/Framework/AzFramework/AzFramework/Viewport/ScreenGeometry.cpp
  55. 66 2
      Code/Framework/AzFramework/AzFramework/Viewport/ScreenGeometry.h
  56. 3 5
      Code/Framework/AzFramework/AzFramework/Viewport/ViewportScreen.cpp
  57. 46 7
      Code/Framework/AzFramework/Tests/CameraInputTests.cpp
  58. 3 17
      Code/Framework/AzFramework/Tests/CameraState.cpp
  59. 2 1
      Code/Framework/AzFramework/Tests/CursorStateTests.cpp
  60. 32 0
      Code/Framework/AzFramework/Tests/Utils/Printers.cpp
  61. 20 0
      Code/Framework/AzFramework/Tests/Utils/Printers.h
  62. 2 0
      Code/Framework/AzFramework/Tests/framework_shared_tests_files.cmake
  63. 2 2
      Code/Framework/AzManipulatorTestFramework/Include/AzManipulatorTestFramework/ViewportInteraction.h
  64. 1 1
      Code/Framework/AzManipulatorTestFramework/Source/AzManipulatorTestFrameworkUtils.cpp
  65. 0 2
      Code/Framework/AzManipulatorTestFramework/Source/ImmediateModeActionDispatcher.cpp
  66. 2 1
      Code/Framework/AzManipulatorTestFramework/Source/IndirectManipulatorViewportInteraction.cpp
  67. 3 4
      Code/Framework/AzManipulatorTestFramework/Source/ViewportInteraction.cpp
  68. 1 1
      Code/Framework/AzManipulatorTestFramework/Tests/WorldSpaceBuilderTest.cpp
  69. 25 0
      Code/Framework/AzToolsFramework/AzToolsFramework/API/PythonLoader.h
  70. 3 0
      Code/Framework/AzToolsFramework/AzToolsFramework/API/ToolsApplicationAPI.h
  71. 25 8
      Code/Framework/AzToolsFramework/AzToolsFramework/Application/ToolsApplication.cpp
  72. 5 2
      Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/AssetPicker/AssetPickerDialog.cpp
  73. 2 0
      Code/Framework/AzToolsFramework/AzToolsFramework/Component/EditorComponentAPIComponent.cpp
  74. 4 1
      Code/Framework/AzToolsFramework/AzToolsFramework/UnitTest/AzToolsFrameworkTestHelpers.h
  75. 2 8
      Code/Framework/AzToolsFramework/AzToolsFramework/Viewport/ViewportMessages.h
  76. 5 3
      Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorHelpers.cpp
  77. 2 2
      Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorTransformComponentSelection.cpp
  78. 3 5
      Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorVisibleEntityDataCache.cpp
  79. 4 1
      Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorVisibleEntityDataCache.h
  80. 1 0
      Code/Framework/AzToolsFramework/AzToolsFramework/aztoolsframework_files.cmake
  81. 20 0
      Code/Framework/AzToolsFramework/Platform/Common/Default/AzToolsFramework/API/PythonLoader_Default.cpp
  82. 34 0
      Code/Framework/AzToolsFramework/Platform/Linux/AzToolsFramework/API/PythonLoader_Linux.cpp
  83. 1 0
      Code/Framework/AzToolsFramework/Platform/Linux/platform_linux_files.cmake
  84. 1 0
      Code/Framework/AzToolsFramework/Platform/Mac/platform_mac_files.cmake
  85. 1 0
      Code/Framework/AzToolsFramework/Platform/Windows/platform_windows_files.cmake
  86. 4 2
      Code/Framework/AzToolsFramework/Tests/BoundsTestComponent.cpp
  87. 2 0
      Code/Framework/AzToolsFramework/Tests/BoundsTestComponent.h
  88. 41 13
      Code/Framework/AzToolsFramework/Tests/EditorTransformComponentSelectionTests.cpp
  89. 1 0
      Code/Framework/AzToolsFramework/Tests/EditorVertexSelectionTests.cpp
  90. 6 4
      Code/Framework/AzToolsFramework/Tests/Viewport/ViewportScreenTests.cpp
  91. 68 0
      Code/Tools/AssetProcessor/AssetBuilderSDK/AssetBuilderSDK/AssetBuilderSDK.cpp
  92. 13 0
      Code/Tools/AssetProcessor/AssetBuilderSDK/AssetBuilderSDK/AssetBuilderSDK.h
  93. 1 0
      Code/Tools/AssetProcessor/AssetBuilderSDK/CMakeLists.txt
  94. 2 1
      Code/Tools/AssetProcessor/Platform/Linux/native/FileWatcher/FileWatcher_linux.cpp
  95. 4 60
      Code/Tools/AssetProcessor/native/utilities/assetUtils.cpp
  96. 0 1
      Code/Tools/AssetProcessor/native/utilities/assetUtils.h
  97. 11 4
      Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp
  98. 1 1
      Code/Tools/ProjectManager/Source/GemRepo/GemRepoInfo.h
  99. 5 2
      Code/Tools/ProjectManager/Source/GemRepo/GemRepoInspector.cpp
  100. 11 15
      Code/Tools/ProjectManager/Source/GemRepo/GemRepoModel.cpp

+ 54 - 0
AutomatedTesting/Gem/PythonTests/AWS/Windows/aws_metrics/aws_metrics_automation_test.py

@@ -14,6 +14,7 @@ from datetime import datetime
 import ly_test_tools.log.log_monitor
 
 from AWS.common import constants
+from AWS.common.resource_mappings import AWS_RESOURCE_MAPPINGS_ACCOUNT_ID_KEY
 from .aws_metrics_custom_thread import AWSMetricsThread
 
 # fixture imports
@@ -200,6 +201,59 @@ class TestAWSMetricsWindows(object):
         for thread in operational_threads:
             thread.join()
 
+    @pytest.mark.parametrize('level', ['AWS/Metrics'])
+    def test_realtime_and_batch_analytics_no_global_accountid(self,
+                                                            level: str,
+                                                            launcher: pytest.fixture,
+                                                            asset_processor: pytest.fixture,
+                                                            workspace: pytest.fixture,
+                                                            aws_utils: pytest.fixture,
+                                                            resource_mappings: pytest.fixture,
+                                                            aws_metrics_utils: pytest.fixture):
+        """
+        Verify that the metrics events are sent to CloudWatch and S3 for analytics.
+        """
+        # Remove top-level account ID from resource mappings
+        resource_mappings.clear_select_keys([AWS_RESOURCE_MAPPINGS_ACCOUNT_ID_KEY])
+        # Start Kinesis analytics application on a separate thread to avoid blocking the test.
+        kinesis_analytics_application_thread = AWSMetricsThread(target=update_kinesis_analytics_application_status,
+                                                                args=(aws_metrics_utils, resource_mappings, True))
+        kinesis_analytics_application_thread.start()
+
+        log_monitor = setup(launcher, asset_processor)
+
+        # Kinesis analytics application needs to be in the running state before we start the game launcher.
+        kinesis_analytics_application_thread.join()
+        launcher.args = ['+LoadLevel', level]
+        launcher.args.extend(['-rhi=null'])
+        start_time = datetime.utcnow()
+        with launcher.start(launch_ap=False):
+            monitor_metrics_submission(log_monitor)
+
+            # Verify that real-time analytics metrics are delivered to CloudWatch.
+            aws_metrics_utils.verify_cloud_watch_delivery(
+                AWS_METRICS_FEATURE_NAME,
+                'TotalLogins',
+                [],
+                start_time)
+            logger.info('Real-time metrics are sent to CloudWatch.')
+
+        # Run time-consuming operations on separate threads to avoid blocking the test.
+        operational_threads = list()
+        operational_threads.append(
+            AWSMetricsThread(target=query_metrics_from_s3,
+                             args=(aws_metrics_utils, resource_mappings)))
+        operational_threads.append(
+            AWSMetricsThread(target=verify_operational_metrics,
+                             args=(aws_metrics_utils, resource_mappings, start_time)))
+        operational_threads.append(
+            AWSMetricsThread(target=update_kinesis_analytics_application_status,
+                             args=(aws_metrics_utils, resource_mappings, False)))
+        for thread in operational_threads:
+            thread.start()
+        for thread in operational_threads:
+            thread.join()
+
     @pytest.mark.parametrize('level', ['AWS/Metrics'])
     def test_unauthorized_user_request_rejected(self,
                                                 level: str,

+ 36 - 0
AutomatedTesting/Gem/PythonTests/AWS/Windows/client_auth/aws_client_auth_automation_test.py

@@ -12,6 +12,7 @@ import pytest
 import ly_test_tools.log.log_monitor
 
 from AWS.common import constants
+from AWS.common.resource_mappings import AWS_RESOURCE_MAPPINGS_ACCOUNT_ID_KEY
 
 # fixture imports
 from assetpipeline.ap_fixtures.asset_processor_fixture import asset_processor
@@ -70,6 +71,41 @@ class TestAWSClientAuthWindows(object):
                 halt_on_unexpected=True,
             )
             assert result, 'Anonymous credentials fetched successfully.'
+    
+    @pytest.mark.parametrize('level', ['AWS/ClientAuth'])
+    def test_anonymous_credentials_no_global_accountid(self,
+                                                       level: str,
+                                                       launcher: pytest.fixture,
+                                                       resource_mappings: pytest.fixture,
+                                                       workspace: pytest.fixture,
+                                                       asset_processor: pytest.fixture
+                                                       ):
+        """
+        Test to verify AWS Cognito Identity pool anonymous authorization.
+
+        Setup: Updates resource mapping file using existing CloudFormation stacks.
+        Tests: Getting credentials when no credentials are configured
+        Verification: Log monitor looks for success credentials log.
+        """
+        # Remove top-level account ID from resource mappings
+        resource_mappings.clear_select_keys([AWS_RESOURCE_MAPPINGS_ACCOUNT_ID_KEY])
+        
+        asset_processor.start()
+        asset_processor.wait_for_idle()
+
+        file_to_monitor = os.path.join(launcher.workspace.paths.project_log(), constants.GAME_LOG_NAME)
+        log_monitor = ly_test_tools.log.log_monitor.LogMonitor(launcher=launcher, log_file_path=file_to_monitor)
+
+        launcher.args = ['+LoadLevel', level]
+        launcher.args.extend(['-rhi=null'])
+
+        with launcher.start(launch_ap=False):
+            result = log_monitor.monitor_log_for_lines(
+                expected_lines=['(Script) - Success anonymous credentials'],
+                unexpected_lines=['(Script) - Fail anonymous credentials'],
+                halt_on_unexpected=True,
+            )
+            assert result, 'Anonymous credentials fetched successfully.'
 
     def test_password_signin_credentials(self,
                                          launcher: pytest.fixture,

+ 49 - 0
AutomatedTesting/Gem/PythonTests/AWS/Windows/core/test_aws_resource_interaction.py

@@ -18,6 +18,7 @@ import ly_test_tools.environment.process_utils as process_utils
 import ly_test_tools.o3de.asset_processor_utils as asset_processor_utils
 
 from AWS.common import constants
+from AWS.common.resource_mappings import AWS_RESOURCE_MAPPINGS_ACCOUNT_ID_KEY
 
 # fixture imports
 from assetpipeline.ap_fixtures.asset_processor_fixture import asset_processor
@@ -141,3 +142,51 @@ class TestAWSCoreAWSResourceInteraction(object):
             'The expected file wasn\'t successfully downloaded.'
         # clean up the file directories.
         shutil.rmtree(s3_download_dir)
+
+    @pytest.mark.parametrize('expected_lines', [
+        ['(Script) - [S3] Head object request is done',
+         '(Script) - [S3] Head object success: Object example.txt is found.',
+         '(Script) - [S3] Get object success: Object example.txt is downloaded.',
+         '(Script) - [Lambda] Completed Invoke',
+         '(Script) - [Lambda] Invoke success: {"statusCode": 200, "body": {}}',
+         '(Script) - [DynamoDB] Results finished']])
+    @pytest.mark.parametrize('unexpected_lines', [
+        ['(Script) - [S3] Head object error: No response body.',
+         '(Script) - [S3] Get object error: Request validation failed, output file directory doesn\'t exist.',
+         '(Script) - Request validation failed, output file miss full path.',
+         '(Script) - ']])
+    def test_scripting_behavior_no_global_accountid(self,
+                                                    level: str,
+                                                    launcher: pytest.fixture,
+                                                    workspace: pytest.fixture,
+                                                    asset_processor: pytest.fixture,
+                                                    resource_mappings: pytest.fixture,
+                                                    aws_utils: pytest.fixture,
+                                                    expected_lines: typing.List[str],
+                                                    unexpected_lines: typing.List[str]):
+        """
+        Setup: Updates resource mapping file using existing CloudFormation stacks.
+        Tests: Interact with AWS S3, DynamoDB and Lambda services.
+        Verification: Script canvas nodes can communicate with AWS services successfully.
+        """
+
+        resource_mappings.clear_select_keys([AWS_RESOURCE_MAPPINGS_ACCOUNT_ID_KEY])
+        log_monitor, s3_download_dir = setup(launcher, asset_processor)
+        write_test_data_to_dynamodb_table(resource_mappings, aws_utils)
+
+        launcher.args = ['+LoadLevel', level]
+        launcher.args.extend(['-rhi=null'])
+
+        with launcher.start(launch_ap=False):
+            result = log_monitor.monitor_log_for_lines(
+                expected_lines=expected_lines,
+                unexpected_lines=unexpected_lines,
+                halt_on_unexpected=True
+                )
+
+            assert result, "Expected lines weren't found."
+
+        assert os.path.exists(os.path.join(s3_download_dir, 'output.txt')), \
+            'The expected file wasn\'t successfully downloaded.'
+        # clean up the file directories.
+        shutil.rmtree(s3_download_dir)

+ 14 - 0
AutomatedTesting/Gem/PythonTests/AWS/common/resource_mappings.py

@@ -102,3 +102,17 @@ class ResourceMappings:
 
     def get_resource_name_id(self, resource_key: str):
         return self._resource_mappings[AWS_RESOURCE_MAPPINGS_KEY][resource_key]['Name/ID']
+
+    def clear_select_keys(self, resource_keys=None) -> None:
+        """
+        Clears values from select resource mapping keys.
+        :param resource_keys: list of keys to clear out
+        """
+        with open(self._resource_mapping_file_path) as file_content:
+            resource_mappings = json.load(file_content)
+
+        for key in resource_keys:
+            resource_mappings[key] = ''
+
+        with open(self._resource_mapping_file_path, 'w') as file_content:
+            json.dump(resource_mappings, file_content, indent=4)

+ 1 - 2
AutomatedTesting/Gem/PythonTests/Atom/atom_utils/atom_component_helper.py

@@ -150,8 +150,7 @@ def create_basic_atom_level(level_name):
         entity_position=default_position,
         components=["HDRi Skybox", "Global Skylight (IBL)"],
         parent_id=default_level.id)
-    global_skylight_asset_path = os.path.join(
-        "LightingPresets", "greenwich_park_02_4k_iblskyboxcm_iblspecular.exr.streamingimage")
+    global_skylight_asset_path = os.path.join("LightingPresets", "default_iblskyboxcm.exr.streamingimage")
     global_skylight_asset_value = asset.AssetCatalogRequestBus(
         bus.Broadcast, "GetAssetIdByPath", global_skylight_asset_path, math.Uuid(), False)
     global_skylight.get_set_test(0, "Controller|Configuration|Cubemap Texture", global_skylight_asset_value)

+ 8 - 0
AutomatedTesting/Gem/PythonTests/Atom/atom_utils/atom_constants.py

@@ -55,11 +55,13 @@ class AtomComponentProperties:
     def camera(property: str = 'name') -> str:
         """
         Camera component properties.
+          - 'Field of view': Sets the value for the camera's FOV (Field of View) in degrees, i.e. 60.0
         :param property: From the last element of the property tree path. Default 'name' for component name string.
         :return: Full property path OR component name if no property specified.
         """
         properties = {
             'name': 'Camera',
+            'Field of view': 'Controller|Configuration|Field of view'
         }
         return properties[property]
 
@@ -198,11 +200,13 @@ class AtomComponentProperties:
     def grid(property: str = 'name') -> str:
         """
         Grid component properties.
+          - 'Secondary Grid Spacing': The spacing value for the secondary grid, i.e. 1.0
         :param property: From the last element of the property tree path. Default 'name' for component name string.
         :return: Full property path OR component name if no property specified.
         """
         properties = {
             'name': 'Grid',
+            'Secondary Grid Spacing': 'Controller|Configuration|Secondary Grid Spacing',
         }
         return properties[property]
 
@@ -225,11 +229,13 @@ class AtomComponentProperties:
     def hdri_skybox(property: str = 'name') -> str:
         """
         HDRi Skybox component properties.
+          - 'Cubemap Texture': Asset.id for the cubemap texture to set.
         :param property: From the last element of the property tree path. Default 'name' for component name string.
         :return: Full property path OR component name if no property specified.
         """
         properties = {
             'name': 'HDRi Skybox',
+            'Cubemap Texture': 'Controller|Configuration|Cubemap Texture',
         }
         return properties[property]
 
@@ -268,12 +274,14 @@ class AtomComponentProperties:
         Material component properties. Requires one of Actor OR Mesh component.
           - 'requires' a list of component names as strings required by this component.
             Only one of these is required at a time for this component.\n
+          - 'Material Asset': the material Asset.id of the material.
         :param property: From the last element of the property tree path. Default 'name' for component name string.
         :return: Full property path OR component name if no property specified.
         """
         properties = {
             'name': 'Material',
             'requires': [AtomComponentProperties.actor(), AtomComponentProperties.mesh()],
+            'Material Asset': 'Default Material|Material Asset',
         }
         return properties[property]
 

+ 1 - 1
AutomatedTesting/Gem/PythonTests/Atom/golden_images/AreaLight_1.ppm

@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:954d7d0df47c840a24e313893800eb3126d0c0d47c3380926776b51833778db7
+oid sha256:aee1fd4d5264e5ef1676b507409ce70af6358cf1ff368d9aeb17f7b2597dfbca
 size 6220817

+ 1 - 1
AutomatedTesting/Gem/PythonTests/Atom/golden_images/AreaLight_2.ppm

@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:e81c19128f42ba362a2d5f3ccf159dfbc942d67ceeb1ac8c21f295a6fd9d2ce5
+oid sha256:d4787cdafbcc2fe71c1cb3f1da53a249db839a9df539a9e88be43ccd6d8e4d6a
 size 6220817

+ 1 - 1
AutomatedTesting/Gem/PythonTests/Atom/golden_images/AreaLight_3.ppm

@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:5e20801213e065b6ea8c95ede81c23faa9b6dc70a2002dc5bced293e1bed989f
+oid sha256:5fac5bf41c9b16b6fbd762868e5cf514376af92d6ef7ebb9e819f024f1a3e1a7
 size 6220817

+ 1 - 1
AutomatedTesting/Gem/PythonTests/Atom/golden_images/AreaLight_4.ppm

@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:e250f812e594e5152bf2d6f23caa8b53b78276bfdf344d7a8d355dd96cb995c0
+oid sha256:7a23969670499524725535e8be7428b55b6f3e887cc24e2e903f7ea821a6d1a5
 size 6220817

+ 1 - 1
AutomatedTesting/Gem/PythonTests/Atom/golden_images/AreaLight_5.ppm

@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:95be359041f8291c74b335297a4dfe9902a180510f24a181b15e1a5ba4d3b024
+oid sha256:2f1f4d8865c56ed7f96f339c39e5feb4e0dbc6c6a8b4a7843b4166381b06b00d
 size 6220817

+ 1 - 1
AutomatedTesting/Gem/PythonTests/Atom/golden_images/AtomBasicLevelSetup.ppm

@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:07e09d3eb5bf0cee3d9b3752aaad40f3ead1dcc5ddd837a6226fadde55d57274
+oid sha256:5d4ee5641e19eef08dd6b93d2f4054a1aae2165325416ed2cbf0b8243f2c0b06
 size 6220817

+ 1 - 1
AutomatedTesting/Gem/PythonTests/Atom/golden_images/SpotLight_1.ppm

@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:118e43e4b915e262726183467cc4b82f244565213fea5b6bfe02be07f0851ab1
+oid sha256:55c8f0d1790bb12660b7557630efca297b2a1b59e6c93167a2563da79e0a8255
 size 6220817

+ 1 - 1
AutomatedTesting/Gem/PythonTests/Atom/golden_images/SpotLight_2.ppm

@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:dc2ce3256a6552975962c9e113c52c1a22bf3817d417151f6f60640dd568e0fa
+oid sha256:082ff368b621e12b083d96562a0889b11a1d683767a74296cbe6d8732830e9e8
 size 6220817

+ 1 - 1
AutomatedTesting/Gem/PythonTests/Atom/golden_images/SpotLight_3.ppm

@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:287d98890b35427688999760f9d066bcbff1a3bc9001534241dc212b32edabd8
+oid sha256:78cc62d89782899747875b41abee57c2efdfacf4c8af6511c88f82d76eaae4ca
 size 6220817

+ 1 - 1
AutomatedTesting/Gem/PythonTests/Atom/golden_images/SpotLight_4.ppm

@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:66e91c92c868167c850078cd91714db47e10a96e23cc30191994486bd79c353f
+oid sha256:3d6719326f4dacae278d1723090ce1182193b793f250963af8be4b2c298e8841
 size 6220817

+ 1 - 1
AutomatedTesting/Gem/PythonTests/Atom/golden_images/SpotLight_5.ppm

@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:d950d173f5101820c5e18205401ca08ce5feeff2302ac2920b292750d86a8fa4
+oid sha256:9e492bb394fb18fb117f8a5b61cd2789922f9d6e88fc83189b5b6d59ffb1c3ef
 size 6220817

+ 1 - 1
AutomatedTesting/Gem/PythonTests/Atom/golden_images/SpotLight_6.ppm

@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:72eddb7126eae0c839b933886e0fb69d78229f72d49ef13199de28df2b7879db
+oid sha256:caca85f7728f660daae36afc81d681ba2de2377c516eb3c637599de5c94012aa
 size 6220817

+ 2 - 4
AutomatedTesting/Gem/PythonTests/Atom/tests/hydra_AtomEditorComponents_GlobalSkylightIBLAdded.py

@@ -123,8 +123,7 @@ def AtomEditorComponents_GlobalSkylightIBL_AddedToEntity():
         Report.result(Tests.is_visible, global_skylight_entity.is_visible() is True)
 
         # 8. Set the Diffuse Image asset on the Global Skylight (IBL) entity.
-        global_skylight_diffuse_image_property = "Controller|Configuration|Diffuse Image"
-        diffuse_image_path = os.path.join("LightingPresets", "greenwich_park_02_4k_iblskyboxcm.exr.streamingimage")
+        diffuse_image_path = os.path.join("LightingPresets", "default_iblskyboxcm.exr.streamingimage")
         diffuse_image_asset = Asset.find_asset_by_path(diffuse_image_path, False)
         global_skylight_component.set_component_property_value(
             global_skylight_diffuse_image_property, diffuse_image_asset.id)
@@ -133,8 +132,7 @@ def AtomEditorComponents_GlobalSkylightIBL_AddedToEntity():
         Report.result(Tests.diffuse_image_set, diffuse_image_set == diffuse_image_asset.id)
 
         # 9. Set the Specular Image asset on the Global Light (IBL) entity.
-        global_skylight_specular_image_property = "Controller|Configuration|Specular Image"
-        specular_image_path = os.path.join("LightingPresets", "greenwich_park_02_4k_iblskyboxcm.exr.streamingimage")
+        specular_image_path = os.path.join("LightingPresets", "default_iblskyboxcm.exr.streamingimage")
         specular_image_asset = Asset.find_asset_by_path(specular_image_path, False)
         global_skylight_component.set_component_property_value(
             global_skylight_specular_image_property, specular_image_asset.id)

+ 149 - 112
AutomatedTesting/Gem/PythonTests/Atom/tests/hydra_AtomGPU_BasicLevelSetup.py

@@ -6,30 +6,70 @@ SPDX-License-Identifier: Apache-2.0 OR MIT
 """
 
 
-# fmt: off
-class Tests :
-    camera_component_added =                ("Camera component was added",                  "Camera component wasn't added")
-    camera_fov_set =                        ("Camera component FOV property set",           "Camera component FOV property wasn't set")
-    directional_light_component_added =     ("Directional Light component added",           "Directional Light component wasn't added")
-    enter_game_mode =                       ("Entered game mode",                           "Failed to enter game mode")
-    exit_game_mode =                        ("Exited game mode",                            "Couldn't exit game mode")
-    global_skylight_component_added =       ("Global Skylight (IBL) component added",       "Global Skylight (IBL) component wasn't added")
-    global_skylight_diffuse_image_set =     ("Global Skylight Diffuse Image property set",  "Global Skylight Diffuse Image property wasn't set")
-    global_skylight_specular_image_set =    ("Global Skylight Specular Image property set", "Global Skylight Specular Image property wasn't set")
-    ground_plane_material_asset_set =       ("Ground Plane Material Asset was set",         "Ground Plane Material Asset wasn't set")
-    ground_plane_material_component_added = ("Ground Plane Material component added",       "Ground Plane Material component wasn't added")
-    ground_plane_mesh_asset_set =           ("Ground Plane Mesh Asset property was set",    "Ground Plane Mesh Asset property wasn't set")
-    hdri_skybox_component_added =           ("HDRi Skybox component added",                 "HDRi Skybox component wasn't added")
-    hdri_skybox_cubemap_texture_set =       ("HDRi Skybox Cubemap Texture property set",    "HDRi Skybox Cubemap Texture property wasn't set")
-    mesh_component_added =                  ("Mesh component added",                        "Mesh component wasn't added")
-    no_assert_occurred =                    ("No asserts detected",                         "Asserts were detected")
-    no_error_occurred =                     ("No errors detected",                          "Errors were detected")
-    secondary_grid_spacing =                ("Secondary Grid Spacing set",                  "Secondary Grid Spacing not set")
-    sphere_material_component_added =       ("Sphere Material component added",             "Sphere Material component wasn't added")
-    sphere_material_set =                   ("Sphere Material Asset was set",               "Sphere Material Asset wasn't set")
-    sphere_mesh_asset_set =                 ("Sphere Mesh Asset was set",                   "Sphere Mesh Asset wasn't set")
-    viewport_set =                          ("Viewport set to correct size",                "Viewport not set to correct size")
-# fmt: on
+class Tests:
+    camera_component_added = (
+        "Camera component was added",
+        "Camera component wasn't added")
+    camera_fov_set = (
+        "Camera component FOV property set",
+        "Camera component FOV property wasn't set")
+    directional_light_component_added = (
+        "Directional Light component added",
+        "Directional Light component wasn't added")
+    enter_game_mode = (
+        "Entered game mode",
+        "Failed to enter game mode")
+    exit_game_mode = (
+        "Exited game mode",
+        "Couldn't exit game mode")
+    global_skylight_component_added = (
+        "Global Skylight (IBL) component added",
+        "Global Skylight (IBL) component wasn't added")
+    global_skylight_diffuse_image_set = (
+        "Global Skylight Diffuse Image property set",
+        "Global Skylight Diffuse Image property wasn't set")
+    global_skylight_specular_image_set = (
+        "Global Skylight Specular Image property set",
+        "Global Skylight Specular Image property wasn't set")
+    ground_plane_material_asset_set = (
+        "Ground Plane Material Asset was set",
+        "Ground Plane Material Asset wasn't set")
+    ground_plane_material_component_added = (
+        "Ground Plane Material component added",
+        "Ground Plane Material component wasn't added")
+    ground_plane_mesh_asset_set = (
+        "Ground Plane Mesh Asset property was set",
+        "Ground Plane Mesh Asset property wasn't set")
+    hdri_skybox_component_added = (
+        "HDRi Skybox component added",
+        "HDRi Skybox component wasn't added")
+    hdri_skybox_cubemap_texture_set = (
+        "HDRi Skybox Cubemap Texture property set",
+        "HDRi Skybox Cubemap Texture property wasn't set")
+    mesh_component_added = (
+        "Mesh component added",
+        "Mesh component wasn't added")
+    no_assert_occurred = (
+        "No asserts detected",
+        "Asserts were detected")
+    no_error_occurred = (
+        "No errors detected",
+        "Errors were detected")
+    secondary_grid_spacing = (
+        "Secondary Grid Spacing set",
+        "Secondary Grid Spacing not set")
+    sphere_material_component_added = (
+        "Sphere Material component added",
+        "Sphere Material component wasn't added")
+    sphere_material_set = (
+        "Sphere Material Asset was set",
+        "Sphere Material Asset wasn't set")
+    sphere_mesh_asset_set = (
+        "Sphere Mesh Asset was set",
+        "Sphere Mesh Asset wasn't set")
+    viewport_set = (
+        "Viewport set to correct size",
+        "Viewport not set to correct size")
 
 
 def AtomGPU_BasicLevelSetup_SetsUpLevel():
@@ -77,19 +117,17 @@ def AtomGPU_BasicLevelSetup_SetsUpLevel():
     import os
     from math import isclose
 
-    import azlmbr.asset as asset
-    import azlmbr.bus as bus
     import azlmbr.legacy.general as general
     import azlmbr.math as math
     import azlmbr.paths
 
+    from editor_python_test_tools.asset_utils import Asset
     from editor_python_test_tools.editor_entity_utils import EditorEntity
-    from editor_python_test_tools.utils import Report, Tracer, TestHelper as helper
+    from editor_python_test_tools.utils import Report, Tracer, TestHelper
 
+    from Atom.atom_utils.atom_constants import AtomComponentProperties
     from Atom.atom_utils.screenshot_utils import ScreenshotHelper
 
-    MATERIAL_COMPONENT_NAME = "Material"
-    MESH_COMPONENT_NAME = "Mesh"
     SCREENSHOT_NAME = "AtomBasicLevelSetup"
     SCREEN_WIDTH = 1280
     SCREEN_HEIGHT = 720
@@ -98,24 +136,24 @@ def AtomGPU_BasicLevelSetup_SetsUpLevel():
     def initial_viewport_setup(screen_width, screen_height):
         general.set_viewport_size(screen_width, screen_height)
         general.update_viewport()
-        result = isclose(
-            a=general.get_viewport_size().x, b=SCREEN_WIDTH, rel_tol=0.1) and isclose(
-            a=general.get_viewport_size().y, b=SCREEN_HEIGHT, rel_tol=0.1)
-
-        return result
+        TestHelper.wait_for_condition(
+            function=lambda: isclose(a=general.get_viewport_size().x, b=SCREEN_WIDTH, rel_tol=0.1)
+                        and isclose(a=general.get_viewport_size().y, b=SCREEN_HEIGHT, rel_tol=0.1),
+            timeout_in_seconds=4.0
+        )
 
     with Tracer() as error_tracer:
         # Test setup begins.
         # Setup: Wait for Editor idle loop before executing Python hydra scripts then open "Base" level.
-        helper.init_idle()
-        helper.open_level("", "Base")
+        TestHelper.init_idle()
+        TestHelper.open_level("", "Base")
 
         # Test steps begin.
         # 1. Close error windows and display helpers then update the viewport size.
-        helper.close_error_windows()
-        helper.close_display_helpers()
+        TestHelper.close_error_windows()
+        TestHelper.close_display_helpers()
+        initial_viewport_setup(SCREEN_WIDTH, SCREEN_HEIGHT)
         general.update_viewport()
-        Report.critical_result(Tests.viewport_set, initial_viewport_setup(SCREEN_WIDTH, SCREEN_HEIGHT))
 
         # 2. Create Default Level Entity.
         default_level_entity_name = "Default Level"
@@ -123,168 +161,167 @@ def AtomGPU_BasicLevelSetup_SetsUpLevel():
             math.Vector3(0.0, 0.0, 0.0), default_level_entity_name)
 
         # 3. Create Grid Entity as a child entity of the Default Level Entity.
-        grid_name = "Grid"
-        grid_entity = EditorEntity.create_editor_entity(grid_name, default_level_entity.id)
+        grid_entity = EditorEntity.create_editor_entity(AtomComponentProperties.grid(), default_level_entity.id)
 
         # 4. Add Grid component to Grid Entity and set Secondary Grid Spacing.
-        grid_component = grid_entity.add_component(grid_name)
-        secondary_grid_spacing_property = "Controller|Configuration|Secondary Grid Spacing"
+        grid_component = grid_entity.add_component(AtomComponentProperties.grid())
         secondary_grid_spacing_value = 1.0
-        grid_component.set_component_property_value(secondary_grid_spacing_property, secondary_grid_spacing_value)
+        grid_component.set_component_property_value(
+            AtomComponentProperties.grid('Secondary Grid Spacing'), secondary_grid_spacing_value)
         secondary_grid_spacing_set = grid_component.get_component_property_value(
-            secondary_grid_spacing_property) == secondary_grid_spacing_value
+            AtomComponentProperties.grid('Secondary Grid Spacing')) == secondary_grid_spacing_value
         Report.result(Tests.secondary_grid_spacing, secondary_grid_spacing_set)
 
         # 5. Create Global Skylight (IBL) Entity as a child entity of the Default Level Entity.
-        global_skylight_name = "Global Skylight (IBL)"
-        global_skylight_entity = EditorEntity.create_editor_entity(global_skylight_name, default_level_entity.id)
+        global_skylight_entity = EditorEntity.create_editor_entity(
+            AtomComponentProperties.global_skylight(), default_level_entity.id)
 
         # 6. Add HDRi Skybox component to the Global Skylight (IBL) Entity.
-        hdri_skybox_name = "HDRi Skybox"
-        hdri_skybox_component = global_skylight_entity.add_component(hdri_skybox_name)
-        Report.result(Tests.hdri_skybox_component_added, global_skylight_entity.has_component(hdri_skybox_name))
+        hdri_skybox_component = global_skylight_entity.add_component(AtomComponentProperties.hdri_skybox())
+        Report.result(Tests.hdri_skybox_component_added, global_skylight_entity.has_component(
+            AtomComponentProperties.hdri_skybox()))
 
         # 7. Add Global Skylight (IBL) component to the Global Skylight (IBL) Entity.
-        global_skylight_component = global_skylight_entity.add_component(global_skylight_name)
-        Report.result(Tests.global_skylight_component_added, global_skylight_entity.has_component(global_skylight_name))
+        global_skylight_component = global_skylight_entity.add_component(AtomComponentProperties.global_skylight())
+        Report.result(Tests.global_skylight_component_added, global_skylight_entity.has_component(
+            AtomComponentProperties.global_skylight()))
 
         # 8. Set the Cubemap Texture property of the HDRi Skybox component.
-        global_skylight_image_asset_path = os.path.join(
-            "LightingPresets", "greenwich_park_02_4k_iblskyboxcm_iblspecular.exr.streamingimage")
-        global_skylight_image_asset = asset.AssetCatalogRequestBus(
-            bus.Broadcast, "GetAssetIdByPath", global_skylight_image_asset_path, math.Uuid(), False)
-        hdri_skybox_cubemap_texture_property = "Controller|Configuration|Cubemap Texture"
+        global_skylight_image_asset_path = os.path.join("LightingPresets", "default_iblskyboxcm.exr.streamingimage")
+        global_skylight_image_asset = Asset.find_asset_by_path(global_skylight_image_asset_path, False)
         hdri_skybox_component.set_component_property_value(
-            hdri_skybox_cubemap_texture_property, global_skylight_image_asset)
+            AtomComponentProperties.hdri_skybox('Cubemap Texture'), global_skylight_image_asset.id)
         Report.result(
             Tests.hdri_skybox_cubemap_texture_set,
             hdri_skybox_component.get_component_property_value(
-                hdri_skybox_cubemap_texture_property) == global_skylight_image_asset)
+                AtomComponentProperties.hdri_skybox('Cubemap Texture')) == global_skylight_image_asset.id)
 
         # 9. Set the Diffuse Image property of the Global Skylight (IBL) component.
         # Re-use the same image that was used in the previous test step.
-        global_skylight_diffuse_image_property = "Controller|Configuration|Diffuse Image"
+        global_skylight_diffuse_image_asset_path = os.path.join(
+            "LightingPresets", "default_iblskyboxcm_ibldiffuse.exr.streamingimage")
+        global_skylight_diffuse_image_asset = Asset.find_asset_by_path(global_skylight_diffuse_image_asset_path, False)
         global_skylight_component.set_component_property_value(
-            global_skylight_diffuse_image_property, global_skylight_image_asset)
+            AtomComponentProperties.global_skylight('Diffuse Image'), global_skylight_diffuse_image_asset.id)
         Report.result(
             Tests.global_skylight_diffuse_image_set,
             global_skylight_component.get_component_property_value(
-                global_skylight_diffuse_image_property) == global_skylight_image_asset)
+                AtomComponentProperties.global_skylight('Diffuse Image')) == global_skylight_diffuse_image_asset.id)
 
         # 10. Set the Specular Image property of the Global Skylight (IBL) component.
         # Re-use the same image that was used in the previous test step.
-        global_skylight_specular_image_property = "Controller|Configuration|Specular Image"
+        global_skylight_specular_image_asset_path = os.path.join(
+            "LightingPresets", "default_iblskyboxcm_iblspecular.exr.streamingimage")
+        global_skylight_specular_image_asset = Asset.find_asset_by_path(
+            global_skylight_specular_image_asset_path, False)
         global_skylight_component.set_component_property_value(
-            global_skylight_specular_image_property, global_skylight_image_asset)
+            AtomComponentProperties.global_skylight('Specular Image'), global_skylight_specular_image_asset.id)
         global_skylight_specular_image_set = global_skylight_component.get_component_property_value(
-            global_skylight_specular_image_property)
+            AtomComponentProperties.global_skylight('Specular Image'))
         Report.result(
-            Tests.global_skylight_specular_image_set, global_skylight_specular_image_set == global_skylight_image_asset)
+            Tests.global_skylight_specular_image_set,
+            global_skylight_specular_image_set == global_skylight_specular_image_asset.id)
 
         # 11. Create a Ground Plane Entity with a Material component that is a child entity of the Default Level Entity.
         ground_plane_name = "Ground Plane"
         ground_plane_entity = EditorEntity.create_editor_entity(ground_plane_name, default_level_entity.id)
-        ground_plane_material_component = ground_plane_entity.add_component(MATERIAL_COMPONENT_NAME)
+        ground_plane_material_component = ground_plane_entity.add_component(AtomComponentProperties.material())
         Report.result(
-            Tests.ground_plane_material_component_added, ground_plane_entity.has_component(MATERIAL_COMPONENT_NAME))
+            Tests.ground_plane_material_component_added,
+            ground_plane_entity.has_component(AtomComponentProperties.material()))
 
         # 12. Set the Material Asset property of the Material component for the Ground Plane Entity.
         ground_plane_entity.set_local_uniform_scale(32.0)
         ground_plane_material_asset_path = os.path.join("Materials", "Presets", "PBR", "metal_chrome.azmaterial")
-        ground_plane_material_asset = asset.AssetCatalogRequestBus(
-            bus.Broadcast, "GetAssetIdByPath", ground_plane_material_asset_path, math.Uuid(), False)
-        ground_plane_material_asset_property = "Default Material|Material Asset"
+        ground_plane_material_asset = Asset.find_asset_by_path(ground_plane_material_asset_path, False)
         ground_plane_material_component.set_component_property_value(
-            ground_plane_material_asset_property, ground_plane_material_asset)
+            AtomComponentProperties.material('Material Asset'), ground_plane_material_asset.id)
         Report.result(
             Tests.ground_plane_material_asset_set,
             ground_plane_material_component.get_component_property_value(
-                ground_plane_material_asset_property) == ground_plane_material_asset)
+                AtomComponentProperties.material('Material Asset')) == ground_plane_material_asset.id)
 
         # 13. Add the Mesh component to the Ground Plane Entity and set the Mesh component Mesh Asset property.
-        ground_plane_mesh_component = ground_plane_entity.add_component(MESH_COMPONENT_NAME)
-        Report.result(Tests.mesh_component_added, ground_plane_entity.has_component(MESH_COMPONENT_NAME))
-        ground_plane_mesh_asset_path = os.path.join("Objects", "plane.azmodel")
-        ground_plane_mesh_asset = asset.AssetCatalogRequestBus(
-            bus.Broadcast, "GetAssetIdByPath", ground_plane_mesh_asset_path, math.Uuid(), False)
-        ground_plane_mesh_asset_property = "Controller|Configuration|Mesh Asset"
+        ground_plane_mesh_component = ground_plane_entity.add_component(AtomComponentProperties.mesh())
+        Report.result(Tests.mesh_component_added, ground_plane_entity.has_component(AtomComponentProperties.mesh()))
+        ground_plane_mesh_asset_path = os.path.join("TestData", "Objects", "plane.azmodel")
+        ground_plane_mesh_asset = Asset.find_asset_by_path(ground_plane_mesh_asset_path, False)
         ground_plane_mesh_component.set_component_property_value(
-            ground_plane_mesh_asset_property, ground_plane_mesh_asset)
+            AtomComponentProperties.mesh('Mesh Asset'), ground_plane_mesh_asset.id)
         Report.result(
             Tests.ground_plane_mesh_asset_set,
             ground_plane_mesh_component.get_component_property_value(
-                ground_plane_mesh_asset_property) == ground_plane_mesh_asset)
+                AtomComponentProperties.mesh('Mesh Asset')) == ground_plane_mesh_asset.id)
 
         # 14. Create a Directional Light Entity as a child entity of the Default Level Entity.
-        directional_light_name = "Directional Light"
         directional_light_entity = EditorEntity.create_editor_entity_at(
-            math.Vector3(0.0, 0.0, 10.0), directional_light_name, default_level_entity.id)
+            math.Vector3(0.0, 0.0, 10.0), AtomComponentProperties.directional_light(), default_level_entity.id)
 
         # 15. Add Directional Light component to Directional Light Entity and set entity rotation.
-        directional_light_entity.add_component(directional_light_name)
+        directional_light_entity.add_component(AtomComponentProperties.directional_light())
         directional_light_entity_rotation = math.Vector3(DEGREE_RADIAN_FACTOR * -90.0, 0.0, 0.0)
         directional_light_entity.set_local_rotation(directional_light_entity_rotation)
         Report.result(
-            Tests.directional_light_component_added, directional_light_entity.has_component(directional_light_name))
+            Tests.directional_light_component_added, directional_light_entity.has_component(
+                AtomComponentProperties.directional_light()))
 
         # 16. Create a Sphere Entity as a child entity of the Default Level Entity then add a Material component.
         sphere_entity = EditorEntity.create_editor_entity_at(
             math.Vector3(0.0, 0.0, 1.0), "Sphere", default_level_entity.id)
-        sphere_material_component = sphere_entity.add_component(MATERIAL_COMPONENT_NAME)
-        Report.result(Tests.sphere_material_component_added, sphere_entity.has_component(MATERIAL_COMPONENT_NAME))
+        sphere_material_component = sphere_entity.add_component(AtomComponentProperties.material())
+        Report.result(Tests.sphere_material_component_added, sphere_entity.has_component(
+            AtomComponentProperties.material()))
 
         # 17. Set the Material Asset property of the Material component for the Sphere Entity.
         sphere_material_asset_path = os.path.join("Materials", "Presets", "PBR", "metal_brass_polished.azmaterial")
-        sphere_material_asset = asset.AssetCatalogRequestBus(
-            bus.Broadcast, "GetAssetIdByPath", sphere_material_asset_path, math.Uuid(), False)
-        sphere_material_asset_property = "Default Material|Material Asset"
-        sphere_material_component.set_component_property_value(sphere_material_asset_property, sphere_material_asset)
+        sphere_material_asset = Asset.find_asset_by_path(sphere_material_asset_path, False)
+        sphere_material_component.set_component_property_value(
+            AtomComponentProperties.material('Material Asset'), sphere_material_asset.id)
         Report.result(Tests.sphere_material_set, sphere_material_component.get_component_property_value(
-            sphere_material_asset_property) == sphere_material_asset)
+            AtomComponentProperties.material('Material Asset')) == sphere_material_asset.id)
 
         # 18. Add Mesh component to Sphere Entity and set the Mesh Asset property for the Mesh component.
-        sphere_mesh_component = sphere_entity.add_component(MESH_COMPONENT_NAME)
+        sphere_mesh_component = sphere_entity.add_component(AtomComponentProperties.mesh())
         sphere_mesh_asset_path = os.path.join("Models", "sphere.azmodel")
-        sphere_mesh_asset = asset.AssetCatalogRequestBus(
-            bus.Broadcast, "GetAssetIdByPath", sphere_mesh_asset_path, math.Uuid(), False)
-        sphere_mesh_asset_property = "Controller|Configuration|Mesh Asset"
-        sphere_mesh_component.set_component_property_value(sphere_mesh_asset_property, sphere_mesh_asset)
+        sphere_mesh_asset = Asset.find_asset_by_path(sphere_mesh_asset_path, False)
+        sphere_mesh_component.set_component_property_value(
+            AtomComponentProperties.mesh('Mesh Asset'), sphere_mesh_asset.id)
         Report.result(Tests.sphere_mesh_asset_set, sphere_mesh_component.get_component_property_value(
-            sphere_mesh_asset_property) == sphere_mesh_asset)
+            AtomComponentProperties.mesh('Mesh Asset')) == sphere_mesh_asset.id)
 
         # 19. Create a Camera Entity as a child entity of the Default Level Entity then add a Camera component.
-        camera_name = "Camera"
         camera_entity = EditorEntity.create_editor_entity_at(
-            math.Vector3(5.5, -12.0, 9.0), camera_name, default_level_entity.id)
-        camera_component = camera_entity.add_component(camera_name)
-        Report.result(Tests.camera_component_added, camera_entity.has_component(camera_name))
+            math.Vector3(5.5, -12.0, 9.0), AtomComponentProperties.camera(), default_level_entity.id)
+        camera_component = camera_entity.add_component(AtomComponentProperties.camera())
+        Report.result(Tests.camera_component_added, camera_entity.has_component(AtomComponentProperties.camera()))
 
         # 20. Set the Camera Entity rotation value and set the Camera component Field of View value.
         camera_entity_rotation = math.Vector3(
             DEGREE_RADIAN_FACTOR * -27.0, DEGREE_RADIAN_FACTOR * -12.0, DEGREE_RADIAN_FACTOR * 25.0)
         camera_entity.set_local_rotation(camera_entity_rotation)
-        camera_fov_property = "Controller|Configuration|Field of view"
         camera_fov_value = 60.0
-        camera_component.set_component_property_value(camera_fov_property, camera_fov_value)
+        camera_component.set_component_property_value(AtomComponentProperties.camera('Field of view'), camera_fov_value)
         azlmbr.camera.EditorCameraViewRequestBus(azlmbr.bus.Event, "ToggleCameraAsActiveView", camera_entity.id)
         Report.result(Tests.camera_fov_set, camera_component.get_component_property_value(
-            camera_fov_property) == camera_fov_value)
+            AtomComponentProperties.camera('Field of view')) == camera_fov_value)
 
         # 21. Enter game mode.
-        helper.enter_game_mode(Tests.enter_game_mode)
-        helper.wait_for_condition(function=lambda: general.is_in_game_mode(), timeout_in_seconds=4.0)
+        TestHelper.enter_game_mode(Tests.enter_game_mode)
+        TestHelper.wait_for_condition(function=lambda: general.is_in_game_mode(), timeout_in_seconds=4.0)
 
         # 22. Take screenshot.
         ScreenshotHelper(general.idle_wait_frames).capture_screenshot_blocking(f"{SCREENSHOT_NAME}.ppm")
 
         # 23. Exit game mode.
-        helper.exit_game_mode(Tests.exit_game_mode)
-        helper.wait_for_condition(function=lambda: not general.is_in_game_mode(), timeout_in_seconds=4.0)
+        TestHelper.exit_game_mode(Tests.exit_game_mode)
+        TestHelper.wait_for_condition(function=lambda: not general.is_in_game_mode(), timeout_in_seconds=4.0)
 
         # 24. Look for errors.
-        helper.wait_for_condition(lambda: error_tracer.has_errors or error_tracer.has_asserts, 1.0)
-        Report.result(Tests.no_assert_occurred, not error_tracer.has_asserts)
-        Report.result(Tests.no_error_occurred, not error_tracer.has_errors)
+        TestHelper.wait_for_condition(lambda: error_tracer.has_errors or error_tracer.has_asserts, 1.0)
+        for error_info in error_tracer.errors:
+            Report.info(f"Error: {error_info.filename} {error_info.function} | {error_info.message}")
+        for assert_info in error_tracer.asserts:
+            Report.info(f"Assert: {assert_info.filename} {assert_info.function} | {assert_info.message}")
 
 
 if __name__ == "__main__":

+ 1 - 2
AutomatedTesting/Gem/PythonTests/Atom/tests/hydra_GPUTest_BasicLevelSetup.py

@@ -131,8 +131,7 @@ def run():
         components=["HDRi Skybox", "Global Skylight (IBL)"],
         parent_id=default_level.id
     )
-    global_skylight_image_asset_path = os.path.join(
-        "LightingPresets", "greenwich_park_02_4k_iblskyboxcm_iblspecular.exr.streamingimage")
+    global_skylight_image_asset_path = os.path.join("LightingPresets", "default_iblskyboxcm.exr.streamingimage")
     global_skylight_image_asset = asset.AssetCatalogRequestBus(
         bus.Broadcast, "GetAssetIdByPath", global_skylight_image_asset_path, math.Uuid(), False)
     global_skylight.get_set_test(0, "Controller|Configuration|Cubemap Texture", global_skylight_image_asset)

+ 4 - 0
AutomatedTesting/Gem/PythonTests/EditorPythonTestTools/editor_python_test_tools/hydra_editor_utils.py

@@ -57,6 +57,10 @@ def add_level_component(component_name):
                                                                  level_component_list, entity.EntityType().Level)
     level_component_outcome = editor.EditorLevelComponentAPIBus(bus.Broadcast, 'AddComponentsOfType',
                                                                 [level_component_type_ids_list[0]])
+    if not level_component_outcome.IsSuccess():
+        print('Failed to add {} level component'.format(component_name))
+        return None
+
     level_component = level_component_outcome.GetValue()[0]
     return level_component
 

+ 46 - 34
AutomatedTesting/Gem/PythonTests/PythonAssetBuilder/AssetBuilder_test_case.py

@@ -8,42 +8,54 @@ import azlmbr.bus
 import azlmbr.asset
 import azlmbr.editor
 import azlmbr.math
-import azlmbr.legacy.general
 
-def raise_and_stop(msg):
-    print (msg)
-    azlmbr.editor.EditorToolsApplicationRequestBus(azlmbr.bus.Broadcast, 'ExitNoPrompt')
-
-# These tests are meant to check that the test_asset.mock source asset turned into
-# a test_asset.mock_asset product asset via the Python asset builder system
-mockAssetType = azlmbr.math.Uuid_CreateString('{9274AD17-3212-4651-9F3B-7DCCB080E467}', 0)
-mockAssetPath = 'gem/pythontests/pythonassetbuilder/test_asset.mock_asset'
-assetId = azlmbr.asset.AssetCatalogRequestBus(azlmbr.bus.Broadcast, 'GetAssetIdByPath', mockAssetPath, mockAssetType, False)
-if (assetId.is_valid() is False):
-    raise_and_stop(f'Mock AssetId is not valid! Got {assetId.to_string()} instead')
-
-assetIdString = assetId.to_string()
-if (assetIdString.endswith(':528cca58') is False):
-    raise_and_stop(f'Mock AssetId {assetIdString} has unexpected sub-id for {mockAssetPath}!')
-
-print ('Mock asset exists')
+print('Starting mock asset tests')
+handler = azlmbr.editor.EditorEventBusHandler()
+
+def on_notify_editor_initialized(args):
+    # These tests are meant to check that the test_asset.mock source asset turned into
+    # a test_asset.mock_asset product asset via the Python asset builder system
+    mockAssetType = azlmbr.math.Uuid_CreateString('{9274AD17-3212-4651-9F3B-7DCCB080E467}', 0)
+    mockAssetPath = 'gem/pythontests/pythonassetbuilder/test_asset.mock_asset'
+    assetId = azlmbr.asset.AssetCatalogRequestBus(azlmbr.bus.Broadcast, 'GetAssetIdByPath', mockAssetPath, mockAssetType, False)
+    if (assetId.is_valid() is False):
+        print(f'Mock AssetId is not valid! Got {assetId.to_string()} instead')
+    else:
+        print(f'Mock AssetId is valid!')
 
-# These tests detect if the geom_group.fbx file turns into a number of azmodel product assets
-def test_azmodel_product(generatedModelAssetPath):
-    azModelAssetType = azlmbr.math.Uuid_CreateString('{2C7477B6-69C5-45BE-8163-BCD6A275B6D8}', 0)
-    assetId = azlmbr.asset.AssetCatalogRequestBus(azlmbr.bus.Broadcast, 'GetAssetIdByPath', generatedModelAssetPath, azModelAssetType, False)
     assetIdString = assetId.to_string()
-    if (assetId.is_valid()):
-        print(f'AssetId found for asset ({generatedModelAssetPath}) found')
+    if (assetIdString.endswith(':528cca58') is False):
+        print(f'Mock AssetId {assetIdString} has unexpected sub-id for {mockAssetPath}!')
     else:
-        raise_and_stop(f'Asset at path {generatedModelAssetPath} has unexpected asset ID ({assetIdString})!')
-
-test_azmodel_product('gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_z_positive_1.azmodel')
-test_azmodel_product('gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_z_negative_1.azmodel')
-test_azmodel_product('gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_y_positive_1.azmodel')
-test_azmodel_product('gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_y_negative_1.azmodel')
-test_azmodel_product('gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_x_positive_1.azmodel')
-test_azmodel_product('gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_x_negative_1.azmodel')
-test_azmodel_product('gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_center_1.azmodel')
+        print(f'Mock AssetId has expected sub-id for {mockAssetPath}!')
+
+    print ('Mock asset exists')
+
+    # These tests detect if the geom_group.fbx file turns into a number of azmodel product assets
+    def test_azmodel_product(generatedModelAssetPath):
+        azModelAssetType = azlmbr.math.Uuid_CreateString('{2C7477B6-69C5-45BE-8163-BCD6A275B6D8}', 0)
+        assetId = azlmbr.asset.AssetCatalogRequestBus(azlmbr.bus.Broadcast, 'GetAssetIdByPath', generatedModelAssetPath, azModelAssetType, False)
+        assetIdString = assetId.to_string()
+        if (assetId.is_valid()):
+            print(f'AssetId found for asset ({generatedModelAssetPath}) found')
+        else:
+            print(f'Asset at path {generatedModelAssetPath} has unexpected asset ID ({assetIdString})!')
+
+    test_azmodel_product('gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_z_positive_1.azmodel')
+    test_azmodel_product('gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_z_negative_1.azmodel')
+    test_azmodel_product('gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_y_positive_1.azmodel')
+    test_azmodel_product('gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_y_negative_1.azmodel')
+    test_azmodel_product('gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_x_positive_1.azmodel')
+    test_azmodel_product('gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_x_negative_1.azmodel')
+    test_azmodel_product('gem/pythontests/pythonassetbuilder/geom_group_fbx_cube_100cm_center_1.azmodel')
+
+    # clear up notification handler
+    global handler
+    handler.disconnect()
+    handler = None
+
+    print('Finished mock asset tests')
+    azlmbr.editor.EditorToolsApplicationRequestBus(azlmbr.bus.Broadcast, 'ExitNoPrompt')
 
-azlmbr.editor.EditorToolsApplicationRequestBus(azlmbr.bus.Broadcast, 'ExitNoPrompt')
+handler.connect()
+handler.add_callback('NotifyEditorInitialized', on_notify_editor_initialized)

+ 0 - 7
Code/Editor/2DViewport.cpp

@@ -547,13 +547,6 @@ QPoint Q2DViewport::WorldToView(const Vec3& wp) const
     QPoint p = QPoint(static_cast<int>(sp.x), static_cast<int>(sp.y));
     return p;
 }
-//////////////////////////////////////////////////////////////////////////
-QPoint Q2DViewport::WorldToViewParticleEditor(const Vec3& wp, [[maybe_unused]] int width, [[maybe_unused]] int height) const //Eric@conffx implement for the children class of IDisplayViewport
-{
-    Vec3 sp = m_screenTM.TransformPoint(wp);
-    QPoint p = QPoint(static_cast<int>(sp.x), static_cast<int>(sp.y));
-    return p;
-}
 
 //////////////////////////////////////////////////////////////////////////
 Vec3    Q2DViewport::ViewToWorld(const QPoint& vp, [[maybe_unused]] bool* collideWithTerrain, [[maybe_unused]] bool onlyTerrain, [[maybe_unused]] bool bSkipVegetation, [[maybe_unused]] bool bTestRenderMesh, [[maybe_unused]] bool* collideWithObject) const

+ 0 - 2
Code/Editor/2DViewport.h

@@ -50,8 +50,6 @@ public:
     //! Map world space position to viewport position.
     QPoint     WorldToView(const Vec3& wp) const override;
 
-    QPoint WorldToViewParticleEditor(const Vec3& wp, int width, int height) const override; //Eric@conffx
-
     //! Map viewport position to world space position.
     Vec3        ViewToWorld(const QPoint& vp, bool* collideWithTerrain = nullptr, bool onlyTerrain = false, bool bSkipVegetation = false, bool bTestRenderMesh = false, bool* collideWithObject = nullptr) const override;
     //! Map viewport position to world space ray from camera.

+ 1 - 1
Code/Editor/AboutDialog.ui

@@ -125,7 +125,7 @@
             </size>
            </property>
            <property name="text">
-            <string>General Availability</string>
+            <string>Stable 21.11</string>
            </property>
            <property name="textFormat">
             <enum>Qt::AutoText</enum>

+ 2 - 0
Code/Editor/CryEdit.cpp

@@ -4181,6 +4181,8 @@ extern "C" int AZ_DLL_EXPORT CryEditMain(int argc, char* argv[])
             "\nThis could be because of incorrectly configured components, or missing required gems."
             "\nSee other errors for more details.");
 
+        AzToolsFramework::EditorEventsBus::Broadcast(&AzToolsFramework::EditorEvents::NotifyEditorInitialized);
+
         if (didCryEditStart)
         {
             app->EnableOnIdle();

+ 21 - 2
Code/Editor/EditorModularViewportCameraComposer.cpp

@@ -145,6 +145,15 @@ namespace SandboxEditor
             }
         };
 
+        const auto trackingTransform = [viewportId = m_viewportId]
+        {
+            bool tracking = false;
+            AtomToolsFramework::ModularViewportCameraControllerRequestBus::EventResult(
+                tracking, viewportId, &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::IsTrackingTransform);
+
+            return tracking;
+        };
+
         m_firstPersonRotateCamera = AZStd::make_shared<AzFramework::RotateCameraInput>(SandboxEditor::CameraFreeLookChannelId());
 
         m_firstPersonRotateCamera->m_rotateSpeedFn = []
@@ -152,6 +161,11 @@ namespace SandboxEditor
             return SandboxEditor::CameraRotateSpeed();
         };
 
+        m_firstPersonRotateCamera->m_constrainPitch = [trackingTransform]
+        {
+            return !trackingTransform();
+        };
+
         // default behavior is to hide the cursor but this can be disabled (useful for remote desktop)
         // note: See CaptureCursorLook in the Settings Registry
         m_firstPersonRotateCamera->SetActivationBeganFn(hideCursor);
@@ -255,6 +269,11 @@ namespace SandboxEditor
             return SandboxEditor::CameraOrbitYawRotationInverted();
         };
 
+        m_orbitRotateCamera->m_constrainPitch = [trackingTransform]
+        {
+            return !trackingTransform();
+        };
+
         m_orbitTranslateCamera = AZStd::make_shared<AzFramework::TranslateCameraInput>(
             translateCameraInputChannelIds, AzFramework::LookTranslation, AzFramework::TranslateOffsetOrbit);
 
@@ -337,12 +356,12 @@ namespace SandboxEditor
             AZ::TransformBus::EventResult(worldFromLocal, viewEntityId, &AZ::TransformBus::Events::GetWorldTM);
 
             AtomToolsFramework::ModularViewportCameraControllerRequestBus::Event(
-                m_viewportId, &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::SetReferenceFrame, worldFromLocal);
+                m_viewportId, &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::StartTrackingTransform, worldFromLocal);
         }
         else
         {
             AtomToolsFramework::ModularViewportCameraControllerRequestBus::Event(
-                m_viewportId, &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::ClearReferenceFrame);
+                m_viewportId, &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::StopTrackingTransform);
         }
     }
 

+ 16 - 72
Code/Editor/EditorViewportWidget.cpp

@@ -299,13 +299,9 @@ AzToolsFramework::ViewportInteraction::MousePick EditorViewportWidget::BuildMous
 {
     AzToolsFramework::ViewportInteraction::MousePick mousePick;
     mousePick.m_screenCoordinates = AzToolsFramework::ViewportInteraction::ScreenPointFromQPoint(point);
-    if (const auto& ray = m_renderViewport->ViewportScreenToWorldRay(mousePick.m_screenCoordinates);
-        ray.has_value())
-    {
-        mousePick.m_rayOrigin = ray.value().origin;
-        mousePick.m_rayDirection = ray.value().direction;
-    }
-
+    const auto[origin, direction] = m_renderViewport->ViewportScreenToWorldRay(mousePick.m_screenCoordinates);
+    mousePick.m_rayOrigin = origin;
+    mousePick.m_rayDirection = direction;
     return mousePick;
 }
 
@@ -912,23 +908,6 @@ AZ::Vector3 EditorViewportWidget::PickTerrain(const AzFramework::ScreenPoint& po
     return LYVec3ToAZVec3(ViewToWorld(AzToolsFramework::ViewportInteraction::QPointFromScreenPoint(point), nullptr, true));
 }
 
-AZ::EntityId EditorViewportWidget::PickEntity(const AzFramework::ScreenPoint& point)
-{
-    AZ::EntityId entityId;
-    HitContext hitInfo;
-    hitInfo.view = this;
-    if (HitTest(AzToolsFramework::ViewportInteraction::QPointFromScreenPoint(point), hitInfo))
-    {
-        if (hitInfo.object && (hitInfo.object->GetType() == OBJTYPE_AZENTITY))
-        {
-            auto entityObject = static_cast<CComponentEntityObject*>(hitInfo.object);
-            entityId = entityObject->GetAssociatedEntityId();
-        }
-    }
-
-    return entityId;
-}
-
 float EditorViewportWidget::TerrainHeight(const AZ::Vector2& position)
 {
     return GetIEditor()->GetTerrainElevation(position.GetX(), position.GetY());
@@ -1653,16 +1632,15 @@ void EditorViewportWidget::RenderSelectedRegion()
 Vec3 EditorViewportWidget::WorldToView3D(const Vec3& wp, [[maybe_unused]] int nFlags) const
 {
     Vec3 out(0, 0, 0);
-    float x, y, z;
+    float x, y;
 
-    ProjectToScreen(wp.x, wp.y, wp.z, &x, &y, &z);
-    if (_finite(x) && _finite(y) && _finite(z))
+    ProjectToScreen(wp.x, wp.y, wp.z, &x, &y);
+    if (_finite(x) && _finite(y))
     {
         out.x = (x / 100) * m_rcClient.width();
         out.y = (y / 100) * m_rcClient.height();
         out.x /= static_cast<float>(QHighDpiScaling::factor(windowHandle()->screen()));
         out.y /= static_cast<float>(QHighDpiScaling::factor(windowHandle()->screen()));
-        out.z = z;
     }
     return out;
 }
@@ -1672,24 +1650,6 @@ QPoint EditorViewportWidget::WorldToView(const Vec3& wp) const
 {
     return AzToolsFramework::ViewportInteraction::QPointFromScreenPoint(m_renderViewport->ViewportWorldToScreen(LYVec3ToAZVec3(wp)));
 }
-//////////////////////////////////////////////////////////////////////////
-QPoint EditorViewportWidget::WorldToViewParticleEditor(const Vec3& wp, int width, int height) const
-{
-    QPoint p;
-    float x, y, z;
-
-    ProjectToScreen(wp.x, wp.y, wp.z, &x, &y, &z);
-    if (_finite(x) || _finite(y))
-    {
-        p.rx() = static_cast<int>((x / 100) * width);
-        p.ry() = static_cast<int>((y / 100) * height);
-    }
-    else
-    {
-        QPoint(0, 0);
-    }
-    return p;
-}
 
 //////////////////////////////////////////////////////////////////////////
 Vec3 EditorViewportWidget::ViewToWorld(
@@ -1705,20 +1665,16 @@ Vec3 EditorViewportWidget::ViewToWorld(
     AZ_UNUSED(collideWithObject);
 
     auto ray = m_renderViewport->ViewportScreenToWorldRay(AzToolsFramework::ViewportInteraction::ScreenPointFromQPoint(vp));
-    if (!ray.has_value())
-    {
-        return Vec3(0, 0, 0);
-    }
 
     const float maxDistance = 10000.f;
-    Vec3 v = AZVec3ToLYVec3(ray.value().direction) * maxDistance;
+    Vec3 v = AZVec3ToLYVec3(ray.direction) * maxDistance;
 
     if (!_finite(v.x) || !_finite(v.y) || !_finite(v.z))
     {
         return Vec3(0, 0, 0);
     }
 
-    Vec3 colp = AZVec3ToLYVec3(ray.value().origin) + 0.002f * v;
+    Vec3 colp = AZVec3ToLYVec3(ray.origin) + 0.002f * v;
 
     return colp;
 }
@@ -1757,21 +1713,19 @@ bool EditorViewportWidget::RayRenderMeshIntersection(IRenderMesh* pRenderMesh, c
     return bRes;*/
 }
 
-void EditorViewportWidget::UnProjectFromScreen(float sx, float sy, float sz, float* px, float* py, float* pz) const
+void EditorViewportWidget::UnProjectFromScreen(float sx, float sy, float* px, float* py, float* pz) const
 {
-    AZ::Vector3 wp;
-    wp = m_renderViewport->ViewportScreenToWorld(AzFramework::ScreenPoint{(int)sx, m_rcClient.bottom() - ((int)sy)}, sz).value_or(wp);
+    const AZ::Vector3 wp = m_renderViewport->ViewportScreenToWorld(AzFramework::ScreenPoint{(int)sx, m_rcClient.bottom() - ((int)sy)});
     *px = wp.GetX();
     *py = wp.GetY();
     *pz = wp.GetZ();
 }
 
-void EditorViewportWidget::ProjectToScreen(float ptx, float pty, float ptz, float* sx, float* sy, float* sz) const
+void EditorViewportWidget::ProjectToScreen(float ptx, float pty, float ptz, float* sx, float* sy) const
 {
     AzFramework::ScreenPoint screenPosition = m_renderViewport->ViewportWorldToScreen(AZ::Vector3{ptx, pty, ptz});
     *sx = static_cast<float>(screenPosition.m_x);
     *sy = static_cast<float>(screenPosition.m_y);
-    *sz = 0.f;
 }
 
 //////////////////////////////////////////////////////////////////////////
@@ -1781,32 +1735,22 @@ void EditorViewportWidget::ViewToWorldRay(const QPoint& vp, Vec3& raySrc, Vec3&
 
     Vec3 pos0, pos1;
     float wx, wy, wz;
-    UnProjectFromScreen(static_cast<float>(vp.x()), static_cast<float>(rc.bottom() - vp.y()), 0.0f, &wx, &wy, &wz);
-    if (!_finite(wx) || !_finite(wy) || !_finite(wz))
-    {
-        return;
-    }
-    if (fabs(wx) > 1000000 || fabs(wy) > 1000000 || fabs(wz) > 1000000)
-    {
-        return;
-    }
-    pos0(wx, wy, wz);
-    UnProjectFromScreen(static_cast<float>(vp.x()), static_cast<float>(rc.bottom() - vp.y()), 1.0f, &wx, &wy, &wz);
+    UnProjectFromScreen(static_cast<float>(vp.x()), static_cast<float>(rc.bottom() - vp.y()), &wx, &wy, &wz);
+
     if (!_finite(wx) || !_finite(wy) || !_finite(wz))
     {
         return;
     }
+
     if (fabs(wx) > 1000000 || fabs(wy) > 1000000 || fabs(wz) > 1000000)
     {
         return;
     }
-    pos1(wx, wy, wz);
 
-    Vec3 v = (pos1 - pos0);
-    v = v.GetNormalized();
+    pos0(wx, wy, wz);
 
     raySrc = pos0;
-    rayDir = v;
+    rayDir = (pos0 - AZVec3ToLYVec3(m_renderViewport->GetCameraState().m_position)).GetNormalized();
 }
 
 //////////////////////////////////////////////////////////////////////////

+ 2 - 4
Code/Editor/EditorViewportWidget.h

@@ -166,7 +166,6 @@ private:
         Qt::MouseButtons buttons, Qt::KeyboardModifiers modifiers, const QPoint& point) override;
     void SetViewportId(int id) override;
     QPoint WorldToView(const Vec3& wp) const override;
-    QPoint WorldToViewParticleEditor(const Vec3& wp, int width, int height) const override;
     Vec3 WorldToView3D(const Vec3& wp, int nFlags = 0) const override;
     Vec3 ViewToWorld(const QPoint& vp, bool* collideWithTerrain = nullptr, bool onlyTerrain = false, bool bSkipVegetation = false, bool bTestRenderMesh = false, bool* collideWithObject = nullptr) const override;
     void ViewToWorldRay(const QPoint& vp, Vec3& raySrc, Vec3& rayDir) const override;
@@ -208,7 +207,6 @@ private:
     void* GetSystemCursorConstraintWindow() const override;
 
     // AzToolsFramework::MainEditorViewportInteractionRequestBus overrides ...
-    AZ::EntityId PickEntity(const AzFramework::ScreenPoint& point) override;
     AZ::Vector3 PickTerrain(const AzFramework::ScreenPoint& point) override;
     float TerrainHeight(const AZ::Vector2& position) override;
     bool ShowingWorldSpace() override;
@@ -306,8 +304,8 @@ private:
     const DisplayContext& GetDisplayContext() const { return m_displayContext; }
     CBaseObject* GetCameraObject() const;
 
-    void UnProjectFromScreen(float sx, float sy, float sz, float* px, float* py, float* pz) const;
-    void ProjectToScreen(float ptx, float pty, float ptz, float* sx, float* sy, float* sz) const;
+    void UnProjectFromScreen(float sx, float sy, float* px, float* py, float* pz) const;
+    void ProjectToScreen(float ptx, float pty, float ptz, float* sx, float* sy) const;
 
     AZ::RPI::ViewPtr GetCurrentAtomView() const;
 

+ 0 - 1
Code/Editor/Include/IDisplayViewport.h

@@ -47,7 +47,6 @@ struct IDisplayViewport
     virtual const Matrix34& GetViewTM() const = 0;
     virtual const Matrix34& GetScreenTM() const = 0;
     virtual QPoint WorldToView(const Vec3& worldPoint) const = 0;
-    virtual QPoint WorldToViewParticleEditor(const Vec3& worldPoint, int width, int height) const = 0;
     virtual Vec3 WorldToView3D(const Vec3& worldPoint, int flags = 0) const = 0;
     virtual Vec3 ViewToWorld(const QPoint& vp, bool* collideWithTerrain = nullptr, bool onlyTerrain = false, bool bSkipVegetation = false, bool bTestRenderMesh = false, bool* collideWithObject = nullptr) const = 0;
     virtual void ViewToWorldRay(const QPoint& vp, Vec3& raySrc, Vec3& rayDir) const = 0;

+ 98 - 50
Code/Editor/Lib/Tests/Camera/test_EditorCamera.cpp

@@ -38,7 +38,9 @@ namespace UnitTest
         AzFramework::ViewportControllerListPtr m_controllerList;
         AZStd::unique_ptr<AZ::Entity> m_entity;
 
-        static const AzFramework::ViewportId TestViewportId;
+        static inline constexpr AzFramework::ViewportId TestViewportId = 2345;
+        static inline constexpr float HalfInterpolateToTransformDuration =
+            AtomToolsFramework::ModularViewportCameraControllerRequests::InterpolateToTransformDuration * 0.5f;
 
         void SetUp() override
         {
@@ -77,8 +79,6 @@ namespace UnitTest
         }
     };
 
-    const AzFramework::ViewportId EditorCameraFixture::TestViewportId = AzFramework::ViewportId(1337);
-
     TEST_F(EditorCameraFixture, ModularViewportCameraControllerReferenceFrameUpdatedWhenViewportEntityisChanged)
     {
         // Given
@@ -92,8 +92,8 @@ namespace UnitTest
             &Camera::EditorCameraNotificationBus::Events::OnViewportViewEntityChanged, m_entity->GetId());
 
         // ensure the viewport updates after the viewport view entity change
-        const float deltaTime = 1.0f / 60.0f;
-        m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(deltaTime), AZ::ScriptTimePoint() });
+        // note: do a large step to ensure smoothing finishes (e.g. not 1.0f/60.0f)
+        m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(2.0f), AZ::ScriptTimePoint() });
 
         // retrieve updated camera transform
         const AZ::Transform cameraTransform = m_cameraViewportContextView->GetCameraTransform();
@@ -103,61 +103,40 @@ namespace UnitTest
         EXPECT_THAT(cameraTransform, IsClose(entityTransform));
     }
 
-    TEST_F(EditorCameraFixture, ReferenceFrameRemainsIdentityAfterExternalCameraTransformChangeWhenNotSet)
-    {
-        // Given
-        m_cameraViewportContextView->SetCameraTransform(AZ::Transform::CreateTranslation(AZ::Vector3(10.0f, 20.0f, 30.0f)));
-
-        // When
-        AZ::Transform referenceFrame = AZ::Transform::CreateTranslation(AZ::Vector3(1.0f, 2.0f, 3.0f));
-        AtomToolsFramework::ModularViewportCameraControllerRequestBus::EventResult(
-            referenceFrame, TestViewportId, &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::GetReferenceFrame);
-
-        // Then
-        // reference frame is still the identity
-        EXPECT_THAT(referenceFrame, IsClose(AZ::Transform::CreateIdentity()));
-    }
-
-    TEST_F(EditorCameraFixture, ExternalCameraTransformChangeWhenReferenceFrameIsSetUpdatesReferenceFrame)
+    TEST_F(EditorCameraFixture, TrackingTransformIsTrueAfterTransformIsTracked)
     {
-        // Given
+        // Given/When
         const AZ::Transform referenceFrame = AZ::Transform::CreateFromQuaternionAndTranslation(
             AZ::Quaternion::CreateRotationX(AZ::DegToRad(90.0f)), AZ::Vector3(1.0f, 2.0f, 3.0f));
         AtomToolsFramework::ModularViewportCameraControllerRequestBus::Event(
-            TestViewportId, &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::SetReferenceFrame, referenceFrame);
-
-        const AZ::Transform nextTransform = AZ::Transform::CreateTranslation(AZ::Vector3(10.0f, 20.0f, 30.0f));
-        m_cameraViewportContextView->SetCameraTransform(nextTransform);
+            TestViewportId, &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::StartTrackingTransform, referenceFrame);
 
-        // When
-        AZ::Transform currentReferenceFrame = AZ::Transform::CreateTranslation(AZ::Vector3(1.0f, 2.0f, 3.0f));
+        bool trackingTransform = false;
         AtomToolsFramework::ModularViewportCameraControllerRequestBus::EventResult(
-            currentReferenceFrame, TestViewportId,
-            &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::GetReferenceFrame);
+            trackingTransform, TestViewportId, &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::IsTrackingTransform);
 
         // Then
-        EXPECT_THAT(currentReferenceFrame, IsClose(nextTransform));
+        EXPECT_THAT(trackingTransform, ::testing::IsTrue());
     }
 
-    TEST_F(EditorCameraFixture, ReferenceFrameReturnedToIdentityAfterClear)
+    TEST_F(EditorCameraFixture, TrackingTransformIsFalseAfterTransformIsStoppedBeingTracked)
     {
         // Given
         const AZ::Transform referenceFrame = AZ::Transform::CreateFromQuaternionAndTranslation(
             AZ::Quaternion::CreateRotationX(AZ::DegToRad(90.0f)), AZ::Vector3(1.0f, 2.0f, 3.0f));
         AtomToolsFramework::ModularViewportCameraControllerRequestBus::Event(
-            TestViewportId, &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::SetReferenceFrame, referenceFrame);
+            TestViewportId, &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::StartTrackingTransform, referenceFrame);
 
         // When
         AtomToolsFramework::ModularViewportCameraControllerRequestBus::Event(
-            TestViewportId, &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::ClearReferenceFrame);
+            TestViewportId, &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::StopTrackingTransform);
 
-        AZ::Transform currentReferenceFrame = AZ::Transform::CreateTranslation(AZ::Vector3(1.0f, 2.0f, 3.0f));
+        // Then
+        bool trackingTransform = false;
         AtomToolsFramework::ModularViewportCameraControllerRequestBus::EventResult(
-            currentReferenceFrame, TestViewportId,
-            &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::GetReferenceFrame);
+            trackingTransform, TestViewportId, &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::IsTrackingTransform);
 
-        // Then
-        EXPECT_THAT(currentReferenceFrame, IsClose(AZ::Transform::CreateIdentity()));
+        EXPECT_THAT(trackingTransform, ::testing::IsFalse());
     }
 
     TEST_F(EditorCameraFixture, InterpolateToTransform)
@@ -170,8 +149,10 @@ namespace UnitTest
             transformToInterpolateTo);
 
         // simulate interpolation
-        m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(0.5f), AZ::ScriptTimePoint() });
-        m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(0.5f), AZ::ScriptTimePoint() });
+        m_controllerList->UpdateViewport(
+            { TestViewportId, AzFramework::FloatSeconds(HalfInterpolateToTransformDuration), AZ::ScriptTimePoint() });
+        m_controllerList->UpdateViewport(
+            { TestViewportId, AzFramework::FloatSeconds(HalfInterpolateToTransformDuration), AZ::ScriptTimePoint() });
 
         const auto finalTransform = m_cameraViewportContextView->GetCameraTransform();
 
@@ -185,7 +166,7 @@ namespace UnitTest
         const AZ::Transform referenceFrame = AZ::Transform::CreateFromQuaternionAndTranslation(
             AZ::Quaternion::CreateRotationX(AZ::DegToRad(90.0f)), AZ::Vector3(1.0f, 2.0f, 3.0f));
         AtomToolsFramework::ModularViewportCameraControllerRequestBus::Event(
-            TestViewportId, &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::SetReferenceFrame, referenceFrame);
+            TestViewportId, &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::StartTrackingTransform, referenceFrame);
 
         AZ::Transform transformToInterpolateTo = AZ::Transform::CreateFromQuaternionAndTranslation(
             AZ::Quaternion::CreateRotationZ(AZ::DegToRad(90.0f)), AZ::Vector3(20.0f, 40.0f, 60.0f));
@@ -196,19 +177,86 @@ namespace UnitTest
             transformToInterpolateTo);
 
         // simulate interpolation
-        m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(0.5f), AZ::ScriptTimePoint() });
-        m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(0.5f), AZ::ScriptTimePoint() });
-
-        AZ::Transform currentReferenceFrame = AZ::Transform::CreateTranslation(AZ::Vector3(1.0f, 2.0f, 3.0f));
-        AtomToolsFramework::ModularViewportCameraControllerRequestBus::EventResult(
-            currentReferenceFrame, TestViewportId,
-            &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::GetReferenceFrame);
+        m_controllerList->UpdateViewport(
+            { TestViewportId, AzFramework::FloatSeconds(HalfInterpolateToTransformDuration), AZ::ScriptTimePoint() });
+        m_controllerList->UpdateViewport(
+            { TestViewportId, AzFramework::FloatSeconds(HalfInterpolateToTransformDuration), AZ::ScriptTimePoint() });
 
         const auto finalTransform = m_cameraViewportContextView->GetCameraTransform();
 
         // Then
         EXPECT_THAT(finalTransform, IsClose(transformToInterpolateTo));
-        EXPECT_THAT(currentReferenceFrame, IsClose(AZ::Transform::CreateIdentity()));
+    }
+
+    TEST_F(EditorCameraFixture, BeginningCameraInterpolationReturnsTrue)
+    {
+        // Given/When
+        bool interpolationBegan = false;
+        AtomToolsFramework::ModularViewportCameraControllerRequestBus::EventResult(
+            interpolationBegan, TestViewportId,
+            &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::InterpolateToTransform,
+            AZ::Transform::CreateTranslation(AZ::Vector3(10.0f, 10.0f, 10.0f)));
+
+        // Then
+        EXPECT_THAT(interpolationBegan, ::testing::IsTrue());
+    }
+
+    TEST_F(EditorCameraFixture, CameraInterpolationDoesNotBeginDuringAnExistingInterpolation)
+    {
+        // Given/When
+        bool initialInterpolationBegan = false;
+        AtomToolsFramework::ModularViewportCameraControllerRequestBus::EventResult(
+            initialInterpolationBegan, TestViewportId,
+            &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::InterpolateToTransform,
+            AZ::Transform::CreateTranslation(AZ::Vector3(10.0f, 10.0f, 10.0f)));
+
+        m_controllerList->UpdateViewport(
+            { TestViewportId, AzFramework::FloatSeconds(HalfInterpolateToTransformDuration), AZ::ScriptTimePoint() });
+
+        bool nextInterpolationBegan = true;
+        AtomToolsFramework::ModularViewportCameraControllerRequestBus::EventResult(
+            nextInterpolationBegan, TestViewportId,
+            &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::InterpolateToTransform,
+            AZ::Transform::CreateTranslation(AZ::Vector3(10.0f, 10.0f, 10.0f)));
+
+        bool interpolating = false;
+        AtomToolsFramework::ModularViewportCameraControllerRequestBus::EventResult(
+            interpolating, TestViewportId, &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::IsInterpolating);
+
+        // Then
+        EXPECT_THAT(initialInterpolationBegan, ::testing::IsTrue());
+        EXPECT_THAT(nextInterpolationBegan, ::testing::IsFalse());
+        EXPECT_THAT(interpolating, ::testing::IsTrue());
+    }
+
+    TEST_F(EditorCameraFixture, CameraInterpolationCanBeginAfterAnInterpolationCompletes)
+    {
+        // Given/When
+        bool initialInterpolationBegan = false;
+        AtomToolsFramework::ModularViewportCameraControllerRequestBus::EventResult(
+            initialInterpolationBegan, TestViewportId,
+            &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::InterpolateToTransform,
+            AZ::Transform::CreateTranslation(AZ::Vector3(10.0f, 10.0f, 10.0f)));
+
+        m_controllerList->UpdateViewport(
+            { TestViewportId,
+              AzFramework::FloatSeconds(AtomToolsFramework::ModularViewportCameraControllerRequests::InterpolateToTransformDuration + 0.5f),
+              AZ::ScriptTimePoint() });
+
+        bool interpolating = true;
+        AtomToolsFramework::ModularViewportCameraControllerRequestBus::EventResult(
+            interpolating, TestViewportId, &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::IsInterpolating);
+
+        bool nextInterpolationBegan = false;
+        AtomToolsFramework::ModularViewportCameraControllerRequestBus::EventResult(
+            nextInterpolationBegan, TestViewportId,
+            &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::InterpolateToTransform,
+            AZ::Transform::CreateTranslation(AZ::Vector3(10.0f, 10.0f, 10.0f)));
+
+        // Then
+        EXPECT_THAT(initialInterpolationBegan, ::testing::IsTrue());
+        EXPECT_THAT(interpolating, ::testing::IsFalse());
+        EXPECT_THAT(nextInterpolationBegan, ::testing::IsTrue());
     }
 } // namespace UnitTest
 

+ 12 - 3
Code/Editor/Lib/Tests/test_ModularViewportCameraController.cpp

@@ -74,7 +74,7 @@ namespace UnitTest
     class ModularViewportCameraControllerFixture : public AllocatorsTestFixture
     {
     public:
-        static const AzFramework::ViewportId TestViewportId;
+        static inline constexpr AzFramework::ViewportId TestViewportId = 1234;
 
         void SetUp() override
         {
@@ -146,6 +146,17 @@ namespace UnitTest
             controller->SetCameraPropsBuilderCallback(
                 [](AzFramework::CameraProps& cameraProps)
                 {
+                    // note: rotateSmoothness is also used for roll (not related to camera input directly)
+                    cameraProps.m_rotateSmoothnessFn = []
+                    {
+                        return 5.0f;
+                    };
+
+                    cameraProps.m_translateSmoothnessFn = []
+                    {
+                        return 5.0f;
+                    };
+
                     cameraProps.m_rotateSmoothingEnabledFn = []
                     {
                         return false;
@@ -209,8 +220,6 @@ namespace UnitTest
         AZStd::unique_ptr<SandboxEditor::EditorModularViewportCameraComposer> m_editorModularViewportCameraComposer;
     };
 
-    const AzFramework::ViewportId ModularViewportCameraControllerFixture::TestViewportId = AzFramework::ViewportId(0);
-
     TEST_F(ModularViewportCameraControllerFixture, MouseMovementDoesNotAccumulateExcessiveDriftInModularViewportCameraWithVaryingDeltaTime)
     {
         SandboxEditor::SetCameraCaptureCursorForLook(false);

+ 6 - 0
Code/Editor/Plugins/ComponentEntityEditorPlugin/SandboxIntegration.cpp

@@ -1675,6 +1675,12 @@ void SandboxIntegrationManager::GoToEntitiesInViewports(const AzToolsFramework::
         if (auto viewportContext = viewportContextManager->GetViewportContextById(viewIndex))
         {
             const AZ::Transform cameraTransform = viewportContext->GetCameraTransform();
+            // do not attempt to interpolate to where we currently are
+            if (cameraTransform.GetTranslation().IsClose(center))
+            {
+                continue;
+            }
+
             const AZ::Vector3 forward = (center - cameraTransform.GetTranslation()).GetNormalized();
 
             // move camera 25% further back than required

+ 1 - 1
Code/Editor/StartupLogoDialog.ui

@@ -103,7 +103,7 @@
          <item>
           <widget class="QLabel" name="label_2">
            <property name="text">
-            <string>General Availability</string>
+            <string>Stable 21.11</string>
            </property>
           </widget>
          </item>

+ 21 - 16
Code/Editor/ViewportManipulatorController.cpp

@@ -8,13 +8,15 @@
 
 #include "ViewportManipulatorController.h"
 
-#include <AzToolsFramework/Manipulators/ManipulatorManager.h>
-#include <AzToolsFramework/ViewportSelection/EditorInteractionSystemViewportSelectionRequestBus.h>
-#include <AzFramework/Input/Devices/Mouse/InputDeviceMouse.h>
-#include <AzFramework/Input/Devices/Keyboard/InputDeviceKeyboard.h>
+#include <AzCore/Script/ScriptTimePoint.h>
 #include <AzFramework/Input/Buses/Requests/InputSystemCursorRequestBus.h>
+#include <AzFramework/Input/Devices/Keyboard/InputDeviceKeyboard.h>
+#include <AzFramework/Input/Devices/Mouse/InputDeviceMouse.h>
 #include <AzFramework/Viewport/ScreenGeometry.h>
-#include <AzCore/Script/ScriptTimePoint.h>
+#include <AzFramework/Viewport/ViewportScreen.h>
+#include <AzToolsFramework/Manipulators/ManipulatorManager.h>
+#include <AzToolsFramework/ViewportSelection/EditorInteractionSystemViewportSelectionRequestBus.h>
+#include <AzToolsFramework/ViewportSelection/EditorSelectionUtil.h>
 
 #include <QApplication>
 
@@ -87,8 +89,14 @@ namespace SandboxEditor
         }
 
         using InteractionBus = AzToolsFramework::EditorInteractionSystemViewportSelectionRequestBus;
-        using namespace AzToolsFramework::ViewportInteraction;
         using AzFramework::InputChannel;
+        using AzToolsFramework::ViewportInteraction::KeyboardModifier;
+        using AzToolsFramework::ViewportInteraction::MouseButton;
+        using AzToolsFramework::ViewportInteraction::MouseEvent;
+        using AzToolsFramework::ViewportInteraction::MouseInteraction;
+        using AzToolsFramework::ViewportInteraction::MouseInteractionEvent;
+        using AzToolsFramework::ViewportInteraction::ProjectedViewportRay;
+        using AzToolsFramework::ViewportInteraction::ViewportInteractionRequestBus;
 
         bool interactionHandled = false;
         float wheelDelta = 0.0f;
@@ -117,16 +125,13 @@ namespace SandboxEditor
                     aznumeric_cast<int>(position->m_normalizedPosition.GetX() * windowSize.m_width),
                     aznumeric_cast<int>(position->m_normalizedPosition.GetY() * windowSize.m_height));
 
-                m_mouseInteraction.m_mousePick.m_screenCoordinates = screenPoint;
-                AZStd::optional<ProjectedViewportRay> ray;
+                ProjectedViewportRay ray{};
                 ViewportInteractionRequestBus::EventResult(
                     ray, GetViewportId(), &ViewportInteractionRequestBus::Events::ViewportScreenToWorldRay, screenPoint);
 
-                if (ray.has_value())
-                {
-                    m_mouseInteraction.m_mousePick.m_rayOrigin = ray.value().origin;
-                    m_mouseInteraction.m_mousePick.m_rayDirection = ray.value().direction;
-                }
+                m_mouseInteraction.m_mousePick.m_rayOrigin = ray.origin;
+                m_mouseInteraction.m_mousePick.m_rayDirection = ray.direction;
+                m_mouseInteraction.m_mousePick.m_screenCoordinates = screenPoint;
             }
 
             eventType = MouseEvent::Move;
@@ -160,8 +165,8 @@ namespace SandboxEditor
             else if (state == InputChannel::State::Ended)
             {
                 // If we've actually logged a mouse down event, forward a mouse up event.
-                // This prevents corner cases like the context menu thinking it should be opened even though no one clicked in this viewport,
-                // due to RenderViewportWidget ensuring all controllers get InputChannel::State::Ended events.
+                // This prevents corner cases like the context menu thinking it should be opened even though no one clicked in this
+                // viewport, due to RenderViewportWidget ensuring all controllers get InputChannel::State::Ended events.
                 if (m_mouseInteraction.m_mouseButtons.m_mouseButtons & mouseButtonValue)
                 {
                     // Erase the button from our state if we're done processing events.
@@ -259,4 +264,4 @@ namespace SandboxEditor
         const double doubleClickThresholdMilliseconds = qApp->doubleClickInterval();
         return (m_curTime.GetMilliseconds() - clickIt->second.GetMilliseconds()) < doubleClickThresholdMilliseconds;
     }
-} //namespace SandboxEditor
+} // namespace SandboxEditor

+ 5 - 1
Code/Framework/AzCore/AzCore/Asset/AssetManager.cpp

@@ -1677,9 +1677,13 @@ namespace AZ
                 // they will trigger a ReleaseAsset call sometime after the AssetManager has begun to shut down, which can lead to
                 // race conditions.
 
+                // Make sure the streamer request is removed first before the asset is released
+                // If the asset is released first it could lead to a race condition where another thread starts loading the asset
+                // again and attempts to add a new streamer request with the same ID before the old one has been removed, causing
+                // that load request to fail
+                RemoveActiveStreamerRequest(assetId);
                 weakAsset = {};
                 loadingAsset.Reset();
-                RemoveActiveStreamerRequest(assetId);
             };
 
             auto&& [deadline, priority] = GetEffectiveDeadlineAndPriority(*handler, asset.GetType(), loadParams);

+ 4 - 3
Code/Framework/AzCore/AzCore/Jobs/JobManagerComponent.cpp

@@ -56,11 +56,12 @@ namespace AZ
         int numberOfWorkerThreads = m_numberOfWorkerThreads;
         if (numberOfWorkerThreads <= 0) // spawn default number of threads
         {
+        #if (AZ_TRAIT_THREAD_NUM_JOB_MANAGER_WORKER_THREADS)
+            numberOfWorkerThreads = AZ_TRAIT_THREAD_NUM_JOB_MANAGER_WORKER_THREADS;
+        #else
             uint32_t scaledHardwareThreads = Threading::CalcNumWorkerThreads(cl_jobThreadsConcurrencyRatio, cl_jobThreadsMinNumber, cl_jobThreadsNumReserved);
             numberOfWorkerThreads = AZ::GetMin(static_cast<unsigned int>(desc.m_workerThreads.capacity()), scaledHardwareThreads);
-        #if (AZ_TRAIT_MAX_JOB_MANAGER_WORKER_THREADS)
-            numberOfWorkerThreads = AZ::GetMin(numberOfWorkerThreads, AZ_TRAIT_MAX_JOB_MANAGER_WORKER_THREADS);
-        #endif // (AZ_TRAIT_MAX_JOB_MANAGER_WORKER_THREADS)
+        #endif // (AZ_TRAIT_THREAD_NUM_JOB_MANAGER_WORKER_THREADS)
         }
 
         threadDesc.m_cpuId = AFFINITY_MASK_USERTHREADS;

+ 2 - 0
Code/Framework/AzCore/AzCore/Serialization/Json/JsonSystemComponent.cpp

@@ -104,6 +104,8 @@ namespace AZ
                 ->HandlesType<AZStd::variant>();
             jsonContext->Serializer<JsonOptionalSerializer>()
                 ->HandlesType<AZStd::optional>();
+            jsonContext->Serializer<JsonBitsetSerializer>()
+                ->HandlesType<AZStd::bitset>();
 
             MathReflect(jsonContext);
         }

+ 7 - 0
Code/Framework/AzCore/AzCore/Serialization/Json/UnsupportedTypesSerializer.cpp

@@ -15,6 +15,7 @@ namespace AZ
     AZ_CLASS_ALLOCATOR_IMPL(JsonAnySerializer, SystemAllocator, 0);
     AZ_CLASS_ALLOCATOR_IMPL(JsonVariantSerializer, SystemAllocator, 0);
     AZ_CLASS_ALLOCATOR_IMPL(JsonOptionalSerializer, SystemAllocator, 0);
+    AZ_CLASS_ALLOCATOR_IMPL(JsonBitsetSerializer, SystemAllocator, 0);
 
     JsonSerializationResult::Result JsonUnsupportedTypesSerializer::Load(void*, const Uuid&, const rapidjson::Value&,
         JsonDeserializerContext& context)
@@ -49,4 +50,10 @@ namespace AZ
         return "The Json Serialization doesn't support AZStd::optional by design. No JSON format has yet been found that wasn't deemed too "
                "complex or overly verbose.";
     }
+
+    AZStd::string_view JsonBitsetSerializer::GetMessage() const
+    {
+        return "The Json Serialization doesn't support AZStd::bitset by design. No JSON format has yet been found that is content creator "
+               "friendly i.e., easy to comprehend the intent.";
+    }
 } // namespace AZ

+ 10 - 0
Code/Framework/AzCore/AzCore/Serialization/Json/UnsupportedTypesSerializer.h

@@ -65,4 +65,14 @@ namespace AZ
     protected:
         AZStd::string_view GetMessage() const override;
     };
+
+    class JsonBitsetSerializer : public JsonUnsupportedTypesSerializer
+    {
+    public:
+        AZ_RTTI(JsonBitsetSerializer, "{10CE969D-D69E-4B3F-8593-069736F8F705}", JsonUnsupportedTypesSerializer);
+        AZ_CLASS_ALLOCATOR_DECL;
+
+    protected:
+        AZStd::string_view GetMessage() const override;
+    };
 } // namespace AZ

+ 6 - 1
Code/Framework/AzCore/AzCore/Task/TaskGraphSystemComponent.cpp

@@ -30,8 +30,13 @@ namespace AZ
 
         if (Interface<TaskGraphActiveInterface>::Get() == nullptr)
         {
+        #if (AZ_TRAIT_THREAD_NUM_TASK_GRAPH_WORKER_THREADS)
+            const uint32_t numberOfWorkerThreads = AZ_TRAIT_THREAD_NUM_TASK_GRAPH_WORKER_THREADS;
+        #else
+            const uint32_t numberOfWorkerThreads = Threading::CalcNumWorkerThreads(cl_taskGraphThreadsConcurrencyRatio, cl_taskGraphThreadsMinNumber, cl_taskGraphThreadsNumReserved);
+        #endif // (AZ_TRAIT_THREAD_NUM_TASK_GRAPH_WORKER_THREADS)
             Interface<TaskGraphActiveInterface>::Register(this); // small window that another thread can try to use taskgraph between this line and the set instance.
-            m_taskExecutor = aznew TaskExecutor(Threading::CalcNumWorkerThreads(cl_taskGraphThreadsConcurrencyRatio, cl_taskGraphThreadsMinNumber, cl_taskGraphThreadsNumReserved));
+            m_taskExecutor = aznew TaskExecutor(numberOfWorkerThreads);
             TaskExecutor::SetInstance(m_taskExecutor);
         }
     }

+ 0 - 1
Code/Framework/AzCore/Platform/Android/AzCore/AzCore_Traits_Android.h

@@ -75,7 +75,6 @@
 #define AZ_TRAIT_HEAPSCHEMA_COMPILE_MALLINFO 0
 #define AZ_TRAIT_IS_ABS_PATH_IF_COLON_FOUND_ANYWHERE 0
 #define AZ_TRAIT_JSON_CLANG_IGNORE_UNKNOWN_WARNING 0
-#define AZ_TRAIT_MAX_JOB_MANAGER_WORKER_THREADS 0
 #define AZ_TRAIT_PERF_MEMORYBENCHMARK_IS_AVAILABLE 0
 #define AZ_TRAIT_PUMP_SYSTEM_EVENTS_WHILE_LOADING 0
 #define AZ_TRAIT_PUMP_SYSTEM_EVENTS_WHILE_LOADING_INTERVAL_MS 0

+ 0 - 1
Code/Framework/AzCore/Platform/Linux/AzCore/AzCore_Traits_Linux.h

@@ -75,7 +75,6 @@
 #define AZ_TRAIT_HEAPSCHEMA_COMPILE_MALLINFO 1
 #define AZ_TRAIT_IS_ABS_PATH_IF_COLON_FOUND_ANYWHERE 0
 #define AZ_TRAIT_JSON_CLANG_IGNORE_UNKNOWN_WARNING 0
-#define AZ_TRAIT_MAX_JOB_MANAGER_WORKER_THREADS 0
 #define AZ_TRAIT_PERF_MEMORYBENCHMARK_IS_AVAILABLE 0
 #define AZ_TRAIT_PUMP_SYSTEM_EVENTS_WHILE_LOADING 0
 #define AZ_TRAIT_PUMP_SYSTEM_EVENTS_WHILE_LOADING_INTERVAL_MS 0

+ 0 - 1
Code/Framework/AzCore/Platform/Mac/AzCore/AzCore_Traits_Mac.h

@@ -75,7 +75,6 @@
 #define AZ_TRAIT_HEAPSCHEMA_COMPILE_MALLINFO 1
 #define AZ_TRAIT_IS_ABS_PATH_IF_COLON_FOUND_ANYWHERE 0
 #define AZ_TRAIT_JSON_CLANG_IGNORE_UNKNOWN_WARNING 0
-#define AZ_TRAIT_MAX_JOB_MANAGER_WORKER_THREADS 0
 #define AZ_TRAIT_PERF_MEMORYBENCHMARK_IS_AVAILABLE 0
 #define AZ_TRAIT_PUMP_SYSTEM_EVENTS_WHILE_LOADING 0
 #define AZ_TRAIT_PUMP_SYSTEM_EVENTS_WHILE_LOADING_INTERVAL_MS 0

+ 0 - 1
Code/Framework/AzCore/Platform/Windows/AzCore/AzCore_Traits_Windows.h

@@ -75,7 +75,6 @@
 #define AZ_TRAIT_HEAPSCHEMA_COMPILE_MALLINFO 1
 #define AZ_TRAIT_IS_ABS_PATH_IF_COLON_FOUND_ANYWHERE 0
 #define AZ_TRAIT_JSON_CLANG_IGNORE_UNKNOWN_WARNING 1
-#define AZ_TRAIT_MAX_JOB_MANAGER_WORKER_THREADS 0
 #define AZ_TRAIT_PERF_MEMORYBENCHMARK_IS_AVAILABLE 1
 #define AZ_TRAIT_PUMP_SYSTEM_EVENTS_WHILE_LOADING 0
 #define AZ_TRAIT_PUMP_SYSTEM_EVENTS_WHILE_LOADING_INTERVAL_MS 0

+ 0 - 1
Code/Framework/AzCore/Platform/iOS/AzCore/AzCore_Traits_iOS.h

@@ -76,7 +76,6 @@
 #define AZ_TRAIT_HEAPSCHEMA_COMPILE_MALLINFO 1
 #define AZ_TRAIT_IS_ABS_PATH_IF_COLON_FOUND_ANYWHERE 0
 #define AZ_TRAIT_JSON_CLANG_IGNORE_UNKNOWN_WARNING 0
-#define AZ_TRAIT_MAX_JOB_MANAGER_WORKER_THREADS 0
 #define AZ_TRAIT_PERF_MEMORYBENCHMARK_IS_AVAILABLE 0
 #define AZ_TRAIT_PUMP_SYSTEM_EVENTS_WHILE_LOADING 0
 #define AZ_TRAIT_PUMP_SYSTEM_EVENTS_WHILE_LOADING_INTERVAL_MS 0

+ 3 - 3
Code/Framework/AzCore/Tests/Asset/AssetManagerLoadingTests.cpp

@@ -652,7 +652,7 @@ namespace UnitTest
             threads.emplace_back([this, &threadCount, &cv, assetUuid]() {
                 bool checkLoaded = true;
 
-                for (int i = 0; i < 5000; i++)
+                for (int i = 0; i < 1000; i++)
                 {
                     Asset<AssetWithAssetReference> asset1 =
                         m_testAssetManager->GetAsset(assetUuid, azrtti_typeid<AssetWithAssetReference>(), AZ::Data::AssetLoadBehavior::PreLoad);
@@ -678,7 +678,7 @@ namespace UnitTest
         while (threadCount > 0 && !timedOut)
         {
             AZStd::unique_lock<AZStd::mutex> lock(mutex);
-            timedOut = (AZStd::cv_status::timeout == cv.wait_until(lock, AZStd::chrono::system_clock::now() + DefaultTimeoutSeconds * 20000));
+            timedOut = (AZStd::cv_status::timeout == cv.wait_until(lock, AZStd::chrono::system_clock::now() + DefaultTimeoutSeconds));
         }
 
         ASSERT_EQ(threadCount, 0) << "Thread count is non-zero, a thread has likely deadlocked.  Test will not shut down cleanly.";
@@ -1190,7 +1190,7 @@ namespace UnitTest
 #if AZ_TRAIT_DISABLE_FAILED_ASSET_MANAGER_TESTS
     TEST_F(AssetJobsFloodTest, DISABLED_ContainerFilterTest_ContainersWithAndWithoutFiltering_Success)
 #else
-    TEST_F(AssetJobsFloodTest, DISABLED_ContainerFilterTest_ContainersWithAndWithoutFiltering_Success)
+    TEST_F(AssetJobsFloodTest, ContainerFilterTest_ContainersWithAndWithoutFiltering_Success)
 #endif // !AZ_TRAIT_DISABLE_FAILED_ASSET_MANAGER_TESTS
     {
         m_assetHandlerAndCatalog->AssetCatalogRequestBus::Handler::BusConnect();

+ 4 - 0
Code/Framework/AzFramework/AzFramework/Archive/Archive.cpp

@@ -1241,6 +1241,10 @@ namespace AZ::IO
 
             m_arrZips.insert(revItZip.base(), desc);
 
+            // This lock is for m_arrZips.
+            // Unlock it now because the modification is complete, and events responding to this signal
+            // will attempt to lock the same mutex, causing the application to lock up.
+            lock.unlock();
             m_levelOpenEvent.Signal(levelDirs);
         }
 

+ 53 - 28
Code/Framework/AzFramework/AzFramework/Viewport/CameraInput.cpp

@@ -94,27 +94,27 @@ namespace AzFramework
         float y;
         float z;
 
-        // 2.4 Factor as RzRyRx
-        if (orientation.GetElement(2, 0) < 1.0f)
+        // 2.5 Factor as RzRxRy
+        if (orientation.GetElement(2, 1) < 1.0f)
         {
-            if (orientation.GetElement(2, 0) > -1.0f)
+            if (orientation.GetElement(2, 1) > -1.0f)
             {
-                x = AZStd::atan2(orientation.GetElement(2, 1), orientation.GetElement(2, 2));
-                y = AZStd::asin(-orientation.GetElement(2, 0));
-                z = AZStd::atan2(orientation.GetElement(1, 0), orientation.GetElement(0, 0));
+                x = AZStd::asin(orientation.GetElement(2, 1));
+                y = AZStd::atan2(-orientation.GetElement(2, 0), orientation.GetElement(2, 2));
+                z = AZStd::atan2(-orientation.GetElement(0, 1), orientation.GetElement(1, 1));
             }
             else
             {
-                x = 0.0f;
-                y = AZ::Constants::Pi * 0.5f;
-                z = -AZStd::atan2(-orientation.GetElement(2, 1), orientation.GetElement(1, 1));
+                x = -AZ::Constants::Pi * 0.5f;
+                y = 0.0f;
+                z = -AZStd::atan2(orientation.GetElement(0, 2), orientation.GetElement(0, 0));
             }
         }
         else
         {
-            x = 0.0f;
-            y = -AZ::Constants::Pi * 0.5f;
-            z = AZStd::atan2(-orientation.GetElement(1, 2), orientation.GetElement(1, 1));
+            x = AZ::Constants::Pi * 0.5f;
+            y = 0.0f;
+            z = AZStd::atan2(orientation.GetElement(0, 2), orientation.GetElement(0, 0));
         }
 
         return { x, y, z };
@@ -122,14 +122,36 @@ namespace AzFramework
 
     void UpdateCameraFromTransform(Camera& camera, const AZ::Transform& transform)
     {
-        const auto eulerAngles = AzFramework::EulerAngles(AZ::Matrix3x3::CreateFromTransform(transform));
+        UpdateCameraFromTranslationAndRotation(
+            camera, transform.GetTranslation(), AzFramework::EulerAngles(AZ::Matrix3x3::CreateFromTransform(transform)));
+    }
 
+    void UpdateCameraFromTranslationAndRotation(Camera& camera, const AZ::Vector3& translation, const AZ::Vector3& eulerAngles)
+    {
         camera.m_pitch = eulerAngles.GetX();
         camera.m_yaw = eulerAngles.GetZ();
-        camera.m_pivot = transform.GetTranslation();
+        camera.m_pivot = translation;
         camera.m_offset = AZ::Vector3::CreateZero();
     }
 
+    float SmoothValueTime(const float smoothness, float deltaTime)
+    {
+        // note: the math for the lerp smoothing implementation for camera rotation and translation was inspired by this excellent
+        // article by Scott Lembcke: https://www.gamasutra.com/blogs/ScottLembcke/20180404/316046/Improved_Lerp_Smoothing.php
+        const float rate = AZStd::exp2(smoothness);
+        return AZStd::exp2(-rate * deltaTime);
+    }
+
+    float SmoothValue(const float target, const float current, const float time)
+    {
+        return AZ::Lerp(target, current, time);
+    }
+
+    float SmoothValue(const float target, const float current, const float smoothness, const float deltaTime)
+    {
+        return SmoothValue(target, current, SmoothValueTime(smoothness, deltaTime));
+    }
+
     bool CameraSystem::HandleEvents(const InputEvent& event)
     {
         if (const auto& cursor = AZStd::get_if<CursorEvent>(&event))
@@ -291,6 +313,11 @@ namespace AzFramework
         {
             return false;
         };
+
+        m_constrainPitch = []() constexpr
+        {
+            return true;
+        };
     }
 
     bool RotateCameraInput::HandleEvents(const InputEvent& event, const ScreenVector& cursorDelta, [[maybe_unused]] const float scrollDelta)
@@ -312,7 +339,10 @@ namespace AzFramework
         nextCamera.m_yaw -= float(cursorDelta.m_x) * rotateSpeed * Invert(m_invertYawFn());
 
         nextCamera.m_yaw = WrapYawRotation(nextCamera.m_yaw);
-        nextCamera.m_pitch = ClampPitchRotation(nextCamera.m_pitch);
+        if (m_constrainPitch())
+        {
+            nextCamera.m_pitch = ClampPitchRotation(nextCamera.m_pitch);
+        }
 
         return nextCamera;
     }
@@ -726,14 +756,14 @@ namespace AzFramework
 
     Camera SmoothCamera(const Camera& currentCamera, const Camera& targetCamera, const CameraProps& cameraProps, const float deltaTime)
     {
-        const auto clamp_rotation = [](const float angle)
+        const auto clampRotation = [](const float angle)
         {
             return AZStd::fmod(angle + AZ::Constants::TwoPi, AZ::Constants::TwoPi);
         };
 
         // keep yaw in 0 - 360 range
-        float targetYaw = clamp_rotation(targetCamera.m_yaw);
-        const float currentYaw = clamp_rotation(currentCamera.m_yaw);
+        float targetYaw = clampRotation(targetCamera.m_yaw);
+        const float currentYaw = clampRotation(currentCamera.m_yaw);
 
         // return the sign of the float input (-1, 0, 1)
         const auto sign = [](const float value)
@@ -742,21 +772,17 @@ namespace AzFramework
         };
 
         // ensure smooth transition when moving across 0 - 360 boundary
-        const float yawDelta = targetYaw - currentYaw;
-        if (AZStd::abs(yawDelta) >= AZ::Constants::Pi)
+        if (const float yawDelta = targetYaw - currentYaw; AZStd::abs(yawDelta) >= AZ::Constants::Pi)
         {
             targetYaw -= AZ::Constants::TwoPi * sign(yawDelta);
         }
 
         Camera camera;
-        // note: the math for the lerp smoothing implementation for camera rotation and translation was inspired by this excellent
-        // article by Scott Lembcke: https://www.gamasutra.com/blogs/ScottLembcke/20180404/316046/Improved_Lerp_Smoothing.php
         if (cameraProps.m_rotateSmoothingEnabledFn())
         {
-            const float lookRate = AZStd::exp2(cameraProps.m_rotateSmoothnessFn());
-            const float lookTime = AZStd::exp2(-lookRate * deltaTime);
-            camera.m_pitch = AZ::Lerp(targetCamera.m_pitch, currentCamera.m_pitch, lookTime);
-            camera.m_yaw = AZ::Lerp(targetYaw, currentYaw, lookTime);
+            const float lookTime = SmoothValueTime(cameraProps.m_rotateSmoothnessFn(), deltaTime);
+            camera.m_pitch = SmoothValue(targetCamera.m_pitch, currentCamera.m_pitch, lookTime);
+            camera.m_yaw = SmoothValue(targetYaw, currentYaw, lookTime);
         }
         else
         {
@@ -766,8 +792,7 @@ namespace AzFramework
 
         if (cameraProps.m_translateSmoothingEnabledFn())
         {
-            const float moveRate = AZStd::exp2(cameraProps.m_translateSmoothnessFn());
-            const float moveTime = AZStd::exp2(-moveRate * deltaTime);
+            const float moveTime = SmoothValueTime(cameraProps.m_rotateSmoothnessFn(), deltaTime);
             camera.m_pivot = targetCamera.m_pivot.Lerp(currentCamera.m_pivot, moveTime);
             camera.m_offset = targetCamera.m_offset.Lerp(currentCamera.m_offset, moveTime);
         }

+ 14 - 0
Code/Framework/AzFramework/AzFramework/Viewport/CameraInput.h

@@ -85,6 +85,19 @@ namespace AzFramework
     //! Extracts Euler angles (orientation) and translation from the transform and writes the values to the camera.
     void UpdateCameraFromTransform(Camera& camera, const AZ::Transform& transform);
 
+    //! Writes the translation value and Euler angles to the camera.
+    void UpdateCameraFromTranslationAndRotation(Camera& camera, const AZ::Vector3& translation, const AZ::Vector3& eulerAngles);
+
+    //! Returns the time ('t') input value to use with SmoothValue.
+    //! Useful if it is to be reused for multiple calls to SmoothValue.
+    float SmoothValueTime(float smoothness, float deltaTime);
+
+    // Smoothly interpolate a value from current to target according to a smoothing parameter.
+    float SmoothValue(float target, float current, float smoothness, float deltaTime);
+
+    // Overload of SmoothValue that takes time ('t') value directly.
+    float SmoothValue(float target, float current, float time);
+
     //! Generic motion type.
     template<typename MotionTag>
     struct MotionEvent
@@ -334,6 +347,7 @@ namespace AzFramework
         AZStd::function<float()> m_rotateSpeedFn;
         AZStd::function<bool()> m_invertPitchFn;
         AZStd::function<bool()> m_invertYawFn;
+        AZStd::function<bool()> m_constrainPitch;
 
     private:
         InputChannelId m_rotateChannelId; //!< Input channel to begin the rotate camera input.

+ 32 - 18
Code/Framework/AzFramework/AzFramework/Viewport/CameraState.cpp

@@ -8,18 +8,18 @@
 
 #include "CameraState.h"
 
-#include <AzCore/Serialization/SerializeContext.h>
 #include <AzCore/Math/Matrix3x4.h>
 #include <AzCore/Math/Transform.h>
+#include <AzCore/Serialization/SerializeContext.h>
 
 namespace AzFramework
 {
     void SetCameraClippingVolume(
-        AzFramework::CameraState& cameraState, const float nearPlane, const float farPlane, const float fovRad)
+        AzFramework::CameraState& cameraState, const float nearPlane, const float farPlane, const float verticalFovRad)
     {
         cameraState.m_nearClip = nearPlane;
         cameraState.m_farClip = farPlane;
-        cameraState.m_fovOrZoom = fovRad;
+        cameraState.m_fovOrZoom = verticalFovRad;
     }
 
     void SetCameraTransform(CameraState& cameraState, const AZ::Transform& transform)
@@ -35,20 +35,34 @@ namespace AzFramework
         SetCameraClippingVolume(cameraState, 0.1f, 1000.0f, AZ::DegToRad(60.0f));
     }
 
-    AzFramework::CameraState CreateDefaultCamera(
-         const AZ::Transform& transform, const AZ::Vector2& viewportSize)
+    CameraState CreateCamera(
+        const AZ::Transform& transform,
+        const float nearPlane,
+        const float farPlane,
+        const float verticalFovRad,
+        const AZ::Vector2& viewportSize)
     {
         AzFramework::CameraState cameraState;
 
-        SetDefaultCameraClippingVolume(cameraState);
         SetCameraTransform(cameraState, transform);
+        SetCameraClippingVolume(cameraState, nearPlane, farPlane, verticalFovRad);
+        cameraState.m_viewportSize = viewportSize;
+
+        return cameraState;
+    }
+
+    AzFramework::CameraState CreateDefaultCamera(const AZ::Transform& transform, const AZ::Vector2& viewportSize)
+    {
+        AzFramework::CameraState cameraState;
+
+        SetCameraTransform(cameraState, transform);
+        SetDefaultCameraClippingVolume(cameraState);
         cameraState.m_viewportSize = viewportSize;
 
         return cameraState;
     }
 
-    AzFramework::CameraState CreateIdentityDefaultCamera(
-        const AZ::Vector3& position, const AZ::Vector2& viewportSize)
+    AzFramework::CameraState CreateIdentityDefaultCamera(const AZ::Vector3& position, const AZ::Vector2& viewportSize)
     {
         return CreateDefaultCamera(AZ::Transform::CreateTranslation(position), viewportSize);
     }
@@ -89,15 +103,15 @@ namespace AzFramework
 
     void CameraState::Reflect(AZ::SerializeContext& serializeContext)
     {
-        serializeContext.Class<CameraState>()->
-            Field("Position", &CameraState::m_position)->
-            Field("Forward", &CameraState::m_forward)->
-            Field("Side", &CameraState::m_side)->
-            Field("Up", &CameraState::m_up)->
-            Field("ViewportSize", &CameraState::m_viewportSize)->
-            Field("NearClip", &CameraState::m_nearClip)->
-            Field("FarClip", &CameraState::m_farClip)->
-            Field("FovZoom", &CameraState::m_fovOrZoom)->
-            Field("Ortho", &CameraState::m_orthographic);
+        serializeContext.Class<CameraState>()
+            ->Field("Position", &CameraState::m_position)
+            ->Field("Forward", &CameraState::m_forward)
+            ->Field("Side", &CameraState::m_side)
+            ->Field("Up", &CameraState::m_up)
+            ->Field("ViewportSize", &CameraState::m_viewportSize)
+            ->Field("NearClip", &CameraState::m_nearClip)
+            ->Field("FarClip", &CameraState::m_farClip)
+            ->Field("FovZoom", &CameraState::m_fovOrZoom)
+            ->Field("Ortho", &CameraState::m_orthographic);
     }
 } // namespace AzFramework

+ 6 - 2
Code/Framework/AzFramework/AzFramework/Viewport/CameraState.h

@@ -40,10 +40,14 @@ namespace AzFramework
         AZ::Vector2 m_viewportSize = AZ::Vector2::CreateZero(); //!< Dimensions of the viewport.
         float m_nearClip = 0.01f; //!< Near clip plane of the camera.
         float m_farClip = 100.0f; //!< Far clip plane of the camera.
-        float m_fovOrZoom = 0.0f; //!< Fov or zoom of camera depending on if it is using orthographic projection or not.
+        float m_fovOrZoom = 0.0f; //!< Vertical fov or zoom of camera depending on if it is using orthographic projection or not.
         bool m_orthographic = false; //!< Is the camera using orthographic projection or not.
     };
 
+    //! Create a camera at the given transform, specifying the near and far clip planes as well as the fov with a specific viewport size.
+    CameraState CreateCamera(
+        const AZ::Transform& transform, float nearPlane, float farPlane, float verticalFovRad, const AZ::Vector2& viewportSize);
+
     //! Create a camera at the given transform with a specific viewport size.
     //! @note The near/far clip planes and fov are sensible default values - please
     //! use SetCameraClippingVolume to override them.
@@ -60,7 +64,7 @@ namespace AzFramework
     CameraState CreateCameraFromWorldFromViewMatrix(const AZ::Matrix4x4& worldFromView, const AZ::Vector2& viewportSize);
 
     //! Override the default near/far clipping planes and fov of the camera.
-    void SetCameraClippingVolume(CameraState& cameraState, float nearPlane, float farPlane, float fovRad);
+    void SetCameraClippingVolume(CameraState& cameraState, float nearPlane, float farPlane, float verticalFovRad);
 
     //! Override the default near/far clipping planes and fov of the camera by inferring them the specified right handed transform into clip space.
     void SetCameraClippingVolumeFromPerspectiveFovMatrixRH(CameraState& cameraState, const AZ::Matrix4x4& clipFromView);

+ 4 - 0
Code/Framework/AzFramework/AzFramework/Viewport/ScreenGeometry.cpp

@@ -24,6 +24,10 @@ namespace AzFramework
             serializeContext->Class<ScreenVector>()->
                 Field("X", &ScreenVector::m_x)->
                 Field("Y", &ScreenVector::m_y);
+
+            serializeContext->Class<ScreenSize>()->
+                Field("Width", &ScreenSize::m_width)->
+                Field("Height", &ScreenSize::m_height);
         }
     }
 } // namespace AzFramework

+ 66 - 2
Code/Framework/AzFramework/AzFramework/Viewport/ScreenGeometry.h

@@ -26,7 +26,7 @@ namespace AzFramework
         AZ_TYPE_INFO(ScreenPoint, "{8472B6C2-527F-44FC-87F8-C226B1A57A97}");
         ScreenPoint() = default;
 
-        ScreenPoint(int x, int y)
+        constexpr ScreenPoint(int x, int y)
             : m_x(x)
             , m_y(y)
         {
@@ -45,7 +45,7 @@ namespace AzFramework
         AZ_TYPE_INFO(ScreenVector, "{1EAA2C62-8FDB-4A28-9FE3-1FA4F1418894}");
         ScreenVector() = default;
 
-        ScreenVector(int x, int y)
+        constexpr ScreenVector(int x, int y)
             : m_x(x)
             , m_y(y)
         {
@@ -55,6 +55,22 @@ namespace AzFramework
         int m_y; //!< Y screen delta.
     };
 
+    //! A wrapper around a screen width and height.
+    struct ScreenSize
+    {
+        AZ_TYPE_INFO(ScreenSize, "{26D28916-6E8E-44B8-83F9-C44BCDA370E2}");
+        ScreenSize() = default;
+
+        constexpr ScreenSize(int width, int height)
+            : m_width(width)
+            , m_height(height)
+        {
+        }
+
+        int m_width; //!< Screen size width.
+        int m_height; //!< Screen size height.
+    };
+
     void ScreenGeometryReflect(AZ::ReflectContext* context);
 
     inline const ScreenVector operator-(const ScreenPoint& lhs, const ScreenPoint& rhs)
@@ -138,6 +154,16 @@ namespace AzFramework
         return !operator==(lhs, rhs);
     }
 
+    inline const bool operator==(const ScreenSize& lhs, const ScreenSize& rhs)
+    {
+        return lhs.m_width == rhs.m_width && lhs.m_height == rhs.m_height;
+    }
+
+    inline const bool operator!=(const ScreenSize& lhs, const ScreenSize& rhs)
+    {
+        return !operator==(lhs, rhs);
+    }
+
     inline ScreenVector& operator*=(ScreenVector& lhs, const float rhs)
     {
         lhs.m_x = aznumeric_cast<int>(AZStd::lround(aznumeric_cast<float>(lhs.m_x) * rhs));
@@ -152,6 +178,20 @@ namespace AzFramework
         return result;
     }
 
+    inline ScreenSize& operator*=(ScreenSize& lhs, const float rhs)
+    {
+        lhs.m_width = aznumeric_cast<int>(AZStd::lround(aznumeric_cast<float>(lhs.m_width) * rhs));
+        lhs.m_height = aznumeric_cast<int>(AZStd::lround(aznumeric_cast<float>(lhs.m_height) * rhs));
+        return lhs;
+    }
+
+    inline const ScreenSize operator*(const ScreenSize& lhs, const float rhs)
+    {
+        ScreenSize result{ lhs };
+        result *= rhs;
+        return result;
+    }
+
     inline float ScreenVectorLength(const ScreenVector& screenVector)
     {
         return aznumeric_cast<float>(AZStd::sqrt(screenVector.m_x * screenVector.m_x + screenVector.m_y * screenVector.m_y));
@@ -168,4 +208,28 @@ namespace AzFramework
     {
         return AZ::Vector2(aznumeric_cast<float>(screenVector.m_x), aznumeric_cast<float>(screenVector.m_y));
     }
+
+    //! Return an AZ::Vector2 from a ScreenSize.
+    inline AZ::Vector2 Vector2FromScreenSize(const ScreenSize& screenSize)
+    {
+        return AZ::Vector2(aznumeric_cast<float>(screenSize.m_width), aznumeric_cast<float>(screenSize.m_height));
+    }
+
+    //! Return a ScreenPoint from an AZ::Vector2.
+    inline ScreenPoint ScreenPointFromVector2(const AZ::Vector2& vector2)
+    {
+        return ScreenPoint(aznumeric_cast<int>(AZStd::lround(vector2.GetX())), aznumeric_cast<int>(AZStd::lround(vector2.GetY())));
+    }
+
+    //! Return a ScreenVector from an AZ::Vector2.
+    inline ScreenVector ScreenVectorFromVector2(const AZ::Vector2& vector2)
+    {
+        return ScreenVector(aznumeric_cast<int>(AZStd::lround(vector2.GetX())), aznumeric_cast<int>(AZStd::lround(vector2.GetY())));
+    }
+
+    //! Return a ScreenSize from an AZ::Vector2.
+    inline ScreenSize ScreenSizeFromVector2(const AZ::Vector2& vector2)
+    {
+        return ScreenSize(aznumeric_cast<int>(AZStd::lround(vector2.GetX())), aznumeric_cast<int>(AZStd::lround(vector2.GetY())));
+    }
 } // namespace AzFramework

+ 3 - 5
Code/Framework/AzFramework/AzFramework/Viewport/ViewportScreen.cpp

@@ -10,6 +10,7 @@
 
 #include <AzCore/Math/Frustum.h>
 #include <AzCore/Math/Matrix4x4.h>
+#include <AzCore/Math/MatrixUtils.h>
 #include <AzCore/Math/Vector4.h>
 #include <AzCore/Math/VectorConversions.h>
 #include <AzFramework/Entity/EntityDebugDisplayBus.h>
@@ -112,9 +113,8 @@ namespace AzFramework
         const AZ::Matrix4x4& cameraProjection,
         const AZ::Vector2& viewportSize)
     {
-        const auto ndcNormalizedPosition = WorldToScreenNdc(worldPosition, cameraView, cameraProjection);
         // scale ndc position by screen dimensions to return screen position
-        return ScreenPointFromNdc(AZ::Vector3ToVector2(ndcNormalizedPosition), viewportSize);
+        return ScreenPointFromNdc(AZ::Vector3ToVector2(WorldToScreenNdc(worldPosition, cameraView, cameraProjection)), viewportSize);
     }
 
     ScreenPoint WorldToScreen(const AZ::Vector3& worldPosition, const CameraState& cameraState)
@@ -144,9 +144,7 @@ namespace AzFramework
         const AZ::Matrix4x4& inverseCameraProjection,
         const AZ::Vector2& viewportSize)
     {
-        const auto normalizedScreenPosition = NdcFromScreenPoint(screenPosition, viewportSize);
-
-        return ScreenNdcToWorld(normalizedScreenPosition, inverseCameraView, inverseCameraProjection);
+        return ScreenNdcToWorld(NdcFromScreenPoint(screenPosition, viewportSize), inverseCameraView, inverseCameraProjection);
     }
 
     AZ::Vector3 ScreenToWorld(const ScreenPoint& screenPosition, const CameraState& cameraState)

+ 46 - 7
Code/Framework/AzFramework/Tests/CameraInputTests.cpp

@@ -104,9 +104,10 @@ namespace UnitTest
         AZStd::shared_ptr<AzFramework::OrbitCameraInput> m_orbitCamera;
         AZ::Vector3 m_pivot = AZ::Vector3::CreateZero();
 
-        //! This is approximately Pi/2 * 1000 - this can be used to rotate the camera 90 degrees (pitch or yaw based
-        //! on vertical or horizontal motion) as the rotate speed function is set to be 1/1000.
-        inline static const int PixelMotionDelta = 1570;
+        // this is approximately Pi/2 * 1000 - this can be used to rotate the camera 90 degrees (pitch or yaw based
+        // on vertical or horizontal motion) as the rotate speed function is set to be 1/1000.
+        inline static const int PixelMotionDelta90Degrees = 1570;
+        inline static const int PixelMotionDelta135Degrees = 2356;
     };
 
     TEST_F(CameraInputFixture, BeginAndEndOrbitCameraInputConsumesCorrectEvents)
@@ -292,7 +293,7 @@ namespace UnitTest
 
         HandleEventAndUpdate(
             AzFramework::DiscreteInputEvent{ AzFramework::InputDeviceMouse::Button::Right, AzFramework::InputChannel::State::Began });
-        HandleEventAndUpdate(AzFramework::HorizontalMotionEvent{ PixelMotionDelta });
+        HandleEventAndUpdate(AzFramework::HorizontalMotionEvent{ PixelMotionDelta90Degrees });
 
         const float expectedYaw = AzFramework::WrapYawRotation(-AZ::Constants::HalfPi);
 
@@ -310,7 +311,7 @@ namespace UnitTest
 
         HandleEventAndUpdate(
             AzFramework::DiscreteInputEvent{ AzFramework::InputDeviceMouse::Button::Right, AzFramework::InputChannel::State::Began });
-        HandleEventAndUpdate(AzFramework::VerticalMotionEvent{ PixelMotionDelta });
+        HandleEventAndUpdate(AzFramework::VerticalMotionEvent{ PixelMotionDelta90Degrees });
 
         const float expectedPitch = AzFramework::ClampPitchRotation(-AZ::Constants::HalfPi);
 
@@ -331,7 +332,7 @@ namespace UnitTest
         HandleEventAndUpdate(AzFramework::DiscreteInputEvent{ m_orbitChannelId, AzFramework::InputChannel::State::Began });
         HandleEventAndUpdate(
             AzFramework::DiscreteInputEvent{ AzFramework::InputDeviceMouse::Button::Left, AzFramework::InputChannel::State::Began });
-        HandleEventAndUpdate(AzFramework::VerticalMotionEvent{ PixelMotionDelta });
+        HandleEventAndUpdate(AzFramework::VerticalMotionEvent{ PixelMotionDelta90Degrees });
 
         const auto expectedCameraEndingPosition = AZ::Vector3(0.0f, -10.0f, 10.0f);
         const float expectedPitch = AzFramework::ClampPitchRotation(-AZ::Constants::HalfPi);
@@ -354,7 +355,7 @@ namespace UnitTest
         HandleEventAndUpdate(AzFramework::DiscreteInputEvent{ m_orbitChannelId, AzFramework::InputChannel::State::Began });
         HandleEventAndUpdate(
             AzFramework::DiscreteInputEvent{ AzFramework::InputDeviceMouse::Button::Left, AzFramework::InputChannel::State::Began });
-        HandleEventAndUpdate(AzFramework::HorizontalMotionEvent{ -PixelMotionDelta });
+        HandleEventAndUpdate(AzFramework::HorizontalMotionEvent{ -PixelMotionDelta90Degrees });
 
         const auto expectedCameraEndingPosition = AZ::Vector3(20.0f, -5.0f, 0.0f);
         const float expectedYaw = AzFramework::WrapYawRotation(AZ::Constants::HalfPi);
@@ -366,4 +367,42 @@ namespace UnitTest
         EXPECT_THAT(m_camera.m_offset, IsClose(AZ::Vector3(5.0f, -10.0f, 0.0f)));
         EXPECT_THAT(m_camera.Translation(), IsCloseTolerance(expectedCameraEndingPosition, 0.01f));
     }
+
+    TEST_F(CameraInputFixture, CameraPitchCanNotBeMovedPastNinetyDegreesWhenConstrained)
+    {
+        const auto cameraStartingPosition = AZ::Vector3(15.0f, -20.0f, 0.0f);
+        m_targetCamera.m_pivot = cameraStartingPosition;
+
+        HandleEventAndUpdate(
+            AzFramework::DiscreteInputEvent{ AzFramework::InputDeviceMouse::Button::Right, AzFramework::InputChannel::State::Began });
+        // pitch by 135.0 degrees
+        HandleEventAndUpdate(AzFramework::VerticalMotionEvent{ -PixelMotionDelta135Degrees });
+
+        // clamped to 90.0 degrees
+        const float expectedPitch = AZ::DegToRad(90.0f);
+
+        using ::testing::FloatNear;
+        EXPECT_THAT(m_camera.m_pitch, FloatNear(expectedPitch, 0.001f));
+    }
+
+    TEST_F(CameraInputFixture, CameraPitchCanBeMovedPastNinetyDegreesWhenUnconstrained)
+    {
+        m_firstPersonRotateCamera->m_constrainPitch = []
+        {
+            return false;
+        };
+
+        const auto cameraStartingPosition = AZ::Vector3(15.0f, -20.0f, 0.0f);
+        m_targetCamera.m_pivot = cameraStartingPosition;
+
+        HandleEventAndUpdate(
+            AzFramework::DiscreteInputEvent{ AzFramework::InputDeviceMouse::Button::Right, AzFramework::InputChannel::State::Began });
+        // pitch by 135.0 degrees
+        HandleEventAndUpdate(AzFramework::VerticalMotionEvent{ -PixelMotionDelta135Degrees });
+
+        const float expectedPitch = AZ::DegToRad(135.0f);
+
+        using ::testing::FloatNear;
+        EXPECT_THAT(m_camera.m_pitch, FloatNear(expectedPitch, 0.001f));
+    }
 } // namespace UnitTest

+ 3 - 17
Code/Framework/AzFramework/Tests/CameraState.cpp

@@ -11,6 +11,7 @@
 #include <AzFramework/Viewport/CameraState.h>
 #include <AZTestShared/Math/MathTestHelpers.h>
 #include <AzCore/Math/SimdMath.h>
+#include <AzCore/Math/MatrixUtils.h>
 #include <AzCore/Math/Matrix4x4.h>
 
 namespace UnitTest
@@ -51,22 +52,6 @@ namespace UnitTest
     {
     };
 
-    // Taken from Atom::MatrixUtils for testing purposes, this can be removed if MakePerspectiveFovMatrixRH makes it into AZ
-    static AZ::Matrix4x4 MakePerspectiveMatrixRH(float fovY, float aspectRatio, float nearClip, float farClip)
-    {
-        float sinFov, cosFov;
-        AZ::SinCos(0.5f * fovY, sinFov, cosFov);
-        float yScale = cosFov / sinFov; //cot(fovY/2)
-        float xScale = yScale / aspectRatio;
-
-        AZ::Matrix4x4 out;
-        out.SetRow(0,   xScale, 0.f,    0.f,                                0.f                                     );
-        out.SetRow(1,   0.f,    yScale, 0.f,                                0.f                                     );
-        out.SetRow(2,   0.f,    0.f,    farClip / (nearClip - farClip),     nearClip*farClip / (nearClip - farClip) );
-        out.SetRow(3,   0.f,    0.f,    -1.f,                               0.f                                     );
-        return out;
-    }
-
     TEST_P(Translation, Permutation)
     {
         // Given a position
@@ -176,7 +161,8 @@ namespace UnitTest
     {
         auto [fovY, aspectRatio, nearClip, farClip] = GetParam();
 
-        AZ::Matrix4x4 clipFromView = MakePerspectiveMatrixRH(fovY, aspectRatio, nearClip, farClip);
+        AZ::Matrix4x4 clipFromView;
+        MakePerspectiveFovMatrixRH(clipFromView, fovY, aspectRatio, nearClip, farClip);
 
         AzFramework::SetCameraClippingVolumeFromPerspectiveFovMatrixRH(m_cameraState, clipFromView);
 

+ 2 - 1
Code/Framework/AzFramework/Tests/CursorStateTests.cpp

@@ -8,12 +8,13 @@
 
 #include <AzCore/UnitTest/TestTypes.h>
 #include <AzFramework/Viewport/CursorState.h>
+#include <Tests/Utils/Printers.h>
 
 namespace UnitTest
 {
     using AzFramework::CursorState;
-    using AzFramework::ScreenVector;
     using AzFramework::ScreenPoint;
+    using AzFramework::ScreenVector;
 
     class CursorStateFixture : public ::testing::Test
     {

+ 32 - 0
Code/Framework/AzFramework/Tests/Utils/Printers.cpp

@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include "Printers.h"
+
+#include <AzFramework/Viewport/ScreenGeometry.h>
+
+#include <ostream>
+#include <string>
+
+namespace AzFramework
+{
+    void PrintTo(const ScreenPoint& screenPoint, std::ostream* os)
+    {
+        *os << "(x: " << screenPoint.m_x << ", y: " << screenPoint.m_y << ")";
+    }
+
+    void PrintTo(const ScreenVector& screenVector, std::ostream* os)
+    {
+        *os << "(x: " << screenVector.m_x << ", y: " << screenVector.m_y << ")";
+    }
+
+    void PrintTo(const ScreenSize& screenSize, std::ostream* os)
+    {
+        *os << "(width: " << screenSize.m_width << ", height: " << screenSize.m_height << ")";
+    }
+} // namespace AzFramework

+ 20 - 0
Code/Framework/AzFramework/Tests/Utils/Printers.h

@@ -0,0 +1,20 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include <iosfwd>
+
+namespace AzFramework
+{
+    struct ScreenPoint;
+    struct ScreenVector;
+    struct ScreenSize;
+
+    void PrintTo(const ScreenPoint& screenPoint, std::ostream* os);
+    void PrintTo(const ScreenVector& screenVector, std::ostream* os);
+    void PrintTo(const ScreenSize& screenSize, std::ostream* os);
+} // namespace AzFramework

+ 2 - 0
Code/Framework/AzFramework/Tests/framework_shared_tests_files.cmake

@@ -11,5 +11,7 @@ set(FILES
     Mocks/MockWindowRequests.h
     Utils/Utils.h
     Utils/Utils.cpp
+    Utils/Printers.h
+    Utils/Printers.cpp
     FrameworkApplicationFixture.h
 )

+ 2 - 2
Code/Framework/AzManipulatorTestFramework/Include/AzManipulatorTestFramework/ViewportInteraction.h

@@ -41,8 +41,8 @@ namespace AzManipulatorTestFramework
         // ViewportInteractionRequestBus overrides ...
         AzFramework::CameraState GetCameraState() override;
         AzFramework::ScreenPoint ViewportWorldToScreen(const AZ::Vector3& worldPosition) override;
-        AZStd::optional<AZ::Vector3> ViewportScreenToWorld(const AzFramework::ScreenPoint& screenPosition, float depth) override;
-        AZStd::optional<AzToolsFramework::ViewportInteraction::ProjectedViewportRay> ViewportScreenToWorldRay(
+        AZ::Vector3 ViewportScreenToWorld(const AzFramework::ScreenPoint& screenPosition) override;
+        AzToolsFramework::ViewportInteraction::ProjectedViewportRay ViewportScreenToWorldRay(
             const AzFramework::ScreenPoint& screenPosition) override;
         float DeviceScalingFactor() override;
 

+ 1 - 1
Code/Framework/AzManipulatorTestFramework/Source/AzManipulatorTestFrameworkUtils.cpp

@@ -95,7 +95,7 @@ namespace AzManipulatorTestFramework
 
         AzToolsFramework::ViewportInteraction::MousePick mousePick;
         mousePick.m_screenCoordinates = screenPoint;
-        mousePick.m_rayOrigin = cameraState.m_position;
+        mousePick.m_rayOrigin = nearPlaneWorldPosition;
         mousePick.m_rayDirection = (nearPlaneWorldPosition - cameraState.m_position).GetNormalized();
 
         return mousePick;

+ 0 - 2
Code/Framework/AzManipulatorTestFramework/Source/ImmediateModeActionDispatcher.cpp

@@ -69,8 +69,6 @@ namespace AzManipulatorTestFramework
     void ImmediateModeActionDispatcher::CameraStateImpl(const AzFramework::CameraState& cameraState)
     {
         m_viewportManipulatorInteraction.GetViewportInteraction().SetCameraState(cameraState);
-        GetMouseInteractionEvent()->m_mouseInteraction.m_mousePick.m_rayOrigin = cameraState.m_position;
-        GetMouseInteractionEvent()->m_mouseInteraction.m_mousePick.m_rayDirection = cameraState.m_forward;
     }
 
     void ImmediateModeActionDispatcher::MouseLButtonDownImpl()

+ 2 - 1
Code/Framework/AzManipulatorTestFramework/Source/IndirectManipulatorViewportInteraction.cpp

@@ -20,7 +20,8 @@ namespace AzManipulatorTestFramework
     {
     public:
         IndirectCallManipulatorManager(ViewportInteractionInterface& viewportInteraction);
-        // ManipulatorManagerInterface ...
+
+        // ManipulatorManagerInterface overrides ...
         void ConsumeMouseInteractionEvent(const MouseInteractionEvent& event) override;
         AzToolsFramework::ManipulatorManagerId GetId() const override;
         bool ManipulatorBeingInteracted() const override;

+ 3 - 4
Code/Framework/AzManipulatorTestFramework/Source/ViewportInteraction.cpp

@@ -140,13 +140,12 @@ namespace AzManipulatorTestFramework
         return m_viewportId;
     }
 
-    AZStd::optional<AZ::Vector3> ViewportInteraction::ViewportScreenToWorld(
-        [[maybe_unused]] const AzFramework::ScreenPoint& screenPosition, [[maybe_unused]] float depth)
+    AZ::Vector3 ViewportInteraction::ViewportScreenToWorld([[maybe_unused]] const AzFramework::ScreenPoint& screenPosition)
     {
-        return {};
+        return AZ::Vector3::CreateZero();
     }
 
-    AZStd::optional<AzToolsFramework::ViewportInteraction::ProjectedViewportRay> ViewportInteraction::ViewportScreenToWorldRay(
+    AzToolsFramework::ViewportInteraction::ProjectedViewportRay ViewportInteraction::ViewportScreenToWorldRay(
         [[maybe_unused]] const AzFramework::ScreenPoint& screenPosition)
     {
         return {};

+ 1 - 1
Code/Framework/AzManipulatorTestFramework/Tests/WorldSpaceBuilderTest.cpp

@@ -140,8 +140,8 @@ namespace UnitTest
         // given a left mouse down ray in world space
         // consume the mouse move event
         state.m_actionDispatcher->CameraState(m_cameraState)
-            ->MouseLButtonDown()
             ->MousePosition(AzManipulatorTestFramework::GetCameraStateViewportCenter(m_cameraState))
+            ->MouseLButtonDown()
             ->ExpectTrue(state.m_linearManipulator->PerformingAction())
             ->ExpectManipulatorBeingInteracted()
             ->MouseLButtonUp()

+ 25 - 0
Code/Framework/AzToolsFramework/AzToolsFramework/API/PythonLoader.h

@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#pragma once
+
+namespace AzToolsFramework::EmbeddedPython
+{
+    // When using embedded Python, some platforms need to explicitly load the python library.
+    // For any modules that depend on 3rdParty::Python package, the AZ::Module should inherit this class.
+    class PythonLoader
+    {
+    public:
+        PythonLoader();
+        ~PythonLoader();
+
+    private:
+        void* m_embeddedLibPythonHandle{ nullptr };
+    };
+
+} // namespace AzToolsFramework::EmbeddedPython

+ 3 - 0
Code/Framework/AzToolsFramework/AzToolsFramework/API/ToolsApplicationAPI.h

@@ -927,6 +927,9 @@ namespace AzToolsFramework
         /// Notify that the MainWindow has been fully initialized
         virtual void NotifyMainWindowInitialized(QMainWindow* /*mainWindow*/) {}
 
+        /// Notify that the Editor has been fully initialized
+        virtual void NotifyEditorInitialized() {}
+
         /// Signal that an asset should be highlighted / selected
         virtual void SelectAsset(const QString& /* assetPath */) {}
     };

+ 25 - 8
Code/Framework/AzToolsFramework/AzToolsFramework/Application/ToolsApplication.cpp

@@ -214,12 +214,17 @@ namespace AzToolsFramework
             , public AZ::BehaviorEBusHandler
         {
             AZ_EBUS_BEHAVIOR_BINDER(EditorEventsBusHandler, "{352F80BB-469A-40B6-B322-FE57AB51E4DA}", AZ::SystemAllocator,
-                NotifyRegisterViews);
+                NotifyRegisterViews, NotifyEditorInitialized);
 
             void NotifyRegisterViews() override
             {
                 Call(FN_NotifyRegisterViews);
             }
+
+            void NotifyEditorInitialized() override
+            {
+                Call(FN_NotifyEditorInitialized);
+            }
         };
 
     } // Internal
@@ -443,6 +448,7 @@ namespace AzToolsFramework
                 ->Attribute(AZ::Script::Attributes::Module, "editor")
                 ->Handler<Internal::EditorEventsBusHandler>()
                 ->Event("NotifyRegisterViews", &EditorEvents::NotifyRegisterViews)
+                ->Event("NotifyEditorInitialized", &EditorEvents::NotifyEditorInitialized)
                 ;
 
             behaviorContext->EBus<ViewPaneCallbackBus>("ViewPaneCallbackBus")
@@ -1190,14 +1196,25 @@ namespace AzToolsFramework
 
     AZ::EntityId ToolsApplication::GetCurrentLevelEntityId()
     {
-        AzFramework::EntityContextId editorEntityContextId = AzFramework::EntityContextId::CreateNull();
-        AzToolsFramework::EditorEntityContextRequestBus::BroadcastResult(editorEntityContextId, &AzToolsFramework::EditorEntityContextRequestBus::Events::GetEditorEntityContextId);
-        AZ::SliceComponent* rootSliceComponent = nullptr;
-        AzFramework::SliceEntityOwnershipServiceRequestBus::EventResult(rootSliceComponent, editorEntityContextId,
-            &AzFramework::SliceEntityOwnershipServiceRequestBus::Events::GetRootSlice);
-        if (rootSliceComponent && rootSliceComponent->GetMetadataEntity())
+        if (IsPrefabSystemEnabled())
         {
-            return rootSliceComponent->GetMetadataEntity()->GetId();
+            if (auto prefabPublicInterface = AZ::Interface<Prefab::PrefabPublicInterface>::Get())
+            {
+                return prefabPublicInterface->GetLevelInstanceContainerEntityId();
+            }
+        }
+        else
+        {
+            AzFramework::EntityContextId editorEntityContextId = AzFramework::EntityContextId::CreateNull();
+            AzToolsFramework::EditorEntityContextRequestBus::BroadcastResult(
+                editorEntityContextId, &AzToolsFramework::EditorEntityContextRequestBus::Events::GetEditorEntityContextId);
+            AZ::SliceComponent* rootSliceComponent = nullptr;
+            AzFramework::SliceEntityOwnershipServiceRequestBus::EventResult(
+                rootSliceComponent, editorEntityContextId, &AzFramework::SliceEntityOwnershipServiceRequestBus::Events::GetRootSlice);
+            if (rootSliceComponent && rootSliceComponent->GetMetadataEntity())
+            {
+                return rootSliceComponent->GetMetadataEntity()->GetId();
+            }
         }
 
         return AZ::EntityId();

+ 5 - 2
Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/AssetPicker/AssetPickerDialog.cpp

@@ -31,7 +31,10 @@ AZ_POP_DISABLE_WARNING
 AZ_CVAR(
     bool, ed_hideAssetPickerPathColumn, true, nullptr, AZ::ConsoleFunctorFlags::Null,
     "Hide AssetPicker path column for a clearer view.");
-AZ_CVAR_EXTERNED(bool, ed_useNewAssetBrowserTableView);
+
+AZ_CVAR(
+    bool, ed_useNewAssetPickerView, false, nullptr, AZ::ConsoleFunctorFlags::Null,
+    "Uses the new Asset Picker View.");
 
 namespace AzToolsFramework
 {
@@ -106,7 +109,7 @@ namespace AzToolsFramework
             m_persistentState = AZ::UserSettings::CreateFind<AzToolsFramework::QWidgetSavedState>(AZ::Crc32(("AssetBrowserTreeView_Dialog_" + name).toUtf8().data()), AZ::UserSettings::CT_GLOBAL);
 
             m_ui->m_assetBrowserTableViewWidget->setVisible(false);
-            if (ed_useNewAssetBrowserTableView)
+            if (ed_useNewAssetPickerView)
             {
                 m_ui->m_assetBrowserTreeViewWidget->setVisible(false);
                 m_ui->m_assetBrowserTableViewWidget->setVisible(true);

+ 2 - 0
Code/Framework/AzToolsFramework/AzToolsFramework/Component/EditorComponentAPIComponent.cpp

@@ -597,11 +597,13 @@ namespace AzToolsFramework
                 pte.SetVisibleEnforcement(true);
             }
 
+            ScopedUndoBatch undo("Modify Entity Property");
             PropertyOutcome result = pte.SetProperty(propertyPath, value);
             if (result.IsSuccess())
             {
                 PropertyEditorEntityChangeNotificationBus::Event(componentInstance.GetEntityId(), &PropertyEditorEntityChangeNotifications::OnEntityComponentPropertyChanged, componentInstance.GetComponentId());
             }
+            undo.MarkEntityDirty(componentInstance.GetEntityId());
 
             return result;
         }

+ 4 - 1
Code/Framework/AzToolsFramework/AzToolsFramework/UnitTest/AzToolsFrameworkTestHelpers.h

@@ -158,7 +158,10 @@ namespace UnitTest
             {
                 // Create & Start a new ToolsApplication if there's no existing one
                 m_app = CreateTestApplication();
-                m_app->Start(AzFramework::Application::Descriptor());
+                AZ::ComponentApplication::StartupParameters startupParameters;
+                startupParameters.m_loadAssetCatalog = false;
+
+                m_app->Start(AzFramework::Application::Descriptor(), startupParameters);
             }
 
             // without this, the user settings component would attempt to save on finalize/shutdown. Since the file is

+ 2 - 8
Code/Framework/AzToolsFramework/AzToolsFramework/Viewport/ViewportMessages.h

@@ -162,12 +162,11 @@ namespace AzToolsFramework
             //! Multiply by DeviceScalingFactor to get the position in viewport pixel space.
             virtual AzFramework::ScreenPoint ViewportWorldToScreen(const AZ::Vector3& worldPosition) = 0;
             //! Transforms a point from Qt widget screen space to world space based on the given clip space depth.
-            //! Depth specifies a relative camera depth to project in the range of [0.f, 1.f].
             //! Returns the world space position if successful.
-            virtual AZStd::optional<AZ::Vector3> ViewportScreenToWorld(const AzFramework::ScreenPoint& screenPosition, float depth) = 0;
+            virtual AZ::Vector3 ViewportScreenToWorld(const AzFramework::ScreenPoint& screenPosition) = 0;
             //! Casts a point in screen space to a ray in world space originating from the viewport camera frustum's near plane.
             //! Returns a ray containing the ray's origin and a direction normal, if successful.
-            virtual AZStd::optional<ProjectedViewportRay> ViewportScreenToWorldRay(const AzFramework::ScreenPoint& screenPosition) = 0;
+            virtual ProjectedViewportRay ViewportScreenToWorldRay(const AzFramework::ScreenPoint& screenPosition) = 0;
             //! Gets the DPI scaling factor that translates Qt widget space into viewport pixel space.
             virtual float DeviceScalingFactor() = 0;
 
@@ -229,9 +228,6 @@ namespace AzToolsFramework
         class MainEditorViewportInteractionRequests
         {
         public:
-            //! Given a point in screen space, return the picked entity (if any).
-            //! Picked EntityId will be returned, InvalidEntityId will be returned on failure.
-            virtual AZ::EntityId PickEntity(const AzFramework::ScreenPoint& point) = 0;
             //! Given a point in screen space, return the terrain position in world space.
             virtual AZ::Vector3 PickTerrain(const AzFramework::ScreenPoint& point) = 0;
             //! Return the terrain height given a world position in 2d (xy plane).
@@ -266,7 +262,6 @@ namespace AzToolsFramework
         {
         public:
             static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single;
-            static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single;
 
             //! Returns the current state of the keyboard modifier keys.
             virtual KeyboardModifiers QueryKeyboardModifiers() = 0;
@@ -290,7 +285,6 @@ namespace AzToolsFramework
         {
         public:
             static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single;
-            static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single;
 
             //! Returns the current time in seconds.
             //! This interface can be overridden for the purposes of testing to simplify viewport input requests.

+ 5 - 3
Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorHelpers.cpp

@@ -148,7 +148,6 @@ namespace AzToolsFramework
         return false;
     }
 
-
     EditorHelpers::EditorHelpers(const EditorVisibleEntityDataCache* entityDataCache)
         : m_entityDataCache(entityDataCache)
     {
@@ -190,7 +189,10 @@ namespace AzToolsFramework
             if (helpersVisible)
             {
                 // some components choose to hide their icons (e.g. meshes)
-                if (!m_entityDataCache->IsVisibleEntityIconHidden(entityCacheIndex))
+                // we also do not want to test against icons that may not be showing as they're inside a 'closed' entity container
+                // (these icons only become visible when it is opened for editing)
+                if (!m_entityDataCache->IsVisibleEntityIconHidden(entityCacheIndex) &&
+                    m_entityDataCache->IsVisibleEntityIndividuallySelectableInViewport(entityCacheIndex))
                 {
                     const AZ::Vector3& entityPosition = m_entityDataCache->GetVisibleEntityPosition(entityCacheIndex);
 
@@ -235,7 +237,7 @@ namespace AzToolsFramework
                     viewportId, &ViewportInteraction::ViewportMouseCursorRequestBus::Events::SetOverrideCursor,
                     ViewportInteraction::CursorStyleOverride::Forbidden);
             }
-                
+
             if (mouseInteraction.m_mouseInteraction.m_mouseButtons.Left() &&
                     mouseInteraction.m_mouseEvent == ViewportInteraction::MouseEvent::Down ||
                 mouseInteraction.m_mouseEvent == ViewportInteraction::MouseEvent::DoubleClick)

+ 2 - 2
Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorTransformComponentSelection.cpp

@@ -409,7 +409,7 @@ namespace AzToolsFramework
             const AzFramework::CameraState cameraState = GetCameraState(viewportId);
             for (size_t entityCacheIndex = 0; entityCacheIndex < entityDataCache.VisibleEntityDataCount(); ++entityCacheIndex)
             {
-                if (!entityDataCache.IsVisibleEntitySelectableInViewport(entityCacheIndex))
+                if (!entityDataCache.IsVisibleEntityIndividuallySelectableInViewport(entityCacheIndex))
                 {
                     continue;
                 }
@@ -983,7 +983,7 @@ namespace AzToolsFramework
     {
         if (auto entityIndex = entityDataCache.GetVisibleEntityIndexFromId(entityId))
         {
-            if (entityDataCache.IsVisibleEntitySelectableInViewport(*entityIndex))
+            if (entityDataCache.IsVisibleEntityIndividuallySelectableInViewport(*entityIndex))
             {
                 return *entityIndex;
             }

+ 3 - 5
Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorVisibleEntityDataCache.cpp

@@ -293,12 +293,10 @@ namespace AzToolsFramework
         return m_impl->m_visibleEntityDatas[index].m_iconHidden;
     }
 
-    bool EditorVisibleEntityDataCache::IsVisibleEntitySelectableInViewport(size_t index) const
+    bool EditorVisibleEntityDataCache::IsVisibleEntityIndividuallySelectableInViewport(const size_t index) const
     {
-        return m_impl->m_visibleEntityDatas[index].m_visible
-            && !m_impl->m_visibleEntityDatas[index].m_locked
-            && m_impl->m_visibleEntityDatas[index].m_inFocus
-            && !m_impl->m_visibleEntityDatas[index].m_descendantOfClosedContainer;
+        return m_impl->m_visibleEntityDatas[index].m_visible && !m_impl->m_visibleEntityDatas[index].m_locked &&
+            m_impl->m_visibleEntityDatas[index].m_inFocus && !m_impl->m_visibleEntityDatas[index].m_descendantOfClosedContainer;
     }
 
     AZStd::optional<size_t> EditorVisibleEntityDataCache::GetVisibleEntityIndexFromId(const AZ::EntityId entityId) const

+ 4 - 1
Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/EditorVisibleEntityDataCache.h

@@ -55,7 +55,10 @@ namespace AzToolsFramework
         bool IsVisibleEntityVisible(size_t index) const;
         bool IsVisibleEntitySelected(size_t index) const;
         bool IsVisibleEntityIconHidden(size_t index) const;
-        bool IsVisibleEntitySelectableInViewport(size_t index) const;
+        //! Returns true if the entity is individually selectable (none of its ancestors are a closed container entity).
+        //! @note It may still be desirable to be able to 'click' an entity that is a descendant of a closed container
+        //! to select the container itself, not the individual entity.
+        bool IsVisibleEntityIndividuallySelectableInViewport(size_t index) const;
 
         AZStd::optional<size_t> GetVisibleEntityIndexFromId(AZ::EntityId entityId) const;
 

+ 1 - 0
Code/Framework/AzToolsFramework/AzToolsFramework/aztoolsframework_files.cmake

@@ -47,6 +47,7 @@ set(FILES
     API/EntityCompositionRequestBus.h
     API/EntityCompositionNotificationBus.h
     API/EditorViewportIconDisplayInterface.h
+    API/PythonLoader.h
     API/ViewPaneOptions.h
     API/ViewportEditorModeTrackerInterface.h
     Application/Ticker.h

+ 20 - 0
Code/Framework/AzToolsFramework/Platform/Common/Default/AzToolsFramework/API/PythonLoader_Default.cpp

@@ -0,0 +1,20 @@
+/*
+* Copyright (c) Contributors to the Open 3D Engine Project.
+* For complete copyright and license terms please see the LICENSE at the root of this distribution.
+*
+* SPDX-License-Identifier: Apache-2.0 OR MIT
+*
+*/
+
+#include <AzToolsFramework/API/PythonLoader.h>
+
+namespace AzToolsFramework::EmbeddedPython
+{
+    PythonLoader::PythonLoader()
+    {
+    }
+
+    PythonLoader::~PythonLoader()
+    {
+    }
+}

+ 34 - 0
Code/Framework/AzToolsFramework/Platform/Linux/AzToolsFramework/API/PythonLoader_Linux.cpp

@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include <AzToolsFramework/API/PythonLoader.h>
+#include <AzCore/Debug/Trace.h>
+#include <dlfcn.h>
+
+namespace AzToolsFramework::EmbeddedPython
+{
+    PythonLoader::PythonLoader()
+    {
+        constexpr char libPythonName[] = "libpython3.7m.so.1.0";
+        if (m_embeddedLibPythonHandle = dlopen(libPythonName, RTLD_NOW | RTLD_GLOBAL);
+            m_embeddedLibPythonHandle == nullptr)
+        {
+            char* err = dlerror();
+            AZ_Error("PythonLoader", false, "Failed to load %s with error: %s\n", libPythonName, err ? err : "Unknown Error");
+        }
+    }
+
+    PythonLoader::~PythonLoader()
+    {
+        if (m_embeddedLibPythonHandle)
+        {
+            dlclose(m_embeddedLibPythonHandle);
+        }
+    }
+    
+} // namespace AzToolsFramework::EmbeddedPython

+ 1 - 0
Code/Framework/AzToolsFramework/Platform/Linux/platform_linux_files.cmake

@@ -7,4 +7,5 @@
 #
 
 set(FILES
+    AzToolsFramework/API/PythonLoader_Linux.cpp
 )

+ 1 - 0
Code/Framework/AzToolsFramework/Platform/Mac/platform_mac_files.cmake

@@ -7,4 +7,5 @@
 #
 
 set(FILES
+    ../Common/Default/AzToolsFramework/API/PythonLoader_Default.cpp
 )

+ 1 - 0
Code/Framework/AzToolsFramework/Platform/Windows/platform_windows_files.cmake

@@ -7,4 +7,5 @@
 #
 
 set(FILES
+    ../Common/Default/AzToolsFramework/API/PythonLoader_Default.cpp
 )

+ 4 - 2
Code/Framework/AzToolsFramework/Tests/BoundsTestComponent.cpp

@@ -40,6 +40,9 @@ namespace UnitTest
     {
         AzFramework::BoundsRequestBus::Handler::BusConnect(GetEntityId());
         AzToolsFramework::EditorComponentSelectionRequestsBus::Handler::BusConnect(GetEntityId());
+
+        // default local bounds to unit cube
+        m_localBounds = AZ::Aabb::CreateFromMinMax(AZ::Vector3(-0.5f), AZ::Vector3(0.5f));
     }
 
     void BoundsTestComponent::Deactivate()
@@ -57,7 +60,6 @@ namespace UnitTest
 
     AZ::Aabb BoundsTestComponent::GetLocalBounds()
     {
-        return AZ::Aabb::CreateFromMinMax(AZ::Vector3(-0.5f), AZ::Vector3(0.5f));
+        return m_localBounds;
     }
-
 } // namespace UnitTest

+ 2 - 0
Code/Framework/AzToolsFramework/Tests/BoundsTestComponent.h

@@ -41,5 +41,7 @@ namespace UnitTest
         // BoundsRequestBus overrides ...
         AZ::Aabb GetWorldBounds() override;
         AZ::Aabb GetLocalBounds() override;
+
+        AZ::Aabb m_localBounds; //!< Local bounds that can be modified for certain tests (defaults to unit cube).
     };
 } // namespace UnitTest

+ 41 - 13
Code/Framework/AzToolsFramework/Tests/EditorTransformComponentSelectionTests.cpp

@@ -38,7 +38,7 @@
 #include <AzToolsFramework/ViewportSelection/EditorVisibleEntityDataCache.h>
 #include <AzToolsFramework/ViewportUi/ViewportUiManager.h>
 
-#include<Tests/BoundsTestComponent.h>
+#include <Tests/BoundsTestComponent.h>
 
 namespace AZ
 {
@@ -493,12 +493,8 @@ namespace UnitTest
 
         ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
         // Then
-        AzToolsFramework::EntityIdList selectedEntities;
-        AzToolsFramework::ToolsApplicationRequestBus::BroadcastResult(
-            selectedEntities, &AzToolsFramework::ToolsApplicationRequestBus::Events::GetSelectedEntities);
-
-        AzToolsFramework::EntityIdList expectedSelectedEntities = { entity4, entity5, entity6 };
-
+        const AzToolsFramework::EntityIdList selectedEntities = SelectedEntities();
+        const AzToolsFramework::EntityIdList expectedSelectedEntities = { entity4, entity5, entity6 };
         EXPECT_THAT(selectedEntities, UnorderedElementsAreArray(expectedSelectedEntities));
         ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
     }
@@ -527,12 +523,8 @@ namespace UnitTest
 
         ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
         // Then
-        AzToolsFramework::EntityIdList selectedEntities;
-        AzToolsFramework::ToolsApplicationRequestBus::BroadcastResult(
-            selectedEntities, &AzToolsFramework::ToolsApplicationRequestBus::Events::GetSelectedEntities);
-
-        AzToolsFramework::EntityIdList expectedSelectedEntities = { m_entityId1, entity2, entity3, entity4 };
-
+        const AzToolsFramework::EntityIdList selectedEntities = SelectedEntities();
+        const AzToolsFramework::EntityIdList expectedSelectedEntities = { m_entityId1, entity2, entity3, entity4 };
         EXPECT_THAT(selectedEntities, UnorderedElementsAreArray(expectedSelectedEntities));
         ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
     }
@@ -946,6 +938,42 @@ namespace UnitTest
         EXPECT_THAT(selectedEntitiesAfter, UnorderedElementsAre(m_entityId1));
     }
 
+    TEST_F(
+        EditorTransformComponentSelectionViewportPickingManipulatorTestFixture, BoundsBetweenCameraAndNearClipPlaneDoesNotIntersectMouseRay)
+    {
+        // move camera to 10 units along the y-axis
+        AzFramework::SetCameraTransform(m_cameraState, AZ::Transform::CreateTranslation(AZ::Vector3::CreateAxisY(10.0f)));
+
+        // send a very narrow bounds for entity1
+        AZ::Entity* entity1 = AzToolsFramework::GetEntityById(m_entityId1);
+        auto* boundTestComponent = entity1->FindComponent<BoundsTestComponent>();
+        boundTestComponent->m_localBounds =
+            AZ::Aabb::CreateFromMinMax(AZ::Vector3(-0.5f, -0.0025f, -0.5f), AZ::Vector3(0.5f, 0.0025f, 0.5f));
+
+        // move entity1 in front of the camera between it and the near clip plane
+        AZ::TransformBus::Event(
+            m_entityId1, &AZ::TransformBus::Events::SetWorldTM, AZ::Transform::CreateTranslation(AZ::Vector3::CreateAxisY(10.05f)));
+        // move entity2 behind entity1
+        AZ::TransformBus::Event(
+            m_entityId2, &AZ::TransformBus::Events::SetWorldTM, AZ::Transform::CreateTranslation(AZ::Vector3::CreateAxisY(15.0f)));
+
+        const auto entity2ScreenPosition = AzFramework::WorldToScreen(AzToolsFramework::GetWorldTranslation(m_entityId2), m_cameraState);
+
+        // click the entity in the viewport
+        m_actionDispatcher->SetStickySelect(true)
+            ->CameraState(m_cameraState)
+            ->MousePosition(entity2ScreenPosition)
+            ->CameraState(m_cameraState)
+            ->MouseLButtonDown()
+            ->MouseLButtonUp();
+
+        // ensure entity1 is not selected as it is before the near clip plane
+        using ::testing::UnorderedElementsAreArray;
+        const AzToolsFramework::EntityIdList selectedEntities = SelectedEntities();
+        const AzToolsFramework::EntityIdList expectedSelectedEntities = { m_entityId2 };
+        EXPECT_THAT(selectedEntities, UnorderedElementsAreArray(expectedSelectedEntities));
+    }
+
     class EditorTransformComponentSelectionViewportPickingManipulatorTestFixtureParam
         : public EditorTransformComponentSelectionViewportPickingManipulatorTestFixture
         , public ::testing::WithParamInterface<bool>

+ 1 - 0
Code/Framework/AzToolsFramework/Tests/EditorVertexSelectionTests.cpp

@@ -23,6 +23,7 @@
 #include <AzManipulatorTestFramework/AzManipulatorTestFrameworkTestHelpers.h>
 #include <AzManipulatorTestFramework/IndirectManipulatorViewportInteraction.h>
 #include <AzManipulatorTestFramework/ImmediateModeActionDispatcher.h>
+#include <Tests/Utils/Printers.h>
 
 using namespace AzToolsFramework;
 

+ 6 - 4
Code/Framework/AzToolsFramework/Tests/Viewport/ViewportScreenTests.cpp

@@ -17,6 +17,7 @@
 #include <AzTest/AzTest.h>
 #include <AzToolsFramework/UnitTest/AzToolsFrameworkTestHelpers.h>
 #include <AzToolsFramework/Viewport/ViewportTypes.h>
+#include <Tests/Utils/Printers.h>
 
 namespace UnitTest
 {
@@ -35,6 +36,7 @@ namespace UnitTest
         const auto worldResult = AzFramework::ScreenToWorld(screenPoint, cameraState);
         return AzFramework::WorldToScreen(worldResult, cameraState);
     }
+
     ////////////////////////////////////////////////////////////////////////////////////////////////////////
     // ScreenPoint tests
     TEST(ViewportScreen, WorldToScreenAndScreenToWorldReturnsTheSameValueIdentityCameraOffsetFromOrigin)
@@ -102,8 +104,8 @@ namespace UnitTest
     }
 
     ////////////////////////////////////////////////////////////////////////////////////////////////////////
-    // NDC tests
-    TEST(ViewportScreen, WorldToScreenNDCAndScreenNDCToWorldReturnsTheSameValueIdentityCameraOffsetFromOrigin)
+    // Ndc tests
+    TEST(ViewportScreen, WorldToScreenNdcAndScreenNdcToWorldReturnsTheSameValueIdentityCameraOffsetFromOrigin)
     {
         using NdcPoint = AZ::Vector2;
 
@@ -136,7 +138,7 @@ namespace UnitTest
         }
     }
 
-    TEST(ViewportScreen, WorldToScreenNDCAndScreenNDCToWorldReturnsTheSameValueOrientatedCamera)
+    TEST(ViewportScreen, WorldToScreenNdcAndScreenNdcToWorldReturnsTheSameValueOrientatedCamera)
     {
         using NdcPoint = AZ::Vector2;
 
@@ -153,7 +155,7 @@ namespace UnitTest
 
     // note: nearClip is 0.1 - the world space value returned will be aligned to the near clip
     // plane of the camera so use that to confirm the mapping to/from is correct
-    TEST(ViewportScreen, ScreenNDCToWorldReturnsPositionOnNearClipPlaneInWorldSpace)
+    TEST(ViewportScreen, ScreenNdcToWorldReturnsPositionOnNearClipPlaneInWorldSpace)
     {
         using NdcPoint = AZ::Vector2;
 

+ 68 - 0
Code/Tools/AssetProcessor/AssetBuilderSDK/AssetBuilderSDK/AssetBuilderSDK.cpp

@@ -23,6 +23,8 @@
 #include <AzCore/RTTI/BehaviorContext.h>
 //////////////////////////////////////////////////////////////////////////
 
+#include <xxhash/xxhash.h>
+
 namespace AssetBuilderSDK
 {
     const char* const ErrorWindow = "Error"; //Use this window name to log error messages.
@@ -1599,4 +1601,70 @@ namespace AssetBuilderSDK
     {
         return m_errorsOccurred;
     }
+
+    AZ::u64 GetHashFromIOStream(AZ::IO::GenericStream& readStream, AZ::IO::SizeType* bytesReadOut, int hashMsDelay)
+    {        
+        constexpr AZ::u64 HashBufferSize = 1024 * 64;
+        char buffer[HashBufferSize];
+
+        if(readStream.IsOpen() && readStream.CanRead())
+        {
+            AZ::IO::SizeType bytesRead;
+
+            auto* state = XXH64_createState();
+
+            if(state == nullptr)
+            {
+                AZ_Assert(false, "Failed to create hash state");
+                return 0;
+            }
+
+            if (XXH64_reset(state, 0) == XXH_ERROR)
+            {
+                AZ_Assert(false, "Failed to reset hash state");
+                return 0;
+            }
+
+            do
+            {
+                // In edge cases where another process is writing to this file while this hashing is occuring and that file wasn't locked,
+                // the following read check can fail because it performs an end of file check, and asserts and shuts down if the read size
+                // was smaller than the buffer and the read is not at the end of the file. The logic used to check end of file internal to read
+                // will be out of date in the edge cases where another process is actively writing to this file while this hash is running.
+                // The stream's length ends up more accurate in this case, preventing this assert and shut down.
+                // One area this occurs is the navigation mesh file (mnmnavmission0.bai) that's temporarily created when exporting a level,
+                // the navigation system can still be writing to this file when hashing begins, causing the EoF marker to change.
+                AZ::IO::SizeType remainingToRead = AZStd::min(readStream.GetLength() - readStream.GetCurPos(), aznumeric_cast<AZ::IO::SizeType>(AZ_ARRAY_SIZE(buffer)));
+                bytesRead = readStream.Read(remainingToRead, buffer);
+
+                if(bytesReadOut)
+                {
+                    *bytesReadOut += bytesRead;
+                }
+
+                XXH64_update(state, buffer, bytesRead);
+
+                // Used by unit tests to force the race condition mentioned above, to verify the crash fix.
+                if(hashMsDelay > 0)
+                {
+                    AZStd::this_thread::sleep_for(AZStd::chrono::milliseconds(hashMsDelay));
+                }
+
+            } while (bytesRead > 0);
+
+            auto hash = XXH64_digest(state);
+
+            XXH64_freeState(state);
+
+            return hash;
+        }
+        return 0;
+    }
+
+    AZ::u64 GetFileHash(const char* filePath, AZ::IO::SizeType* bytesReadOut, int hashMsDelay)
+    {
+        constexpr bool ErrorOnReadFailure = true;
+        AZ::IO::FileIOStream readStream(filePath, AZ::IO::OpenMode::ModeRead | AZ::IO::OpenMode::ModeBinary, ErrorOnReadFailure);
+        return GetHashFromIOStream(readStream, bytesReadOut, hashMsDelay);
+    }
 }

+ 13 - 0
Code/Tools/AssetProcessor/AssetBuilderSDK/AssetBuilderSDK/AssetBuilderSDK.h

@@ -910,6 +910,19 @@ namespace AssetBuilderSDK
         //! There can be multiple builders running at once, so we need to filter out ones coming from other builders
         AZStd::thread_id m_jobThreadId;
     };
+
+    //! Get hash for a whole file
+    //! @filePath the path for the file
+    //! @bytesReadOut output the read file size in bytes
+    //! @hashMsDelay [Do not use except for unit test] add a delay in ms for between each block reading.
+    AZ::u64 GetFileHash(const char* filePath, AZ::IO::SizeType* bytesReadOut = nullptr, int hashMsDelay = 0);
+
+    //! Get hash for a generic IO stream
+    //! @readStream the input readable stream
+    //! @bytesReadOut output the read size in bytes
+    //! @hashMsDelay [Do not use except for unit test] add a delay in ms for between each block reading.
+    AZ::u64 GetHashFromIOStream(AZ::IO::GenericStream& readStream, AZ::IO::SizeType* bytesReadOut = nullptr, int hashMsDelay = 0);
+
 } // namespace AssetBuilderSDK
 
 namespace AZ

+ 1 - 0
Code/Tools/AssetProcessor/AssetBuilderSDK/CMakeLists.txt

@@ -32,6 +32,7 @@ ly_add_target(
         PUBLIC
             AZ::AzFramework
             AZ::AzToolsFramework
+            3rdParty::xxhash
 )
 ly_add_source_properties(
     SOURCES AssetBuilderSDK/AssetBuilderSDK.cpp

+ 2 - 1
Code/Tools/AssetProcessor/Platform/Linux/native/FileWatcher/FileWatcher_linux.cpp

@@ -32,7 +32,8 @@ struct FolderRootWatch::PlatformImplementation
     {
         if (m_iNotifyHandle < 0)
         {
-            m_iNotifyHandle = inotify_init();
+            // The CLOEXEC flag prevents the inotify watchers from copying on fork/exec
+            m_iNotifyHandle = inotify_init1(IN_CLOEXEC);
         }
         return (m_iNotifyHandle >= 0);
     }

+ 4 - 60
Code/Tools/AssetProcessor/native/utilities/assetUtils.cpp

@@ -1161,7 +1161,7 @@ namespace AssetUtilities
     {
 #ifndef AZ_TESTS_ENABLED
         // Only used for unit tests, speed is critical for GetFileHash.
-        AZ_UNUSED(hashMsDelay);
+        hashMsDelay = 0;
 #endif
         bool useFileHashing = ShouldUseFileHashing();
 
@@ -1170,10 +1170,10 @@ namespace AssetUtilities
             return 0;
         }
 
+        AZ::u64 hash = 0;
         if(!force)
         {
             auto* fileStateInterface = AZ::Interface<AssetProcessor::IFileStateRequests>::Get();
-            AZ::u64 hash = 0;
 
             if (fileStateInterface && fileStateInterface->GetHash(filePath, &hash))
             {
@@ -1181,64 +1181,8 @@ namespace AssetUtilities
             }
         }
 
-        char buffer[FileHashBufferSize];
-
-        constexpr bool ErrorOnReadFailure = true;
-        AZ::IO::FileIOStream readStream(filePath, AZ::IO::OpenMode::ModeRead | AZ::IO::OpenMode::ModeBinary, ErrorOnReadFailure);
-
-        if(readStream.IsOpen() && readStream.CanRead())
-        {
-            AZ::IO::SizeType bytesRead;
-
-            auto* state = XXH64_createState();
-
-            if(state == nullptr)
-            {
-                AZ_Assert(false, "Failed to create hash state");
-                return 0;
-            }
-
-            if (XXH64_reset(state, 0) == XXH_ERROR)
-            {
-                AZ_Assert(false, "Failed to reset hash state");
-                return 0;
-            }
-
-            do
-            {
-                // In edge cases where another process is writing to this file while this hashing is occuring and that file wasn't locked,
-                // the following read check can fail because it performs an end of file check, and asserts and shuts down if the read size
-                // was smaller than the buffer and the read is not at the end of the file. The logic used to check end of file internal to read
-                // will be out of date in the edge cases where another process is actively writing to this file while this hash is running.
-                // The stream's length ends up more accurate in this case, preventing this assert and shut down.
-                // One area this occurs is the navigation mesh file (mnmnavmission0.bai) that's temporarily created when exporting a level,
-                // the navigation system can still be writing to this file when hashing begins, causing the EoF marker to change.
-                AZ::IO::SizeType remainingToRead = AZStd::min(readStream.GetLength() - readStream.GetCurPos(), aznumeric_cast<AZ::IO::SizeType>(AZ_ARRAY_SIZE(buffer)));
-                bytesRead = readStream.Read(remainingToRead, buffer);
-
-                if(bytesReadOut)
-                {
-                    *bytesReadOut += bytesRead;
-                }
-
-                XXH64_update(state, buffer, bytesRead);
-#ifdef AZ_TESTS_ENABLED
-                // Used by unit tests to force the race condition mentioned above, to verify the crash fix.
-                if(hashMsDelay > 0)
-                {
-                    AZStd::this_thread::sleep_for(AZStd::chrono::milliseconds(hashMsDelay));
-                }
-#endif
-
-            } while (bytesRead > 0);
-
-            auto hash = XXH64_digest(state);
-
-            XXH64_freeState(state);
-
-            return hash;
-        }
-        return 0;
+        hash = AssetBuilderSDK::GetFileHash(filePath, bytesReadOut, hashMsDelay);
+        return hash;
     }
 
     AZ::u64 AdjustTimestamp(QDateTime timestamp)

+ 0 - 1
Code/Tools/AssetProcessor/native/utilities/assetUtils.h

@@ -238,7 +238,6 @@ namespace AssetUtilities
     // hashMsDelay is only for automated tests to test that writing to a file while it's hashing does not cause a crash.
     // hashMsDelay is not used in non-unit test builds.
     AZ::u64 GetFileHash(const char* filePath, bool force = false, AZ::IO::SizeType* bytesReadOut = nullptr, int hashMsDelay = 0);
-    inline constexpr AZ::u64 FileHashBufferSize = 1024 * 64;
 
     //! Adjusts a timestamp to fix timezone settings and account for any precision adjustment needed
     AZ::u64 AdjustTimestamp(QDateTime timestamp);

+ 11 - 4
Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp

@@ -201,7 +201,7 @@ namespace O3DE::ProjectManager
         }
 
         // add all the gem repos into the hash
-        const AZ::Outcome<QVector<GemInfo>, AZStd::string>& allRepoGemInfosResult = PythonBindingsInterface::Get()->GetAllGemRepoGemsInfos();
+        const AZ::Outcome<QVector<GemInfo>, AZStd::string>& allRepoGemInfosResult = PythonBindingsInterface::Get()->GetAllGemReposGemInfos();
         if (allRepoGemInfosResult.IsSuccess())
         {
             const QVector<GemInfo>& allRepoGemInfos = allRepoGemInfosResult.GetValue();
@@ -378,7 +378,10 @@ namespace O3DE::ProjectManager
         {
             const QString selectedGemPath = m_gemModel->GetPath(modelIndex);
 
-            // Remove gem from gems to be added
+            const bool wasAdded = GemModel::WasPreviouslyAdded(modelIndex);
+            const bool wasAddedDependency = GemModel::WasPreviouslyAddedDependency(modelIndex);
+
+            // Remove gem from gems to be added to update any dependencies
             GemModel::SetIsAdded(*m_gemModel, modelIndex, false);
 
             // Unregister the gem
@@ -406,6 +409,8 @@ namespace O3DE::ProjectManager
 
                 // Select remote gem
                 QModelIndex remoteGemIndex = m_gemModel->FindIndexByNameString(selectedGemName);
+                GemModel::SetWasPreviouslyAdded(*m_gemModel, remoteGemIndex, wasAdded);
+                GemModel::SetWasPreviouslyAddedDependency(*m_gemModel, remoteGemIndex, wasAddedDependency);
                 QModelIndex proxyIndex = m_proxyModel->mapFromSource(remoteGemIndex);
                 m_proxyModel->GetSelectionModel()->setCurrentIndex(proxyIndex, QItemSelectionModel::ClearAndSelect);
             }
@@ -450,7 +455,7 @@ namespace O3DE::ProjectManager
                 m_gemModel->AddGem(gemInfo);
             }
 
-            const AZ::Outcome<QVector<GemInfo>, AZStd::string>& allRepoGemInfosResult = PythonBindingsInterface::Get()->GetAllGemRepoGemsInfos();
+            const AZ::Outcome<QVector<GemInfo>, AZStd::string>& allRepoGemInfosResult = PythonBindingsInterface::Get()->GetAllGemReposGemInfos();
             if (allRepoGemInfosResult.IsSuccess())
             {
                 const QVector<GemInfo>& allRepoGemInfos = allRepoGemInfosResult.GetValue();
@@ -544,7 +549,9 @@ namespace O3DE::ProjectManager
             const QString& gemPath = GemModel::GetPath(modelIndex);
 
             // make sure any remote gems we added were downloaded successfully 
-            if (GemModel::GetGemOrigin(modelIndex) == GemInfo::Remote && GemModel::GetDownloadStatus(modelIndex) != GemInfo::Downloaded)
+            const GemInfo::DownloadStatus status = GemModel::GetDownloadStatus(modelIndex);
+            if (GemModel::GetGemOrigin(modelIndex) == GemInfo::Remote &&
+                !(status == GemInfo::Downloaded || status == GemInfo::DownloadSuccessful))
             {
                 QMessageBox::critical(
                     nullptr, "Cannot add gem that isn't downloaded",

+ 1 - 1
Code/Tools/ProjectManager/Source/GemRepo/GemRepoInfo.h

@@ -37,7 +37,7 @@ namespace O3DE::ProjectManager
         QString m_additionalInfo = "";
         QString m_directoryLink = "";
         QString m_repoUri = "";
-        QStringList m_includedGemPaths = {};
+        QStringList m_includedGemUris = {};
         QDateTime m_lastUpdated;
     };
 } // namespace O3DE::ProjectManager

+ 5 - 2
Code/Tools/ProjectManager/Source/GemRepo/GemRepoInspector.cpp

@@ -8,6 +8,7 @@
 
 #include <GemRepo/GemRepoInspector.h>
 #include <GemRepo/GemRepoItemDelegate.h>
+#include <PythonBindingsInterface.h>
 
 #include <QFrame>
 #include <QLabel>
@@ -60,8 +61,10 @@ namespace O3DE::ProjectManager
 
         // Repo name and url link
         m_nameLabel->setText(m_model->GetName(modelIndex));
-        m_repoLinkLabel->setText(m_model->GetRepoUri(modelIndex));
-        m_repoLinkLabel->SetUrl(m_model->GetRepoUri(modelIndex));
+
+        const QString repoUri = m_model->GetRepoUri(modelIndex);
+        m_repoLinkLabel->setText(repoUri);
+        m_repoLinkLabel->SetUrl(repoUri);
 
         // Repo summary
         m_summaryLabel->setText(m_model->GetSummary(modelIndex));

+ 11 - 15
Code/Tools/ProjectManager/Source/GemRepo/GemRepoModel.cpp

@@ -41,7 +41,7 @@ namespace O3DE::ProjectManager
         item->setData(gemRepoInfo.m_lastUpdated, RoleLastUpdated);
         item->setData(gemRepoInfo.m_path, RolePath);
         item->setData(gemRepoInfo.m_additionalInfo, RoleAdditionalInfo);
-        item->setData(gemRepoInfo.m_includedGemPaths, RoleIncludedGems);
+        item->setData(gemRepoInfo.m_includedGemUris, RoleIncludedGems);
 
         appendRow(item);
 
@@ -98,7 +98,7 @@ namespace O3DE::ProjectManager
         return modelIndex.data(RolePath).toString();
     }
 
-    QStringList GemRepoModel::GetIncludedGemPaths(const QModelIndex& modelIndex)
+    QStringList GemRepoModel::GetIncludedGemUris(const QModelIndex& modelIndex)
     {
         return modelIndex.data(RoleIncludedGems).toStringList();
     }
@@ -118,23 +118,19 @@ namespace O3DE::ProjectManager
 
     QVector<GemInfo> GemRepoModel::GetIncludedGemInfos(const QModelIndex& modelIndex)
     {
-        QVector<GemInfo> allGemInfos;
-        QStringList repoGemPaths = GetIncludedGemPaths(modelIndex);
+        QString repoUri = GetRepoUri(modelIndex);
 
-        for (const QString& gemPath : repoGemPaths)
+        const AZ::Outcome<QVector<GemInfo>, AZStd::string>& gemInfosResult = PythonBindingsInterface::Get()->GetGemRepoGemInfos(repoUri);
+        if (gemInfosResult.IsSuccess())
         {
-            AZ::Outcome<GemInfo> gemInfoResult = PythonBindingsInterface::Get()->GetGemInfo(gemPath);
-            if (gemInfoResult.IsSuccess())
-            {
-                allGemInfos.append(gemInfoResult.GetValue());
-            }
-            else
-            {
-                QMessageBox::critical(nullptr, tr("Gem Not Found"), tr("Cannot find info for gem %1.").arg(gemPath));
-            }
+            return gemInfosResult.GetValue();
+        }
+        else
+        {
+            QMessageBox::critical(nullptr, tr("Gems not found"), tr("Cannot find info for gems from repo %1").arg(GetName(modelIndex)));
         }
 
-        return allGemInfos;
+        return QVector<GemInfo>();
     }
 
     bool GemRepoModel::IsEnabled(const QModelIndex& modelIndex)

Неке датотеке нису приказане због велике количине промена