diff --git a/AutomatedTesting/Gem/PythonTests/EditorPythonTestTools/editor_python_test_tools/hydra_test_utils.py b/AutomatedTesting/Gem/PythonTests/EditorPythonTestTools/editor_python_test_tools/hydra_test_utils.py index 3d4d9ea419..54bc118f48 100644 --- a/AutomatedTesting/Gem/PythonTests/EditorPythonTestTools/editor_python_test_tools/hydra_test_utils.py +++ b/AutomatedTesting/Gem/PythonTests/EditorPythonTestTools/editor_python_test_tools/hydra_test_utils.py @@ -81,7 +81,8 @@ def launch_and_validate_results(request, test_directory, editor, editor_script, def launch_and_validate_results_launcher(launcher, level, remote_console_instance, expected_lines, null_renderer=True, - port_listener_timeout=120, log_monitor_timeout=300, remote_console_port=4600): + port_listener_timeout=120, log_monitor_timeout=300, remote_console_port=4600, + launch_ap=True): """ Runs the launcher with the specified level, and monitors Game.log for expected lines. :param launcher: Configured launcher object to run test against. @@ -92,6 +93,7 @@ def launch_and_validate_results_launcher(launcher, level, remote_console_instanc :param port_listener_timeout: Timeout for verifying successful connection to Remote Console. :param log_monitor_timeout: Timeout for monitoring for lines in Game.log :param remote_console_port: The port used to communicate with the Remote Console. + :param launch_ap: Whether or not to launch AP. Defaults to True. """ def _check_for_listening_port(port): @@ -110,7 +112,7 @@ def launch_and_validate_results_launcher(launcher, level, remote_console_instanc launcher.args.extend(["-rhi=Null"]) # Start the Launcher - with launcher.start(): + with launcher.start(launch_ap=launch_ap): # Ensure Remote Console can be reached waiter.wait_for( diff --git a/AutomatedTesting/Gem/PythonTests/atom_renderer/CMakeLists.txt b/AutomatedTesting/Gem/PythonTests/atom_renderer/CMakeLists.txt index f91423324d..117c710824 100644 --- a/AutomatedTesting/Gem/PythonTests/atom_renderer/CMakeLists.txt +++ b/AutomatedTesting/Gem/PythonTests/atom_renderer/CMakeLists.txt @@ -65,15 +65,4 @@ if(PAL_TRAIT_BUILD_HOST_TOOLS AND PAL_TRAIT_BUILD_TESTS_SUPPORTED AND AutomatedT COMPONENT Atom ) - ly_add_pytest( - NAME AutomatedTesting::AtomRenderer_HydraTests_ShaderBuildPipeline - TEST_SUITE main - PATH ${CMAKE_CURRENT_LIST_DIR}/test_Atom_ShaderBuildPipelineSuite.py - TEST_SERIAL - TIMEOUT 600 - RUNTIME_DEPENDENCIES - AssetProcessor - AutomatedTesting.Assets - Editor - ) endif() diff --git a/AutomatedTesting/Gem/PythonTests/atom_renderer/atom_hydra_scripts/hydra_ShaderAssetBuilder_RecompilesShaderAsChainOfDependenciesChanges.py b/AutomatedTesting/Gem/PythonTests/atom_renderer/atom_hydra_scripts/hydra_ShaderAssetBuilder_RecompilesShaderAsChainOfDependenciesChanges.py index a05420d960..f13227aa9d 100644 --- a/AutomatedTesting/Gem/PythonTests/atom_renderer/atom_hydra_scripts/hydra_ShaderAssetBuilder_RecompilesShaderAsChainOfDependenciesChanges.py +++ b/AutomatedTesting/Gem/PythonTests/atom_renderer/atom_hydra_scripts/hydra_ShaderAssetBuilder_RecompilesShaderAsChainOfDependenciesChanges.py @@ -3,78 +3,19 @@ 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 - """ -import os -import shutil - -def _copy_file(src_file, src_path, target_file, target_path): - # type: (str, str, str, str) -> None - """ - Copies the [src_file] located in [src_path] to the [target_file] located at [target_path]. - Leaves the [target_file] unlocked for reading and writing privileges - :param src_file: The source file to copy (file name) - :param src_path: The source file's path - :param target_file: The target file to copy into (file name) - :param target_path: The target file's path - :return: None - """ - target_file_path = os.path.join(target_path, target_file) - src_file_path = os.path.join(src_path, src_file) - if os.path.exists(target_file_path): - fs.unlock_file(target_file_path) - shutil.copyfile(src_file_path, target_file_path) - -def _copy_tmp_files_in_order(src_directory, file_list, dst_directory, wait_time_in_between = 0.0): - # type: (str, list, str, float) -> None - """ - This function assumes that for each file name listed in @file_list - there's file named "@filename.txt" which the original source file - but they will be copied with just the @filename (.txt removed). - """ - for filename in file_list: - src_name = f"{filename}.txt" - _copy_file(src_name, src_directory, filename, dst_directory) - if wait_time_in_between > 0.0: - print(f"Created {filename} in {dst_directory}") - general.idle_wait(wait_time_in_between) - - -def _remove_file(src_file, src_path): - # type: (str, str) -> None - """ - Removes the [src_file] located in [src_path]. - :param src_file: The source file to copy (file name) - :param src_path: The source file's path - :return: None - """ - src_file_path = os.path.join(src_path, src_file) - if os.path.exists(src_file_path): - fs.unlock_file(src_file_path) - os.remove(src_file_path) - - -def _remove_files(directory, file_list): - for filename in file_list: - _remove_file(filename, directory) - - -def _asset_exists(cache_relative_path): - asset_id = azasset.AssetCatalogRequestBus(azbus.Broadcast, "GetAssetIdByPath", cache_relative_path, azmath.Uuid(), False) - return asset_id.is_valid() - -# List of results that we want to check, this is not 100% necessary but it's a good -# practice to make it easier to debug tests. -# Here we define a tuple of tests -class Results(): - azshader_was_removed = ("azshader was removed", "Failed to remove azshader") - azshader_was_compiled = ("azshader was compiled", "Failed to compile azshader") +# fmt: off +class Tests(): + azshader_was_removed = ("azshader was removed", "Failed to remove azshader") + azshader_was_compiled = ("azshader was compiled", "Failed to compile azshader") + no_error_occurred = ("No errors detected", "Errors were detected") +# fmt: on def ShaderAssetBuilder_RecompilesShaderAsChainOfDependenciesChanges(): """ - This test validates [ATOM-5441] Shader Builders May Fail When Multiple New Files Are Added + This test validates: "Shader Builders May Fail When Multiple New Files Are Added" It creates source assets to compile a particular shader. 1- The first phase generates the source assets out of order and slowly. The AP should wakeup each time one of the source dependencies appears but will fail each time. Only when the @@ -82,6 +23,71 @@ def ShaderAssetBuilder_RecompilesShaderAsChainOfDependenciesChanges(): 2- The second phase is similar as above, except that all source assets will be created at once and We also expect that in the end the shader is built successfully. """ + import os + import shutil + + import azlmbr.asset as azasset + import azlmbr.bus as azbus + import azlmbr.legacy.general as general + import azlmbr.math as azmath + + from editor_python_test_tools.utils import TestHelper as helper + from editor_python_test_tools.utils import Tracer + import ly_test_tools.environment.file_system as fs + + def _copy_file(src_file, src_path, target_file, target_path): + # type: (str, str, str, str) -> None + """ + Copies the [src_file] located in [src_path] to the [target_file] located at [target_path]. + Leaves the [target_file] unlocked for reading and writing privileges + :param src_file: The source file to copy (file name) + :param src_path: The source file's path + :param target_file: The target file to copy into (file name) + :param target_path: The target file's path + :return: None + """ + target_file_path = os.path.join(target_path, target_file) + src_file_path = os.path.join(src_path, src_file) + if os.path.exists(target_file_path): + fs.unlock_file(target_file_path) + shutil.copyfile(src_file_path, target_file_path) + + def _copy_tmp_files_in_order(src_directory, file_list, dst_directory, wait_time_in_between=0.0): + # type: (str, list, str, float) -> None + """ + This function assumes that for each file name listed in @file_list + there's file named "@filename.txt" which the original source file + but they will be copied with just the @filename (.txt removed). + """ + for filename in file_list: + src_name = f"{filename}.txt" + _copy_file(src_name, src_directory, filename, dst_directory) + if wait_time_in_between > 0.0: + print(f"Created {filename} in {dst_directory}") + general.idle_wait(wait_time_in_between) + + def _remove_file(src_file, src_path): + # type: (str, str) -> None + """ + Removes the [src_file] located in [src_path]. + :param src_file: The source file to copy (file name) + :param src_path: The source file's path + :return: None + """ + src_file_path = os.path.join(src_path, src_file) + if os.path.exists(src_file_path): + fs.unlock_file(src_file_path) + os.remove(src_file_path) + + def _remove_files(directory, file_list): + for filename in file_list: + _remove_file(filename, directory) + + def _asset_exists(cache_relative_path): + asset_id = azasset.AssetCatalogRequestBus(azbus.Broadcast, "GetAssetIdByPath", cache_relative_path, + azmath.Uuid(), False) + return asset_id.is_valid() + # Required for automated tests helper.init_idle() @@ -115,14 +121,14 @@ def ShaderAssetBuilder_RecompilesShaderAsChainOfDependenciesChanges(): azshader_name = "assets/dependencyvalidation.azshader" helper.wait_for_condition(lambda: not _asset_exists(azshader_name), 5.0) - Report.critical_result(Results.azshader_was_removed, not _asset_exists(azshader_name)) + Report.critical_result(Tests.azshader_was_removed, not _asset_exists(azshader_name)) _copy_tmp_files_in_order(src_assets_subdir, file_list, game_asset_path, 1.0) # Give enough time to AP to compile the shader helper.wait_for_condition(lambda: _asset_exists(azshader_name), 60.0) - Report.critical_result(Results.azshader_was_compiled, _asset_exists(azshader_name)) + Report.critical_result(Tests.azshader_was_compiled, _asset_exists(azshader_name)) # The first part was about compiling the shader under normal conditions. # Let's remove the files from the previous phase and will proceed @@ -130,7 +136,7 @@ def ShaderAssetBuilder_RecompilesShaderAsChainOfDependenciesChanges(): # ShaderAssetBuilder will only succeed when the last file becomes visible. _remove_files(game_asset_path, reverse_file_list) helper.wait_for_condition(lambda: not _asset_exists(azshader_name), 5.0) - Report.critical_result(Results.azshader_was_removed, not _asset_exists(azshader_name)) + Report.critical_result(Tests.azshader_was_removed, not _asset_exists(azshader_name)) # Remark, if you are running this test manually from the Editor with "pyRunFile", # You'll notice how the AP issues notifications that it fails to compile the shader @@ -148,7 +154,7 @@ def ShaderAssetBuilder_RecompilesShaderAsChainOfDependenciesChanges(): # Give enough time to AP to compile the shader helper.wait_for_condition(lambda: _asset_exists(azshader_name), 60.0) - Report.critical_result(Results.azshader_was_compiled, _asset_exists(azshader_name)) + Report.critical_result(Tests.azshader_was_compiled, _asset_exists(azshader_name)) # The last phase of the test puts stress on potential race conditions # when all required files appear as soon as possible. @@ -157,7 +163,7 @@ def ShaderAssetBuilder_RecompilesShaderAsChainOfDependenciesChanges(): # Remove left over files. _remove_files(game_asset_path, reverse_file_list) helper.wait_for_condition(lambda: not _asset_exists(azshader_name), 5.0) - Report.critical_result(Results.azshader_was_removed, not _asset_exists(azshader_name)) + Report.critical_result(Tests.azshader_was_removed, not _asset_exists(azshader_name)) # Now let's copy all the source files to the "Assets" folder as fast as possible. _copy_tmp_files_in_order(src_assets_subdir, reverse_file_list, game_asset_path) @@ -165,24 +171,17 @@ def ShaderAssetBuilder_RecompilesShaderAsChainOfDependenciesChanges(): # Give enough time to AP to compile the shader helper.wait_for_condition(lambda: _asset_exists(azshader_name), 60.0) - Report.critical_result(Results.azshader_was_compiled, _asset_exists(azshader_name)) + Report.critical_result(Tests.azshader_was_compiled, _asset_exists(azshader_name)) # All good, let's cleanup leftover files before closing the test. _remove_files(game_asset_path, reverse_file_list) helper.wait_for_condition(lambda: not _asset_exists(azshader_name), 5.0) + # Look for errors to raise. + helper.wait_for_condition(lambda: error_tracer.has_errors, 1.0) + Report.result(Tests.no_error_occurred, not error_tracer.has_errors) + if __name__ == "__main__": - # All exposed python bindings are in azlmbr - import azlmbr.legacy.general as general - import azlmbr.bus as azbus - import azlmbr.asset as azasset - import azlmbr.math as azmath - - # Import report and test helper utilities from editor_python_test_tools.utils import Report - from editor_python_test_tools.utils import TestHelper as helper - from editor_python_test_tools.utils import Tracer - import ly_test_tools.environment.file_system as fs - - Report.start_test(ShaderAssetBuilder_RecompilesShaderAsChainOfDependenciesChanges) \ No newline at end of file + Report.start_test(ShaderAssetBuilder_RecompilesShaderAsChainOfDependenciesChanges) diff --git a/AutomatedTesting/Gem/PythonTests/atom_renderer/test_Atom_MainSuite_Optimized.py b/AutomatedTesting/Gem/PythonTests/atom_renderer/test_Atom_MainSuite_Optimized.py index 329d3ecb91..e206e77d60 100644 --- a/AutomatedTesting/Gem/PythonTests/atom_renderer/test_Atom_MainSuite_Optimized.py +++ b/AutomatedTesting/Gem/PythonTests/atom_renderer/test_Atom_MainSuite_Optimized.py @@ -41,3 +41,6 @@ class TestAutomation(EditorTestSuite): class AtomEditorComponents_DisplayMapperAdded(EditorSharedTest): from atom_renderer.atom_hydra_scripts import hydra_AtomEditorComponents_DisplayMapperAdded as test_module + + class ShaderAssetBuilder_RecompilesShaderAsChainOfDependenciesChanges(EditorSharedTest): + from .atom_hydra_scripts import hydra_ShaderAssetBuilder_RecompilesShaderAsChainOfDependenciesChanges as test_module diff --git a/AutomatedTesting/Gem/PythonTests/atom_renderer/test_Atom_ShaderBuildPipelineSuite.py b/AutomatedTesting/Gem/PythonTests/atom_renderer/test_Atom_ShaderBuildPipelineSuite.py deleted file mode 100644 index 9ef93ea238..0000000000 --- a/AutomatedTesting/Gem/PythonTests/atom_renderer/test_Atom_ShaderBuildPipelineSuite.py +++ /dev/null @@ -1,19 +0,0 @@ -""" -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 - -Main suite tests for the Shader Build Pipeline. -""" -import pytest -from ly_test_tools import LAUNCHERS -from ly_test_tools.o3de.editor_test import EditorTestSuite, EditorSingleTest - -@pytest.mark.parametrize("project", ["AutomatedTesting"]) -@pytest.mark.parametrize("launcher_platform", ['windows_editor']) -class TestShaderBuildPipelineMain(EditorTestSuite): - """Holds tests for Shader Build Pipeline validation""" - - class ShaderAssetBuilder_RecompilesShaderAsChainOfDependenciesChanges(EditorSingleTest): - from .atom_hydra_scripts import hydra_ShaderAssetBuilder_RecompilesShaderAsChainOfDependenciesChanges as test_module \ No newline at end of file diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/CMakeLists.txt b/AutomatedTesting/Gem/PythonTests/largeworlds/CMakeLists.txt index b3030e84ac..f1299ddc2d 100644 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/CMakeLists.txt +++ b/AutomatedTesting/Gem/PythonTests/largeworlds/CMakeLists.txt @@ -14,8 +14,7 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED AND PAL_TRAIT_BUILD_HOST_TOOLS AND PAL_TRAIT_ NAME AutomatedTesting::DynamicVegetationTests_Main TEST_SERIAL TEST_SUITE main - PATH ${CMAKE_CURRENT_LIST_DIR}/dyn_veg - PYTEST_MARKS "not SUITE_sandbox and not SUITE_periodic and not SUITE_benchmark" + PATH ${CMAKE_CURRENT_LIST_DIR}/dyn_veg/TestSuite_Main.py RUNTIME_DEPENDENCIES AZ::AssetProcessor Legacy::Editor @@ -27,104 +26,33 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED AND PAL_TRAIT_BUILD_HOST_TOOLS AND PAL_TRAIT_ ly_add_pytest( - NAME AutomatedTesting::DynamicVegetationTests_Sandbox + NAME AutomatedTesting::DynamicVegetationTests_Periodic TEST_SERIAL - TEST_SUITE sandbox - PATH ${CMAKE_CURRENT_LIST_DIR}/dyn_veg - PYTEST_MARKS "SUITE_sandbox" + TEST_SUITE periodic + PATH ${CMAKE_CURRENT_LIST_DIR}/dyn_veg/TestSuite_Periodic.py RUNTIME_DEPENDENCIES AZ::AssetProcessor Legacy::Editor + AutomatedTesting.Assets AutomatedTesting.GameLauncher - AutomatedTesting.Assets COMPONENT LargeWorlds ) ly_add_pytest( - NAME AutomatedTesting::DynamicVegetationFilterTests_Periodic + NAME AutomatedTesting::DynamicVegetationTests_Main_Optimized TEST_SERIAL - TEST_SUITE periodic - PATH ${CMAKE_CURRENT_LIST_DIR}/dyn_veg - PYTEST_MARKS "SUITE_periodic and dynveg_filter" + TEST_SUITE main + PATH ${CMAKE_CURRENT_LIST_DIR}/dyn_veg/TestSuite_Main_Optimized.py RUNTIME_DEPENDENCIES - AZ::AssetProcessor - Legacy::Editor - AutomatedTesting.Assets + AZ::AssetProcessor + Legacy::Editor + AutomatedTesting.Assets + AutomatedTesting.GameLauncher COMPONENT LargeWorlds ) - ly_add_pytest( - NAME AutomatedTesting::DynamicVegetationModifierTests_Periodic - TEST_SERIAL - TEST_SUITE periodic - PATH ${CMAKE_CURRENT_LIST_DIR}/dyn_veg - PYTEST_MARKS "SUITE_periodic and dynveg_modifier" - RUNTIME_DEPENDENCIES - AZ::AssetProcessor - Legacy::Editor - AutomatedTesting.Assets - COMPONENT - LargeWorlds - ) - - ly_add_pytest( - NAME AutomatedTesting::DynamicVegetationRegressionTests_Periodic - TEST_SERIAL - TEST_SUITE periodic - PATH ${CMAKE_CURRENT_LIST_DIR}/dyn_veg - PYTEST_MARKS "SUITE_periodic and dynveg_regression" - RUNTIME_DEPENDENCIES - AZ::AssetProcessor - Legacy::Editor - AutomatedTesting.Assets - COMPONENT - LargeWorlds - ) - - ly_add_pytest( - NAME AutomatedTesting::DynamicVegetationAreaTests_Periodic - TEST_SERIAL - TEST_SUITE periodic - PATH ${CMAKE_CURRENT_LIST_DIR}/dyn_veg - PYTEST_MARKS "SUITE_periodic and dynveg_area" - RUNTIME_DEPENDENCIES - AZ::AssetProcessor - Legacy::Editor - AutomatedTesting.Assets - COMPONENT - LargeWorlds - ) - - ly_add_pytest( - NAME AutomatedTesting::DynamicVegetationMiscTests_Periodic - TEST_SERIAL - TEST_SUITE periodic - PATH ${CMAKE_CURRENT_LIST_DIR}/dyn_veg - PYTEST_MARKS "SUITE_periodic and dynveg_misc" - RUNTIME_DEPENDENCIES - AZ::AssetProcessor - Legacy::Editor - AutomatedTesting.Assets - COMPONENT - LargeWorlds - ) - - ly_add_pytest( - NAME AutomatedTesting::DynamicVegetationSurfaceTagTests_Periodic - TEST_SERIAL - TEST_SUITE periodic - PATH ${CMAKE_CURRENT_LIST_DIR}/dyn_veg - PYTEST_MARKS "SUITE_periodic and dynveg_surfacetagemitter" - RUNTIME_DEPENDENCIES - AZ::AssetProcessor - Legacy::Editor - AutomatedTesting.Assets - COMPONENT - LargeWorlds - ) - ## LandscapeCanvas ## ly_add_pytest( diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/AltitudeFilter_ComponentAndOverrides_InstancesPlantAtSpecifiedAltitude.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/AltitudeFilter_ComponentAndOverrides_InstancesPlantAtSpecifiedAltitude.py index 928d38ac5a..e404624c93 100755 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/AltitudeFilter_ComponentAndOverrides_InstancesPlantAtSpecifiedAltitude.py +++ b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/AltitudeFilter_ComponentAndOverrides_InstancesPlantAtSpecifiedAltitude.py @@ -5,119 +5,119 @@ For complete copyright and license terms please see the LICENSE at the root of t SPDX-License-Identifier: Apache-2.0 OR MIT """ -""" -C4814463 - Altitude Filter overrides function as expected -C4847477 - Altitude Min/Max can be manually set -""" -import os -import sys - -sys.path.append(os.path.dirname(os.path.abspath(__file__))) -import azlmbr.editor as editor -import azlmbr.legacy.general as general -import azlmbr.bus as bus -import azlmbr.math as math -import azlmbr.paths - -sys.path.append(os.path.join(azlmbr.paths.devroot, 'AutomatedTesting', 'Gem', 'PythonTests')) -import editor_python_test_tools.hydra_editor_utils as hydra -from editor_python_test_tools.editor_test_helper import EditorTestHelper -from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg +class Tests: + prefilter_instance_count = ( + "Pre-filter instance counts are accurate", + "Unexpected number of pre-filter instances found" + ) + postfilter_instance_count = ( + "Post-filter instance counts are accurate", + "Unexpected number of post-filter instances found" + ) + postfilter_overrides_instance_count = ( + "Override instance counts are accurate", + "Unexpected number of override instances found" + ) -class TestAltitudeFilterComponentAndOverrides(EditorTestHelper): - def __init__(self): - EditorTestHelper.__init__(self, log_prefix="AltitudeFilterComponentAndOverrides", args=["level"]) +def AltitudeFilter_ComponentAndOverrides_InstancesPlantAtSpecifiedAltitude(): + """ + Summary: + A new level is created. A spawner entity is added, along with a planting surface at 32 on Z, and another at 36 + on Z. An Altitude Filter is added to the spawner entity, and Altitude Min/Max values are set. Instance counts + are validated. The same test is then performed for Altitude Filter overrides. - def run_test(self): - """ - Summary: - A new level is created. A spawner entity is added, along with a planting surface at 32 on Z, and another at 36 - on Z. An Altitude Filter is added to the spawner entity, and Altitude Min/Max values are set. Instance counts - are validated. The same test is then performed for Altitude Filter overrides. + Expected Behavior: + Instances are only spawned within the specified altitude ranges. - Expected Behavior: - Instances are only spawned within the specified altitude ranges. + Test Steps: + 1) Open a simple level + 2) Create an instance spawner entity + 3) Create surfaces to plant on, one at 32 on Z and another at 36 on Z. + 4) Initial instance counts pre-filter are verified. + 5) Altitude Min/Max is set on the Vegetation Altitude Filter component. + 6) Instance counts post-filter are verified. + 7) Altitude Min/Max is set on descriptor overrides. + 8) Instance counts post-filter are verified. - Test Steps: - 1) Create a new level - 2) Create an instance spawner entity - 3) Create surfaces to plant on, one at 32 on Z and another at 36 on Z. - 4) Initial instance counts pre-filter are verified. - 5) Altitude Min/Max is set on the Vegetation Altitude Filter component. - 6) Instance counts post-filter are verified. - 7) Altitude Min/Max is set on descriptor overrides. - 8) Instance counts post-filter are verified. + Note: + - This test file must be called from the Open 3D Engine Editor command terminal + - Any passed and failed tests are written to the Editor.log file. + Parsing the file or running a log_monitor are required to observe the test results. - Note: - - This test file must be called from the Open 3D Engine Editor command terminal - - Any passed and failed tests are written to the Editor.log file. - Parsing the file or running a log_monitor are required to observe the test results. + :return: None + """ - :return: None - """ + import os - # 1) Create a new, temporary level - self.test_success = self.create_level( - self.args["level"], - heightmap_resolution=1024, - heightmap_meters_per_pixel=1, - terrain_texture_resolution=4096, - use_terrain=False, - ) + import azlmbr.editor as editor + import azlmbr.legacy.general as general + import azlmbr.bus as bus + import azlmbr.math as math - # Set view of planting area for visual debugging - general.set_current_view_position(512.0, 500.0, 38.0) - general.set_current_view_rotation(-20.0, 0.0, 0.0) + import editor_python_test_tools.hydra_editor_utils as hydra + from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg + from editor_python_test_tools.utils import Report + from editor_python_test_tools.utils import TestHelper as helper - # 2) Create a new entity with required vegetation area components - center_point = math.Vector3(512.0, 512.0, 32.0) - asset_path = os.path.join("Slices", "PinkFlower.dynamicslice") - spawner_entity = dynveg.create_vegetation_area("Instance Spawner", center_point, 32.0, 32.0, 32.0, asset_path) + # 1) Open an existing simple level + helper.init_idle() + helper.open_level("Physics", "Base") - # Add a Vegetation Altitude Filter - spawner_entity.add_component("Vegetation Altitude Filter") + # Set view of planting area for visual debugging + general.set_current_view_position(512.0, 500.0, 38.0) + general.set_current_view_rotation(-20.0, 0.0, 0.0) - # 3) Add surfaces to plant on - dynveg.create_surface_entity("Planting Surface", center_point, 32.0, 32.0, 1.0) - elevated_surface_center_point = math.Vector3(512.0, 512.0, 36.0) - dynveg.create_surface_entity("Planting Surface Elevated", elevated_surface_center_point, 32.0, 32.0, 1.0) + # 2) Create a new entity with required vegetation area components + center_point = math.Vector3(512.0, 512.0, 32.0) + asset_path = os.path.join("Slices", "PinkFlower.dynamicslice") + spawner_entity = dynveg.create_vegetation_area("Instance Spawner", center_point, 32.0, 32.0, 32.0, asset_path) - # Set instances to spawn on a center snap point to avoid unexpected instances around the edges of the box shape - veg_system_settings_component = hydra.add_level_component("Vegetation System Settings") - editor.EditorComponentAPIBus(bus.Broadcast, "SetComponentProperty", veg_system_settings_component, - 'Configuration|Area System Settings|Sector Point Snap Mode', 1) + # Add a Vegetation Altitude Filter + spawner_entity.add_component("Vegetation Altitude Filter") - # 4) Verify initial instance counts pre-filter - num_expected = (40 * 40) * 2 # 20 instances per 16m per side x 2 surfaces - spawner_success = self.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, num_expected), 5.0) - self.test_success = self.test_success and spawner_success + # 3) Add surfaces to plant on + dynveg.create_surface_entity("Planting Surface", center_point, 32.0, 32.0, 1.0) + elevated_surface_center_point = math.Vector3(512.0, 512.0, 36.0) + dynveg.create_surface_entity("Planting Surface Elevated", elevated_surface_center_point, 32.0, 32.0, 1.0) - # 5) Set min/max vegetation altitude, instances should now only appear between 35-37m on the Z-axis - spawner_entity.get_set_test(3, "Configuration|Altitude Min", 35) - spawner_entity.get_set_test(3, "Configuration|Altitude Max", 37) + # Set instances to spawn on a center snap point to avoid unexpected instances around the edges of the box shape + veg_system_settings_component = hydra.add_level_component("Vegetation System Settings") + editor.EditorComponentAPIBus(bus.Broadcast, "SetComponentProperty", veg_system_settings_component, + 'Configuration|Area System Settings|Sector Point Snap Mode', 1) - # 6) Validate expected instance counts - num_expected = 40 * 40 # Instances should now only plant on the elevated surface - altitude_min_max_success = self.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, num_expected), 5.0) - self.test_success = self.test_success and altitude_min_max_success + # 4) Verify initial instance counts pre-filter + num_expected = (40 * 40) * 2 # 20 instances per 16m per side x 2 surfaces + spawner_success = helper.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, num_expected), 5.0) + Report.result(Tests.prefilter_instance_count, spawner_success) - # Resize Spawner Entity's Box Shape component to allow monitoring for a different instance count - box_size = math.Vector3(16.0, 16.0, 16.0) - spawner_entity.get_set_test(1, "Box Shape|Box Configuration|Dimensions", box_size) + # 5) Set min/max vegetation altitude, instances should now only appear between 35-37m on the Z-axis + spawner_entity.get_set_test(3, "Configuration|Altitude Min", 35) + spawner_entity.get_set_test(3, "Configuration|Altitude Max", 37) - # 7) Allow overrides on Altitude Filter and set Altitude Filter Min/Max overrides on descriptor - spawner_entity.get_set_test(3, "Configuration|Allow Per-Item Overrides", True) - spawner_entity.get_set_test(2, "Configuration|Embedded Assets|[0]|Altitude Filter|Override Enabled", True) - spawner_entity.get_set_test(2, "Configuration|Embedded Assets|[0]|Altitude Filter|Min", 35) - spawner_entity.get_set_test(2, "Configuration|Embedded Assets|[0]|Altitude Filter|Max", 37) + # 6) Validate expected instance counts + num_expected = 40 * 40 # Instances should now only plant on the elevated surface + altitude_min_max_success = helper.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, num_expected), 5.0) + Report.result(Tests.postfilter_instance_count, altitude_min_max_success) - # 8) Validate expected instances at specified elevations - num_expected = 20 * 20 # 20 instances per 16m per side - overrides_success = self.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, num_expected), 5.0) - self.test_success = self.test_success and overrides_success + # Resize Spawner Entity's Box Shape component to allow monitoring for a different instance count + box_size = math.Vector3(16.0, 16.0, 16.0) + spawner_entity.get_set_test(1, "Box Shape|Box Configuration|Dimensions", box_size) + + # 7) Allow overrides on Altitude Filter and set Altitude Filter Min/Max overrides on descriptor + spawner_entity.get_set_test(3, "Configuration|Allow Per-Item Overrides", True) + spawner_entity.get_set_test(2, "Configuration|Embedded Assets|[0]|Altitude Filter|Override Enabled", True) + spawner_entity.get_set_test(2, "Configuration|Embedded Assets|[0]|Altitude Filter|Min", 35) + spawner_entity.get_set_test(2, "Configuration|Embedded Assets|[0]|Altitude Filter|Max", 37) + + # 8) Validate expected instances at specified elevations + num_expected = 20 * 20 # 20 instances per 16m per side + overrides_success = helper.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, num_expected), 5.0) + Report.result(Tests.postfilter_overrides_instance_count, overrides_success) -test = TestAltitudeFilterComponentAndOverrides() -test.run() +if __name__ == "__main__": + + from editor_python_test_tools.utils import Report + Report.start_test(AltitudeFilter_ComponentAndOverrides_InstancesPlantAtSpecifiedAltitude) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/AltitudeFilter_FilterStageToggle.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/AltitudeFilter_FilterStageToggle.py index 2ef72f72eb..3fc6a0afde 100755 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/AltitudeFilter_FilterStageToggle.py +++ b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/AltitudeFilter_FilterStageToggle.py @@ -5,90 +5,88 @@ For complete copyright and license terms please see the LICENSE at the root of t SPDX-License-Identifier: Apache-2.0 OR MIT """ -import os -import sys -import azlmbr.bus as bus -import azlmbr.components as components -import azlmbr.editor as editor -import azlmbr.legacy.general as general -import azlmbr.math as math -import azlmbr.paths - -sys.path.append(os.path.join(azlmbr.paths.devroot, "AutomatedTesting", "Gem", "PythonTests")) -import editor_python_test_tools.hydra_editor_utils as hydra -from editor_python_test_tools.editor_test_helper import EditorTestHelper -from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg +class Tests: + preprocess_instance_count = ( + "Pre-process instance counts are accurate", + "Unexpected number of pre-process instances found" + ) + postprocess_instance_count = ( + "Post-process instance counts are accurate", + "Unexpected number of post-process instances found" + ) -class TestAltitudeFilterFilterStageToggle(EditorTestHelper): - def __init__(self): - EditorTestHelper.__init__(self, log_prefix="AltitudeFilter_FilterStageToggle", args=["level"]) +def AltitudeFilter_FilterStageToggle(): + """ + Summary: + Filter Stage toggle affects final vegetation position - def run_test(self): - """ - Summary: - Filter Stage toggle affects final vegetation position + Expected Result: + Vegetation instances plant differently depending on the Filter Stage setting. + PostProcess should cause some number of plants that appear above and below the desired altitude range to disappear. - Expected Result: - Vegetation instances plant differently depending on the Filter Stage setting. - PostProcess should cause some number of plants that appear above and below the desired altitude range to disappear. + :return: None + """ - :return: None - """ + import os - PREPROCESS_INSTANCE_COUNT = 44 - POSTPROCESS_INSTANCE_COUNT = 34 + import azlmbr.legacy.general as general + import azlmbr.math as math - # Create empty level - self.test_success = self.create_level( - self.args["level"], - heightmap_resolution=1024, - heightmap_meters_per_pixel=1, - terrain_texture_resolution=4096, - use_terrain=False, - ) + import editor_python_test_tools.hydra_editor_utils as hydra + from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg + from editor_python_test_tools.utils import Report + from editor_python_test_tools.utils import TestHelper as helper - general.set_current_view_position(512.0, 480.0, 38.0) + PREPROCESS_INSTANCE_COUNT = 44 + POSTPROCESS_INSTANCE_COUNT = 34 - # Create basic vegetation entity - position = math.Vector3(512.0, 512.0, 32.0) - asset_path = os.path.join("Slices", "PinkFlower.dynamicslice") - vegetation = dynveg.create_vegetation_area("vegetation", position, 16.0, 16.0, 16.0, asset_path) + # Open an existing simple level + helper.init_idle() + helper.open_level("Physics", "Base") + general.set_current_view_position(512.0, 480.0, 38.0) - # Add a Vegetation Altitude Filter to the vegetation area entity - vegetation.add_component("Vegetation Altitude Filter") + # Create basic vegetation entity + position = math.Vector3(512.0, 512.0, 32.0) + asset_path = os.path.join("Slices", "PinkFlower.dynamicslice") + vegetation = dynveg.create_vegetation_area("vegetation", position, 16.0, 16.0, 16.0, asset_path) - # Create Surface for instances to plant on - dynveg.create_surface_entity("Surface_Entity_Parent", position, 16.0, 16.0, 1.0) + # Add a Vegetation Altitude Filter to the vegetation area entity + vegetation.add_component("Vegetation Altitude Filter") - # Add entity with Mesh to replicate creation of hills - hill_entity = dynveg.create_mesh_surface_entity_with_slopes("hill", position, 10.0) + # Create Surface for instances to plant on + dynveg.create_surface_entity("Surface_Entity_Parent", position, 16.0, 16.0, 1.0) - # Set a Min Altitude of 38 and Max of 40 in Vegetation Altitude Filter - vegetation.get_set_test(3, "Configuration|Altitude Min", 38.0) - vegetation.get_set_test(3, "Configuration|Altitude Max", 40.0) + # Add entity with Mesh to replicate creation of hills + hill_entity = dynveg.create_mesh_surface_entity_with_slopes("hill", position, 10.0) - # Create a new entity as a child of the vegetation area entity with Random Noise Gradient Generator, Gradient - # Transform Modifier, and Box Shape component - random_noise = hydra.Entity("random_noise") - random_noise.create_entity(position, ["Random Noise Gradient", "Gradient Transform Modifier", "Box Shape"]) - random_noise.set_test_parent_entity(vegetation) + # Set a Min Altitude of 38 and Max of 40 in Vegetation Altitude Filter + vegetation.get_set_test(3, "Configuration|Altitude Min", 38.0) + vegetation.get_set_test(3, "Configuration|Altitude Max", 40.0) - # Add a Vegetation Position Modifier to the vegetation area entity. - vegetation.add_component("Vegetation Position Modifier") + # Create a new entity as a child of the vegetation area entity with Random Noise Gradient Generator, Gradient + # Transform Modifier, and Box Shape component + random_noise = hydra.Entity("random_noise") + random_noise.create_entity(position, ["Random Noise Gradient", "Gradient Transform Modifier", "Box Shape"]) + random_noise.set_test_parent_entity(vegetation) - # Pin the Random Noise entity to the Gradient Entity Id field of the Position Modifier's Gradient X - vegetation.get_set_test(4, "Configuration|Position X|Gradient|Gradient Entity Id", random_noise.id) + # Add a Vegetation Position Modifier to the vegetation area entity. + vegetation.add_component("Vegetation Position Modifier") - # Toggle between PreProcess and PostProcess in Vegetation Altitude Filter - vegetation.get_set_test(3, "Configuration|Filter Stage", 1) - result = self.wait_for_condition(lambda: dynveg.validate_instance_count(position, 30.0, PREPROCESS_INSTANCE_COUNT), 2.0) - self.log(f"Vegetation instances count equal to expected value for PREPROCESS filter stage: {result}") - vegetation.get_set_test(3, "Configuration|Filter Stage", 2) - result = self.wait_for_condition(lambda: dynveg.validate_instance_count(position, 30.0, POSTPROCESS_INSTANCE_COUNT), 2.0) - self.log(f"Vegetation instances count equal to expected value for POSTPROCESS filter stage: {result}") + # Pin the Random Noise entity to the Gradient Entity Id field of the Position Modifier's Gradient X + vegetation.get_set_test(4, "Configuration|Position X|Gradient|Gradient Entity Id", random_noise.id) + + # Toggle between PreProcess and PostProcess in Vegetation Altitude Filter + vegetation.get_set_test(3, "Configuration|Filter Stage", 1) + result = helper.wait_for_condition(lambda: dynveg.validate_instance_count(position, 30.0, PREPROCESS_INSTANCE_COUNT), 2.0) + Report.result(Tests.preprocess_instance_count, result) + vegetation.get_set_test(3, "Configuration|Filter Stage", 2) + result = helper.wait_for_condition(lambda: dynveg.validate_instance_count(position, 30.0, POSTPROCESS_INSTANCE_COUNT), 2.0) + Report.result(Tests.postprocess_instance_count, result) -test = TestAltitudeFilterFilterStageToggle() -test.run() +if __name__ == "__main__": + + from editor_python_test_tools.utils import Report + Report.start_test(AltitudeFilter_FilterStageToggle) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/AltitudeFilter_ShapeSample_InstancesPlantAtSpecifiedAltitude.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/AltitudeFilter_ShapeSample_InstancesPlantAtSpecifiedAltitude.py index 8139ec5cac..bcd42b7fbb 100755 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/AltitudeFilter_ShapeSample_InstancesPlantAtSpecifiedAltitude.py +++ b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/AltitudeFilter_ShapeSample_InstancesPlantAtSpecifiedAltitude.py @@ -5,104 +5,105 @@ For complete copyright and license terms please see the LICENSE at the root of t SPDX-License-Identifier: Apache-2.0 OR MIT """ -import os -import sys -sys.path.append(os.path.dirname(os.path.abspath(__file__))) -import azlmbr.editor as editor -import azlmbr.legacy.general as general -import azlmbr.bus as bus -import azlmbr.math as math -import azlmbr.paths - -sys.path.append(os.path.join(azlmbr.paths.devroot, 'AutomatedTesting', 'Gem', 'PythonTests')) -import editor_python_test_tools.hydra_editor_utils as hydra -from editor_python_test_tools.editor_test_helper import EditorTestHelper -from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg +class Tests: + prefilter_instance_count = ( + "Pre-filter instance counts are accurate", + "Unexpected number of pre-filter instances found" + ) + postfilter_instance_count = ( + "Post-filter instance counts are accurate", + "Unexpected number of post-filter instances found" + ) -class TestAltitudeFilterShapeSample(EditorTestHelper): - def __init__(self): - EditorTestHelper.__init__(self, log_prefix="AltitudeFilterShapeSample", args=["level"]) +def AltitudeFilter_ShapeSample_InstancesPlantAtSpecifiedAltitude(): + """ + Summary: + A new level is created. A spawner entity is added, along with a planting surface at 32 on Z, and another at 36 + on Z. An Altitude Filter is added to the spawner entity, and set to sample a shape entity. Instance counts are + validated. - def run_test(self): - """ - Summary: - A new level is created. A spawner entity is added, along with a planting surface at 32 on Z, and another at 36 - on Z. An Altitude Filter is added to the spawner entity, and set to sample a shape entity. Instance counts are - validated. + Expected Behavior: + Instances are only spawned within the altitude range specified by the sampled shape. - Expected Behavior: - Instances are only spawned within the altitude range specified by the sampled shape. + Test Steps: + 1) Open a simple level + 2) Create an instance spawner entity + 3) Create surfaces to plant on, one at 32 on Z and another at 36 on Z. + 4) Initial instance counts pre-filter are verified. + 5) A new entity with shape is created, an sampled on the Vegetation Altitude Filter. + 6) Instance counts post-filter are verified. - Test Steps: - 1) Create a new level - 2) Create an instance spawner entity - 3) Create surfaces to plant on, one at 32 on Z and another at 36 on Z. - 4) Initial instance counts pre-filter are verified. - 5) A new entity with shape is created, an sampled on the Vegetation Altitude Filter. - 6) Instance counts post-filter are verified. + Note: + - This test file must be called from the Open 3D Engine Editor command terminal + - Any passed and failed tests are written to the Editor.log file. + Parsing the file or running a log_monitor are required to observe the test results. - Note: - - This test file must be called from the Open 3D Engine Editor command terminal - - Any passed and failed tests are written to the Editor.log file. - Parsing the file or running a log_monitor are required to observe the test results. + :return: None + """ - :return: None - """ + import os - # 1) Create a new, temporary level - self.test_success = self.create_level( - self.args["level"], - heightmap_resolution=1024, - heightmap_meters_per_pixel=1, - terrain_texture_resolution=4096, - use_terrain=False, - ) + import azlmbr.editor as editor + import azlmbr.legacy.general as general + import azlmbr.bus as bus + import azlmbr.math as math - # Set view of planting area for visual debugging - general.set_current_view_position(512.0, 500.0, 38.0) - general.set_current_view_rotation(-20.0, 0.0, 0.0) + import editor_python_test_tools.hydra_editor_utils as hydra + from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg + from editor_python_test_tools.utils import Report + from editor_python_test_tools.utils import TestHelper as helper - # 2) Create a new entity with required vegetation area components - center_point = math.Vector3(512.0, 512.0, 32.0) - asset_path = os.path.join("Slices", "PinkFlower.dynamicslice") - spawner_entity = dynveg.create_vegetation_area("Instance Spawner", center_point, 16.0, 16.0, 16.0, asset_path) + # 1) Open an existing simple level + helper.init_idle() + helper.open_level("Physics", "Base") - # Add a Vegetation Altitude Filter - spawner_entity.add_component("Vegetation Altitude Filter") + # Set view of planting area for visual debugging + general.set_current_view_position(512.0, 500.0, 38.0) + general.set_current_view_rotation(-20.0, 0.0, 0.0) - # 3) Add surfaces to plant on - dynveg.create_surface_entity("Planting Surface", center_point, 32.0, 32.0, 1.0) - elevated_surface_center_point = math.Vector3(512.0, 512.0, 36.0) - dynveg.create_surface_entity("Planting Surface Elevated", elevated_surface_center_point, 32.0, 32.0, 1.0) + # 2) Create a new entity with required vegetation area components + center_point = math.Vector3(512.0, 512.0, 32.0) + asset_path = os.path.join("Slices", "PinkFlower.dynamicslice") + spawner_entity = dynveg.create_vegetation_area("Instance Spawner", center_point, 16.0, 16.0, 16.0, asset_path) - # Set instances to spawn on a center snap point to avoid unexpected instances around the edges of the box shape - veg_system_settings_component = hydra.add_level_component("Vegetation System Settings") - editor.EditorComponentAPIBus(bus.Broadcast, "SetComponentProperty", veg_system_settings_component, - 'Configuration|Area System Settings|Sector Point Snap Mode', 1) + # Add a Vegetation Altitude Filter + spawner_entity.add_component("Vegetation Altitude Filter") - # 4) Verify initial instance counts pre-filter - num_expected = (20 * 20) * 2 # 20 instances per 16m per side x 2 surfaces - spawner_success = self.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, num_expected), 5.0) - self.test_success = self.test_success and spawner_success + # 3) Add surfaces to plant on + dynveg.create_surface_entity("Planting Surface", center_point, 32.0, 32.0, 1.0) + elevated_surface_center_point = math.Vector3(512.0, 512.0, 36.0) + dynveg.create_surface_entity("Planting Surface Elevated", elevated_surface_center_point, 32.0, 32.0, 1.0) - # 5) Create a new entity with a shape at 36 on the Z-axis, and pin the entity to the Vegetation Altitude Filter - shape_sampler_center_point = math.Vector3(512.0, 512.0, 36.0) - shape_sampler = hydra.Entity("Shape Sampler") - shape_sampler.create_entity( - shape_sampler_center_point, - ["Box Shape"] - ) - if shape_sampler.id.IsValid(): - print(f"'{shape_sampler.name}' created") - spawner_entity.get_set_test(3, 'Configuration|Pin To Shape Entity Id', shape_sampler.id) + # Set instances to spawn on a center snap point to avoid unexpected instances around the edges of the box shape + veg_system_settings_component = hydra.add_level_component("Vegetation System Settings") + editor.EditorComponentAPIBus(bus.Broadcast, "SetComponentProperty", veg_system_settings_component, + 'Configuration|Area System Settings|Sector Point Snap Mode', 1) - # 6) Validate expected instance counts - num_expected = 20 * 20 # Instances should now only plant on the elevated surface - sampler_success = self.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, num_expected), 5.0) - self.test_success = self.test_success and sampler_success + # 4) Verify initial instance counts pre-filter + num_expected = (20 * 20) * 2 # 20 instances per 16m per side x 2 surfaces + spawner_success = helper.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, num_expected), 5.0) + Report.result(Tests.prefilter_instance_count, spawner_success) + + # 5) Create a new entity with a shape at 36 on the Z-axis, and pin the entity to the Vegetation Altitude Filter + shape_sampler_center_point = math.Vector3(512.0, 512.0, 36.0) + shape_sampler = hydra.Entity("Shape Sampler") + shape_sampler.create_entity( + shape_sampler_center_point, + ["Box Shape"] + ) + if shape_sampler.id.IsValid(): + print(f"'{shape_sampler.name}' created") + spawner_entity.get_set_test(3, 'Configuration|Pin To Shape Entity Id', shape_sampler.id) + + # 6) Validate expected instance counts + num_expected = 20 * 20 # Instances should now only plant on the elevated surface + sampler_success = helper.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, num_expected), 5.0) + Report.result(Tests.postfilter_instance_count, sampler_success) -test = TestAltitudeFilterShapeSample() -test.run() +if __name__ == "__main__": + + from editor_python_test_tools.utils import Report + Report.start_test(AltitudeFilter_ShapeSample_InstancesPlantAtSpecifiedAltitude) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/AreaComponentSlices_SliceCreationAndVisibilityToggle.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/AreaComponentSlices_SliceCreationAndVisibilityToggle.py deleted file mode 100755 index 095024cca1..0000000000 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/AreaComponentSlices_SliceCreationAndVisibilityToggle.py +++ /dev/null @@ -1,125 +0,0 @@ -""" -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 -""" - -import os -import sys - -import azlmbr.math as math -import azlmbr.legacy.general as general -import azlmbr.slice as slice -import azlmbr.bus as bus -import azlmbr.editor as editor -import azlmbr.asset as asset -import azlmbr.paths - -sys.path.append(os.path.join(azlmbr.paths.devroot, "AutomatedTesting", "Gem", "PythonTests")) -import editor_python_test_tools.hydra_editor_utils as hydra -from editor_python_test_tools.editor_test_helper import EditorTestHelper -from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg - - -class TestAreaComponentsSliceCreationAndVisibilityToggle(EditorTestHelper): - def __init__(self): - EditorTestHelper.__init__( - self, log_prefix="AreaComponentSlices_SliceCreationAndVisibilityToggle", args=["level"] - ) - - def run_test(self): - """ - Summary: - C2627900 Verifies if a slice containing the component can be created. - C2627905 A slice containing the Vegetation Layer Blender component can be created. - C2627904: Hiding a slice containing the component clears any visuals from the Viewport. - - Expected Result: - C2627900, C2627905: Slice is created, and is properly processed in the Asset Processor. - C2627904: Vegetation area visuals are hidden from the Viewport. - - :return: None - """ - - def path_is_valid_asset(asset_path): - asset_id = asset.AssetCatalogRequestBus(bus.Broadcast, "GetAssetIdByPath", asset_path, math.Uuid(), False) - return asset_id.invoke("IsValid") - - # 1) Create empty level - self.test_success = self.create_level( - self.args["level"], - heightmap_resolution=1024, - heightmap_meters_per_pixel=1, - terrain_texture_resolution=4096, - use_terrain=False, - ) - - general.set_current_view_position(512.0, 480.0, 38.0) - - # 2) C2627900 Verifies if a slice containing the Vegetation Layer Spawner component can be created. - # 2.1) Create basic vegetation entity - position = math.Vector3(512.0, 512.0, 32.0) - asset_path = os.path.join("Slices", "PinkFlower.dynamicslice") - veg_1 = dynveg.create_vegetation_area("vegetation_1", position, 16.0, 16.0, 16.0, asset_path) - - # 2.2) Create slice from the entity - slice_path = os.path.join("slices", "TestSlice_1.slice") - slice.SliceRequestBus(bus.Broadcast, "CreateNewSlice", veg_1.id, slice_path) - - # 2.3) Verify if the slice has been created successfully - self.wait_for_condition(lambda: path_is_valid_asset(slice_path), 5.0) - self.log( - f"Slice has been created successfully (entity with spawner component): {path_is_valid_asset(slice_path)}" - ) - - # 3) C2627904: Hiding a slice containing the component clears any visuals from the Viewport - # 3.1) Create Surface for instances to plant on - dynveg.create_surface_entity("Surface_Entity", position, 16.0, 16.0, 1.0) - - # 3.2) Initially verify instance count before hiding slice - self.wait_for_condition(lambda: dynveg.validate_instance_count(position, 16.0, 400)) - self.log( - f"Vegetation plants initially when slice is shown: {dynveg.validate_instance_count(position, 16.0, 400)}" - ) - - # 3.3) Hide the slice and verify instance count - editor.EditorEntityAPIBus(bus.Event, "SetVisibilityState", veg_1.id, False) - self.wait_for_condition(lambda: dynveg.validate_instance_count(position, 16.0, 0)) - self.log(f"Vegetation is cleared when slice is hidden: {dynveg.validate_instance_count(position, 16.0, 0)}") - - # 3.4) Unhide the slice - editor.EditorEntityAPIBus(bus.Event, "SetVisibilityState", veg_1.id, True) - - # 4) C2627905 A slice containing the Vegetation Layer Blender component can be created. - # 4.1) Create another vegetation entity to add to blender component - veg_2 = dynveg.create_vegetation_area("vegetation_2", position, 1.0, 1.0, 1.0, "") - - # 4.2) Create entity with Vegetation Layer Blender - components_to_add = ["Box Shape", "Vegetation Layer Blender"] - blender_entity = hydra.Entity("blender_entity") - blender_entity.create_entity(position, components_to_add) - - # 4.3) Pin both the vegetation areas to the blender entity - pte = hydra.get_property_tree(blender_entity.components[1]) - path = "Configuration|Vegetation Areas" - pte.update_container_item(path, 0, veg_1.id) - pte.add_container_item(path, 1, veg_2.id) - - # 4.4) Drag the simple vegetation areas under the Vegetation Layer Blender entity to create an entity hierarchy. - veg_1.set_test_parent_entity(blender_entity) - veg_2.set_test_parent_entity(blender_entity) - - # 4.5) Create slice from blender entity - slice_path = os.path.join("slices", "TestSlice_2.slice") - slice.SliceRequestBus(bus.Broadcast, "CreateNewSlice", blender_entity.id, slice_path) - - # 4.6) Verify if the slice has been created successfully - self.wait_for_condition(lambda: path_is_valid_asset(slice_path), 5.0) - self.log( - f"Slice has been created successfully (entity with blender component): {path_is_valid_asset(slice_path)}" - ) - - -test = TestAreaComponentsSliceCreationAndVisibilityToggle() -test.run() diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/AssetListCombiner_CombinedDescriptorsExpressInConfiguredArea.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/AssetListCombiner_CombinedDescriptorsExpressInConfiguredArea.py index 5242160b35..7f6ce87110 100755 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/AssetListCombiner_CombinedDescriptorsExpressInConfiguredArea.py +++ b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/AssetListCombiner_CombinedDescriptorsExpressInConfiguredArea.py @@ -5,162 +5,167 @@ For complete copyright and license terms please see the LICENSE at the root of t SPDX-License-Identifier: Apache-2.0 OR MIT """ -import os -import sys -sys.path.append(os.path.dirname(os.path.abspath(__file__))) -import azlmbr.bus as bus -import azlmbr.editor as editor -import azlmbr.legacy.general as general -import azlmbr.math as math -import azlmbr.paths -import azlmbr.vegetation as vegetation - -sys.path.append(os.path.join(azlmbr.paths.devroot, 'AutomatedTesting', 'Gem', 'PythonTests')) -import editor_python_test_tools.hydra_editor_utils as hydra -from editor_python_test_tools.editor_test_helper import EditorTestHelper -from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg +class Tests: + combined_instance_count_validation = ( + "Combined instance counts are as expected", + "Found an unexpected number of instances" + ) + replaced_asset_list_combined_instance_count_validation = ( + "Combined instance counts are as expected after replacing an Asset List reference", + "Found an unexpected number of instances after replacing an Asset List reference" + ) + removed_asset_lists_combined_instance_count_validation = ( + "Instance counts are as expected after removing the referenced Asset Lists", + "Found an unexpected number of instances after removing the referenced Asset Lists" + ) -class TestAssetListCombiner(EditorTestHelper): - def __init__(self): - EditorTestHelper.__init__(self, log_prefix="AssetListCombiner_CombinedDescriptors", args=["level"]) +def AssetListCombiner_CombinedDescriptorsExpressInConfiguredArea(): + """ + Summary: + Combined descriptors appear as expected in a vegetation area. Also verifies remove/replace of assigned Asset + Lists. - def run_test(self): - """ - Summary: - Combined descriptors appear as expected in a vegetation area. Also verifies remove/replace of assigned Asset - Lists. + Expected Behavior: + Vegetation fills in the area using the assets assigned to both Vegetation Asset Lists. - Expected Behavior: - Vegetation fills in the area using the assets assigned to both Vegetation Asset Lists. + Test Steps: + 1) Open a simple level + 2) Create 3 entities with Vegetation Asset List components set to spawn different descriptors + 3) Create a planting surface and add a Vegetation System Settings level component with instances set to spawn + on center instead of corner + 4) Create a spawner using a Vegetation Asset List Combiner component and a Weight Selector, and disallow + spawning empty assets + 5) Add 2 of the Asset List entities to the Vegetation Asset List Combiner component (PinkFlower and Empty) + 6) Create a Constant Gradient entity as a child of the spawner entity, and a Dither Gradient Modifier entity + as a child of the Constant Gradient entity, and configure for a checkerboard pattern + 7) Pin the Dither Gradient Entity to the Asset Weight Selector of the spawner entity + 8) Validate instance count with configured Asset List Combiner + 9) Replace the reference to the 2nd asset list on the Vegetation Asset List Combiner component and validate + instance count + 10) Remove the referenced Asset Lists on the Asset List Combiner, Disable/Re-enable the Asset List + Combiner component to force a refresh, and validate instance count - Test Steps: - 1) Create a new, temporary level - 2) Create 3 entities with Vegetation Asset List components set to spawn different descriptors - 3) Create a planting surface and add a Vegetation System Settings level component with instances set to spawn - on center instead of corner - 4) Create a spawner using a Vegetation Asset List Combiner component and a Weight Selector, and disallow - spawning empty assets - 5) Add 2 of the Asset List entities to the Vegetation Asset List Combiner component (PinkFlower and Empty) - 6) Create a Constant Gradient entity as a child of the spawner entity, and a Dither Gradient Modifier entity - as a child of the Constant Gradient entity, and configure for a checkerboard pattern - 7) Pin the Dither Gradient Entity to the Asset Weight Selector of the spawner entity - 8) Validate instance count with configured Asset List Combiner - 9) Replace the reference to the 2nd asset list on the Vegetation Asset List Combiner component and validate - instance count - 10) Remove the referenced Asset Lists on the Asset List Combiner, Disable/Re-enable the Asset List - Combiner component to force a refresh, and validate instance count + Note: + - This test file must be called from the Open 3D Engine Editor command terminal + - Any passed and failed tests are written to the Editor.log file. + Parsing the file or running a log_monitor are required to observe the test results. - Note: - - This test file must be called from the Open 3D Engine Editor command terminal - - Any passed and failed tests are written to the Editor.log file. - Parsing the file or running a log_monitor are required to observe the test results. + :return: None + """ - :return: None - """ + import os - def create_asset_list_entity(name, center, dynamic_slice_asset_path): - asset_list_entity = hydra.Entity(name) - asset_list_entity.create_entity( - center, - ["Vegetation Asset List"] - ) - if asset_list_entity.id.IsValid(): - print(f"'{asset_list_entity.name}' created") + import azlmbr.bus as bus + import azlmbr.editor as editor + import azlmbr.legacy.general as general + import azlmbr.math as math + import azlmbr.vegetation as vegetation - # Set the Asset List to a Dynamic Slice spawner with a specific slice asset selected - dynamic_slice_spawner = vegetation.DynamicSliceInstanceSpawner() - dynamic_slice_spawner.SetSliceAssetPath(dynamic_slice_asset_path) - descriptor = hydra.get_component_property_value(asset_list_entity.components[0], - "Configuration|Embedded Assets|[0]") - descriptor.spawner = dynamic_slice_spawner - asset_list_entity.get_set_test(0, "Configuration|Embedded Assets|[0]", descriptor) - return asset_list_entity + import editor_python_test_tools.hydra_editor_utils as hydra + from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg + from editor_python_test_tools.utils import Report + from editor_python_test_tools.utils import TestHelper as helper - # 1) Create a new, temporary level - self.test_success = self.create_level( - self.args["level"], - heightmap_resolution=1024, - heightmap_meters_per_pixel=1, - terrain_texture_resolution=4096, - use_terrain=False, + def create_asset_list_entity(name, center, dynamic_slice_asset_path): + asset_list_entity = hydra.Entity(name) + asset_list_entity.create_entity( + center, + ["Vegetation Asset List"] ) + if asset_list_entity.id.IsValid(): + print(f"'{asset_list_entity.name}' created") - # Set view of planting area for visual debugging - general.set_current_view_position(512.0, 500.0, 38.0) - general.set_current_view_rotation(-20.0, 0.0, 0.0) + # Set the Asset List to a Dynamic Slice spawner with a specific slice asset selected + dynamic_slice_spawner = vegetation.DynamicSliceInstanceSpawner() + dynamic_slice_spawner.SetSliceAssetPath(dynamic_slice_asset_path) + descriptor = hydra.get_component_property_value(asset_list_entity.components[0], + "Configuration|Embedded Assets|[0]") + descriptor.spawner = dynamic_slice_spawner + asset_list_entity.get_set_test(0, "Configuration|Embedded Assets|[0]", descriptor) + return asset_list_entity - # 2) Create 3 entities with Vegetation Asset List components set to spawn different descriptors - center_point = math.Vector3(512.0, 512.0, 32.0) - asset_path = os.path.join("Slices", "PinkFlower.dynamicslice") - asset_path2 = os.path.join("Slices", "PurpleFlower.dynamicslice") - asset_list_entity = create_asset_list_entity("Asset List 1", center_point, asset_path) - asset_list_entity2 = create_asset_list_entity("Asset List 2", center_point, None) - asset_list_entity3 = create_asset_list_entity("Asset List 3", center_point, asset_path2) + # 1) Open an existing simple level + helper.init_idle() + helper.open_level("Physics", "Base") - # 3) Create a planting surface and add a Vegetation System Settings level component with instances set to spawn - # on center instead of corner - dynveg.create_surface_entity("Surface Entity", center_point, 32.0, 32.0, 1.0) - veg_system_settings_component = hydra.add_level_component("Vegetation System Settings") - editor.EditorComponentAPIBus(bus.Broadcast, "SetComponentProperty", veg_system_settings_component, - 'Configuration|Area System Settings|Sector Point Snap Mode', 1) + # Set view of planting area for visual debugging + general.set_current_view_position(512.0, 500.0, 38.0) + general.set_current_view_rotation(-20.0, 0.0, 0.0) - # 4) Create a spawner using a Vegetation Asset List Combiner component and a Weight Selector, and disallow - # spawning empty assets - spawner_entity = dynveg.create_vegetation_area("Spawner Entity", center_point, 16.0, 16.0, 16.0, None) - spawner_entity.remove_component("Vegetation Asset List") - spawner_entity.add_component("Vegetation Asset List Combiner") - spawner_entity.add_component("Vegetation Asset Weight Selector") - spawner_entity.get_set_test(0, "Configuration|Allow Empty Assets", False) + # 2) Create 3 entities with Vegetation Asset List components set to spawn different descriptors + center_point = math.Vector3(512.0, 512.0, 32.0) + asset_path = os.path.join("Slices", "PinkFlower.dynamicslice") + asset_path2 = os.path.join("Slices", "PurpleFlower.dynamicslice") + asset_list_entity = create_asset_list_entity("Asset List 1", center_point, asset_path) + asset_list_entity2 = create_asset_list_entity("Asset List 2", center_point, None) + asset_list_entity3 = create_asset_list_entity("Asset List 3", center_point, asset_path2) - # 5) Add the Asset List entities to the Vegetation Asset List Combiner component - asset_list_entities = [asset_list_entity.id, asset_list_entity2.id] - spawner_entity.get_set_test(2, "Configuration|Descriptor Providers", asset_list_entities) + # 3) Create a planting surface and add a Vegetation System Settings level component with instances set to spawn + # on center instead of corner + dynveg.create_surface_entity("Surface Entity", center_point, 32.0, 32.0, 1.0) + veg_system_settings_component = hydra.add_level_component("Vegetation System Settings") + editor.EditorComponentAPIBus(bus.Broadcast, "SetComponentProperty", veg_system_settings_component, + 'Configuration|Area System Settings|Sector Point Snap Mode', 1) - # 6) Create a Constant Gradient entity as a child of the spawner entity, and a Dither Gradient Modifier entity - # as a child of the Constant Gradient entity, and configure for a checkerboard pattern - components_to_add = ["Constant Gradient"] - constant_gradient_entity = hydra.Entity("Constant Gradient Entity") - constant_gradient_entity.create_entity(center_point, components_to_add, parent_id=spawner_entity.id) - constant_gradient_entity.get_set_test(0, "Configuration|Value", 0.5) + # 4) Create a spawner using a Vegetation Asset List Combiner component and a Weight Selector, and disallow + # spawning empty assets + spawner_entity = dynveg.create_vegetation_area("Spawner Entity", center_point, 16.0, 16.0, 16.0, None) + spawner_entity.remove_component("Vegetation Asset List") + spawner_entity.add_component("Vegetation Asset List Combiner") + spawner_entity.add_component("Vegetation Asset Weight Selector") + spawner_entity.get_set_test(0, "Configuration|Allow Empty Assets", False) - components_to_add = ["Dither Gradient Modifier"] - dither_gradient_entity = hydra.Entity("Dither Gradient Entity") - dither_gradient_entity.create_entity(center_point, components_to_add, parent_id=constant_gradient_entity.id) - dither_gradient_entity.get_set_test(0, "Configuration|Gradient|Gradient Entity Id", constant_gradient_entity.id) + # 5) Add the Asset List entities to the Vegetation Asset List Combiner component + asset_list_entities = [asset_list_entity.id, asset_list_entity2.id] + spawner_entity.get_set_test(2, "Configuration|Descriptor Providers", asset_list_entities) - # 7) Pin the Dither Gradient Entity to the Asset Weight Selector of the spawner entity - spawner_entity.get_set_test(3, "Configuration|Gradient|Gradient Entity Id", dither_gradient_entity.id) + # 6) Create a Constant Gradient entity as a child of the spawner entity, and a Dither Gradient Modifier entity + # as a child of the Constant Gradient entity, and configure for a checkerboard pattern + components_to_add = ["Constant Gradient"] + constant_gradient_entity = hydra.Entity("Constant Gradient Entity") + constant_gradient_entity.create_entity(center_point, components_to_add, parent_id=spawner_entity.id) + constant_gradient_entity.get_set_test(0, "Configuration|Value", 0.5) - # 8) Validate instance count. We should now have 200 instances in the spawner area as every other instance - # should be an empty asset which the spawner is set to disallow - num_expected = 20 * 20 / 2 - success = self.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, - num_expected), 5.0) - self.test_success = success and self.test_success + components_to_add = ["Dither Gradient Modifier"] + dither_gradient_entity = hydra.Entity("Dither Gradient Entity") + dither_gradient_entity.create_entity(center_point, components_to_add, parent_id=constant_gradient_entity.id) + dither_gradient_entity.get_set_test(0, "Configuration|Gradient|Gradient Entity Id", constant_gradient_entity.id) - # 9) Replace the reference to the 2nd asset list on the Vegetation Asset List Combiner component and validate - # instance count. Should now be 400 instances as the empty spaces can now be claimed by the new descriptor - spawner_entity.get_set_test(2, "Configuration|Descriptor Providers|[1]", asset_list_entity3.id) - num_expected = 20 * 20 - success = self.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, - num_expected), 5.0) - self.test_success = success and self.test_success + # 7) Pin the Dither Gradient Entity to the Asset Weight Selector of the spawner entity + spawner_entity.get_set_test(3, "Configuration|Gradient|Gradient Entity Id", dither_gradient_entity.id) - # 10) Remove the referenced Asset Lists on the Asset List Combiner, Disable/Re-enable the Asset List - # Combiner component to force a refresh, and validate instance count. We should now have 0 instances. - pte = hydra.get_property_tree(spawner_entity.components[2]) - path = "Configuration|Descriptor Providers" - pte.reset_container(path) - # Component refresh is currently necessary due to container operations not causing a refresh (LY-120947) - editor.EditorComponentAPIBus(bus.Broadcast, "DisableComponents", [spawner_entity.components[2]]) - editor.EditorComponentAPIBus(bus.Broadcast, "EnableComponents", [spawner_entity.components[2]]) - num_expected = 0 - success = self.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, - num_expected), 5.0) - self.test_success = success and self.test_success + # 8) Validate instance count. We should now have 200 instances in the spawner area as every other instance + # should be an empty asset which the spawner is set to disallow + num_expected = 20 * 20 / 2 + success = helper.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, + num_expected), 5.0) + Report.result(Tests.combined_instance_count_validation, success) + + # 9) Replace the reference to the 2nd asset list on the Vegetation Asset List Combiner component and validate + # instance count. Should now be 400 instances as the empty spaces can now be claimed by the new descriptor + spawner_entity.get_set_test(2, "Configuration|Descriptor Providers|[1]", asset_list_entity3.id) + num_expected = 20 * 20 + success = helper.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, + num_expected), 5.0) + Report.result(Tests.replaced_asset_list_combined_instance_count_validation, success) + + # 10) Remove the referenced Asset Lists on the Asset List Combiner, Disable/Re-enable the Asset List + # Combiner component to force a refresh, and validate instance count. We should now have 0 instances. + pte = hydra.get_property_tree(spawner_entity.components[2]) + path = "Configuration|Descriptor Providers" + pte.reset_container(path) + # Component refresh is currently necessary due to container operations not causing a refresh (LY-120947) + editor.EditorComponentAPIBus(bus.Broadcast, "DisableComponents", [spawner_entity.components[2]]) + editor.EditorComponentAPIBus(bus.Broadcast, "EnableComponents", [spawner_entity.components[2]]) + num_expected = 0 + success = helper.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, + num_expected), 5.0) + Report.result(Tests.removed_asset_lists_combined_instance_count_validation, success) -test = TestAssetListCombiner() -test.run() +if __name__ == "__main__": + + from editor_python_test_tools.utils import Report + Report.start_test(AssetListCombiner_CombinedDescriptorsExpressInConfiguredArea) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/AssetWeightSelector_InstancesExpressBasedOnWeight.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/AssetWeightSelector_InstancesExpressBasedOnWeight.py index 113bbc8ea8..53b45e8470 100755 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/AssetWeightSelector_InstancesExpressBasedOnWeight.py +++ b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/AssetWeightSelector_InstancesExpressBasedOnWeight.py @@ -5,112 +5,110 @@ For complete copyright and license terms please see the LICENSE at the root of t SPDX-License-Identifier: Apache-2.0 OR MIT """ -""" -C6269654: Vegetation areas using weight selectors properly distribute instances according to Sort By Weight setting -""" -import os -import sys - -sys.path.append(os.path.dirname(os.path.abspath(__file__))) -import azlmbr.legacy.general as general -import azlmbr.math as math -import azlmbr.paths - -sys.path.append(os.path.join(azlmbr.paths.devroot, 'AutomatedTesting', 'Gem', 'PythonTests')) -import editor_python_test_tools.hydra_editor_utils as hydra -from editor_python_test_tools.editor_test_helper import EditorTestHelper -from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg +class Tests: + highest_weight_instance_count = ( + "Found the expected number of instances when sorting by highest weight", + "Found an unexpected number of instances when sorting by highest weight" + ) + lowest_weight_instance_count = ( + "Found the expected number of instances when sorting by lowest weight", + "Found an unexpected number of instances when sorting by lowest weight" + ) -class TestAssetWeightSelectorSortByWeight(EditorTestHelper): - def __init__(self): - EditorTestHelper.__init__(self, log_prefix="AssetWeightSelector_SortByWeight", args=["level"]) +def AssetWeightSelector_InstancesExpressBasedOnWeight(): + """ + Summary: + Vegetation areas using weight selectors properly distribute instances according to Sort By Weight setting - def run_test(self): - """ - Summary: - Vegetation areas using weight selectors properly distribute instances according to Sort By Weight setting + Expected Behavior: + Vegetation is planted in the area according to the generated gradient pattern. + Higher weight assets are more likely to express when "Descending (highest first)" is selected. + Lower weight assets are more likely to express when "Ascending (lowest first)" is selected. - Expected Behavior: - Vegetation is planted in the area according to the generated gradient pattern. - Higher weight assets are more likely to express when "Descending (highest first)" is selected. - Lower weight assets are more likely to express when "Ascending (lowest first)" is selected. + Test Steps: + 1) Open a simple level + 2) Create instance spawner with 2 descriptors, one with an Empty Asset + 3) Create a planting surface + 4) Create a child entity of the instance spawner with a Constant Gradient component with default values (1.0) + 5) Pin the child entity to Vegetation Asset Weight Selector of the instance spawner entity + 6) Set first descriptor to a higher weight, and toggle off Allow Empty Assets + 7) Validate instance count with initial setup/sort values + 8) Change sort values and validate instance count - Test Steps: - 1) Create new level - 2) Create instance spawner with 2 descriptors, one with an Empty Asset - 3) Create a planting surface - 4) Create a child entity of the instance spawner with a Constant Gradient component with default values (1.0) - 5) Pin the child entity to Vegetation Asset Weight Selector of the instance spawner entity - 6) Set first descriptor to a higher weight, and toggle off Allow Empty Assets - 7) Validate instance count with initial setup/sort values - 8) Change sort values and validate instance count + Note: + - This test file must be called from the Open 3D Engine Editor command terminal + - Any passed and failed tests are written to the Editor.log file. + Parsing the file or running a log_monitor are required to observe the test results. - Note: - - This test file must be called from the Open 3D Engine Editor command terminal - - Any passed and failed tests are written to the Editor.log file. - Parsing the file or running a log_monitor are required to observe the test results. + :return: None + """ - :return: None - """ - # 1) Create a new, temporary level - self.test_success = self.create_level( - self.args["level"], - heightmap_resolution=1024, - heightmap_meters_per_pixel=1, - terrain_texture_resolution=4096, - use_terrain=False, - ) + import os - # Set view of planting area for visual debugging - general.set_current_view_position(512.0, 500.0, 38.0) - general.set_current_view_rotation(-20.0, 0.0, 0.0) + import azlmbr.legacy.general as general + import azlmbr.math as math - # 2) Create a new instance spawner entity with multiple Dynamic Slice Instance Spawner descriptors, one set to a - # valid slice entity, and one set to None - spawner_center_point = math.Vector3(512.0, 512.0, 32.0) - asset_path = os.path.join("Slices", "PinkFlower.dynamicslice") - spawner_entity = dynveg.create_vegetation_area("Instance Spawner", spawner_center_point, 16.0, 16.0, 16.0, - asset_path) - desc_asset = hydra.get_component_property_value(spawner_entity.components[2], - "Configuration|Embedded Assets")[0] - desc_list = [desc_asset, desc_asset] - spawner_entity.get_set_test(2, "Configuration|Embedded Assets", desc_list) - spawner_entity.get_set_test(2, "Configuration|Embedded Assets|[1]|Instance|Slice Asset", None) + import editor_python_test_tools.hydra_editor_utils as hydra + from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg + from editor_python_test_tools.utils import Report + from editor_python_test_tools.utils import TestHelper as helper - # Add an Asset Weight Selector component to the spawner entity - spawner_entity.add_component("Vegetation Asset Weight Selector") + # 1) Open an existing simple level + helper.init_idle() + helper.open_level("Physics", "Base") - # 3) Create a planting surface - dynveg.create_surface_entity("Planting Surface", spawner_center_point, 32.0, 32.0, 1.0) + # Set view of planting area for visual debugging + general.set_current_view_position(512.0, 500.0, 38.0) + general.set_current_view_rotation(-20.0, 0.0, 0.0) - # 4) Create a child entity of the spawner entity with a Constant Gradient component - components_to_add = ["Constant Gradient"] - gradient_entity = hydra.Entity("Gradient Entity") - gradient_entity.create_entity(spawner_center_point, components_to_add, parent_id=spawner_entity.id) + # 2) Create a new instance spawner entity with multiple Dynamic Slice Instance Spawner descriptors, one set to a + # valid slice entity, and one set to None + spawner_center_point = math.Vector3(512.0, 512.0, 32.0) + asset_path = os.path.join("Slices", "PinkFlower.dynamicslice") + spawner_entity = dynveg.create_vegetation_area("Instance Spawner", spawner_center_point, 16.0, 16.0, 16.0, + asset_path) + desc_asset = hydra.get_component_property_value(spawner_entity.components[2], + "Configuration|Embedded Assets")[0] + desc_list = [desc_asset, desc_asset] + spawner_entity.get_set_test(2, "Configuration|Embedded Assets", desc_list) + spawner_entity.get_set_test(2, "Configuration|Embedded Assets|[1]|Instance|Slice Asset", None) - # 5) Pin the Constant Gradient to the Vegetation Asset Weight Selector - spawner_entity.get_set_test(3, 'Configuration|Gradient|Gradient Entity Id', gradient_entity.id) + # Add an Asset Weight Selector component to the spawner entity + spawner_entity.add_component("Vegetation Asset Weight Selector") - # 6) Set the first descriptor weight to a higher value and toggle off Allow Empty Assets on the Layer Spawner - # component - spawner_entity.get_set_test(2, 'Configuration|Embedded Assets|[0]|Weight', 50) - spawner_entity.get_set_test(0, 'Configuration|Allow Empty Assets', False) + # 3) Create a planting surface + dynveg.create_surface_entity("Planting Surface", spawner_center_point, 32.0, 32.0, 1.0) - # 7) Query for expected instances with default settings. We should have 0 instances with default Constant - # Gradient setup sorting by higher weight first - num_expected = 0 - initial_success = self.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, num_expected), 5.0) - self.test_success = initial_success and self.test_success + # 4) Create a child entity of the spawner entity with a Constant Gradient component + components_to_add = ["Constant Gradient"] + gradient_entity = hydra.Entity("Gradient Entity") + gradient_entity.create_entity(spawner_center_point, components_to_add, parent_id=spawner_entity.id) - # 8) Sort by lowest weight first, and verify instance counts. We should now have 400 instances as the highest - # priority instance won't be allowed to claim space due to "Allow Empty Assets" being False - spawner_entity.get_set_test(3, 'Configuration|Sort By Weight', 1) - num_expected = 20 * 20 - final_success = self.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, num_expected), 5.0) - self.test_success = final_success and self.test_success + # 5) Pin the Constant Gradient to the Vegetation Asset Weight Selector + spawner_entity.get_set_test(3, 'Configuration|Gradient|Gradient Entity Id', gradient_entity.id) + + # 6) Set the first descriptor weight to a higher value and toggle off Allow Empty Assets on the Layer Spawner + # component + spawner_entity.get_set_test(2, 'Configuration|Embedded Assets|[0]|Weight', 50) + spawner_entity.get_set_test(0, 'Configuration|Allow Empty Assets', False) + + # 7) Query for expected instances with default settings. We should have 0 instances with default Constant + # Gradient setup sorting by higher weight first + num_expected = 0 + initial_success = helper.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, num_expected), 5.0) + Report.result(Tests.highest_weight_instance_count, initial_success) + + # 8) Sort by lowest weight first, and verify instance counts. We should now have 400 instances as the highest + # priority instance won't be allowed to claim space due to "Allow Empty Assets" being False + spawner_entity.get_set_test(3, 'Configuration|Sort By Weight', 1) + num_expected = 20 * 20 + final_success = helper.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, num_expected), 5.0) + Report.result(Tests.lowest_weight_instance_count, final_success) -test = TestAssetWeightSelectorSortByWeight() -test.run() +if __name__ == "__main__": + + from editor_python_test_tools.utils import Report + Report.start_test(AssetWeightSelector_InstancesExpressBasedOnWeight) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/DistanceBetweenFilterOverrides_InstancesPlantAtSpecifiedRadius.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/DistanceBetweenFilterOverrides_InstancesPlantAtSpecifiedRadius.py index 12fb6d8eff..c2adffc6ca 100755 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/DistanceBetweenFilterOverrides_InstancesPlantAtSpecifiedRadius.py +++ b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/DistanceBetweenFilterOverrides_InstancesPlantAtSpecifiedRadius.py @@ -5,108 +5,118 @@ For complete copyright and license terms please see the LICENSE at the root of t SPDX-License-Identifier: Apache-2.0 OR MIT """ -import os -import sys -sys.path.append(os.path.dirname(os.path.abspath(__file__))) -import azlmbr.editor as editor -import azlmbr.legacy.general as general -import azlmbr.bus as bus -import azlmbr.math as math -import azlmbr.paths - -sys.path.append(os.path.join(azlmbr.paths.devroot, 'AutomatedTesting', 'Gem', 'PythonTests')) -import editor_python_test_tools.hydra_editor_utils as hydra -from editor_python_test_tools.editor_test_helper import EditorTestHelper -from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg +class Tests: + initial_instance_counts = ( + "Initial instance counts are as expected", + "Unexpected number of initial instances found" + ) + instance_counts_1m = ( + "Instance counts with 1 meters between instances are as expected", + "Unexpected number of instances found with 1 meters between instances" + ) + instance_counts_2m = ( + "Instance counts with 2 meters between instances are as expected", + "Unexpected number of instances found with 2 meters between instances" + ) + instance_counts_16m = ( + "Instance counts with 16 meters between instances are as expected", + "Unexpected number of instances found with 16 meters between instances" + ) -class TestDistanceBetweenFilterComponentOverrides(EditorTestHelper): - def __init__(self): - EditorTestHelper.__init__(self, log_prefix="DistanceBetweenFilterComponentOverrides", args=["level"]) +def DistanceBetweenFilterOverrides_InstancesPlantAtSpecifiedRadius(): + """ + Summary: Creates a level with a simple vegetation area. A Vegetation Distance Between Filter is + added and the min radius is changed as an override on the descriptor. Instance counts at specific points are + validated. - def run_test(self): - """ - Summary: Creates a level with a simple vegetation area. A Vegetation Distance Between Filter is - added and the min radius is changed as an override on the descriptor. Instance counts at specific points are - validated. + Test Steps: + 1) Open a simple level + 2) Create a vegetation area + 3) Create a surface for planting + 4) Add the Vegetation System Settings component and setup for the test + 5-8) Add the Distance Between Filter, setup overrides on both the component and descriptor, and validate + expected instance counts with a few different Radius values - Test Steps: - 1) Create a new level - 2) Create a vegetation area - 3) Create a surface for planting - 4) Add the Vegetation System Settings component and setup for the test - 5-8) Add the Distance Between Filter, setup overrides on both the component and descriptor, and validate - expected instance counts with a few different Radius values + Note: + - This test file must be called from the Open 3D Engine Editor command terminal + - Any passed and failed tests are written to the Editor.log file. + Parsing the file or running a log_monitor are required to observe the test results. - Note: - - This test file must be called from the Open 3D Engine Editor command terminal - - Any passed and failed tests are written to the Editor.log file. - Parsing the file or running a log_monitor are required to observe the test results. + :return: None + """ - :return: None - """ - instance_query_point_a = math.Vector3(512.5, 512.5, 32.0) - instance_query_point_b = math.Vector3(514.0, 512.5, 32.0) - instance_query_point_c = math.Vector3(515.0, 512.5, 32.0) + import os - # 1) Create a new, temporary level - self.test_success = self.create_level( - self.args["level"], - heightmap_resolution=1024, - heightmap_meters_per_pixel=1, - terrain_texture_resolution=4096, - use_terrain=False, - ) + import azlmbr.editor as editor + import azlmbr.legacy.general as general + import azlmbr.bus as bus + import azlmbr.math as math - general.set_current_view_position(512.0, 480.0, 38.0) + import editor_python_test_tools.hydra_editor_utils as hydra + from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg + from editor_python_test_tools.utils import Report + from editor_python_test_tools.utils import TestHelper as helper - # 2) Create a new entity with required vegetation area components - spawner_center_point = math.Vector3(520.0, 520.0, 32.0) - asset_path = os.path.join("Slices", "1m_cube.dynamicslice") - spawner_entity = dynveg.create_vegetation_area("Instance Spawner", spawner_center_point, 16.0, 16.0, 16.0, - asset_path) + instance_query_point_a = math.Vector3(512.5, 512.5, 32.0) + instance_query_point_b = math.Vector3(514.0, 512.5, 32.0) + instance_query_point_c = math.Vector3(515.0, 512.5, 32.0) - # 3) Create a surface to plant on - surface_center_point = math.Vector3(512.0, 512.0, 32.0) - dynveg.create_surface_entity("Planting Surface", surface_center_point, 128.0, 128.0, 1.0) + # 1) Open an existing simple level + helper.init_idle() + helper.open_level("Physics", "Base") - # 4) Add a Vegetation System Settings Level component and set Sector Point Snap Mode to Center - veg_system_settings_component = hydra.add_level_component("Vegetation System Settings") - editor.EditorComponentAPIBus(bus.Broadcast, "SetComponentProperty", veg_system_settings_component, - 'Configuration|Area System Settings|Sector Point Snap Mode', 1) - editor.EditorComponentAPIBus(bus.Broadcast, "SetComponentProperty", veg_system_settings_component, - 'Configuration|Area System Settings|Sector Point Density', 16) + general.set_current_view_position(512.0, 480.0, 38.0) - # 5) Add a Vegetation Distance Between Filter, toggle overrides on both the component and descriptor, - # and verify initial instance counts are accurate - spawner_entity.add_component("Vegetation Distance Between Filter") - spawner_entity.get_set_test(3, "Configuration|Allow Per-Item Overrides", True) - spawner_entity.get_set_test(2, "Configuration|Embedded Assets|[0]|Distance Between Filter (Radius)|Override Enabled", True) - num_expected = 16 * 16 - initial_success = self.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, num_expected), 5.0) - self.test_success = self.test_success and initial_success + # 2) Create a new entity with required vegetation area components + spawner_center_point = math.Vector3(520.0, 520.0, 32.0) + asset_path = os.path.join("Slices", "1m_cube.dynamicslice") + spawner_entity = dynveg.create_vegetation_area("Instance Spawner", spawner_center_point, 16.0, 16.0, 16.0, + asset_path) - # 6) Change Radius Min to 1.0, refresh, and verify instance counts are accurate - spawner_entity.get_set_test(2, "Configuration|Embedded Assets|[0]|Distance Between Filter (Radius)|Radius Min", 1.0) - point_a_success = self.wait_for_condition(lambda: dynveg.validate_instance_count(instance_query_point_a, 0.5, 1), 5.0) - point_b_success = self.wait_for_condition(lambda: dynveg.validate_instance_count(instance_query_point_b, 0.5, 0), 5.0) - point_c_success = self.wait_for_condition(lambda: dynveg.validate_instance_count(instance_query_point_c, 0.5, 1), 5.0) - self.test_success = self.test_success and point_a_success and point_b_success and point_c_success + # 3) Create a surface to plant on + surface_center_point = math.Vector3(512.0, 512.0, 32.0) + dynveg.create_surface_entity("Planting Surface", surface_center_point, 128.0, 128.0, 1.0) - # 7) Change Radius Min to 2.0, refresh, and verify instance counts are accurate - spawner_entity.get_set_test(2, "Configuration|Embedded Assets|[0]|Distance Between Filter (Radius)|Radius Min", 2.0) - point_a_success = self.wait_for_condition(lambda: dynveg.validate_instance_count(instance_query_point_a, 0.5, 1), 5.0) - point_b_success = self.wait_for_condition(lambda: dynveg.validate_instance_count(instance_query_point_b, 0.5, 0), 5.0) - point_c_success = self.wait_for_condition(lambda: dynveg.validate_instance_count(instance_query_point_c, 0.5, 0), 5.0) - self.test_success = self.test_success and point_a_success and point_b_success and point_c_success + # 4) Add a Vegetation System Settings Level component and set Sector Point Snap Mode to Center + veg_system_settings_component = hydra.add_level_component("Vegetation System Settings") + editor.EditorComponentAPIBus(bus.Broadcast, "SetComponentProperty", veg_system_settings_component, + 'Configuration|Area System Settings|Sector Point Snap Mode', 1) + editor.EditorComponentAPIBus(bus.Broadcast, "SetComponentProperty", veg_system_settings_component, + 'Configuration|Area System Settings|Sector Point Density', 16) - # 8) Change Radius Min to 16.0, refresh, and verify instance counts are accurate, only a single instance should plant - spawner_entity.get_set_test(2, "Configuration|Embedded Assets|[0]|Distance Between Filter (Radius)|Radius Min", 16.0) - num_expected_instances = 1 - final_check_success = self.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, num_expected_instances), 5.0) - self.test_success = self.test_success and final_check_success + # 5) Add a Vegetation Distance Between Filter, toggle overrides on both the component and descriptor, + # and verify initial instance counts are accurate + spawner_entity.add_component("Vegetation Distance Between Filter") + spawner_entity.get_set_test(3, "Configuration|Allow Per-Item Overrides", True) + spawner_entity.get_set_test(2, "Configuration|Embedded Assets|[0]|Distance Between Filter (Radius)|Override Enabled", True) + num_expected = 16 * 16 + initial_success = helper.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, num_expected), 5.0) + Report.result(Tests.initial_instance_counts, initial_success) + + # 6) Change Radius Min to 1.0, refresh, and verify instance counts are accurate + spawner_entity.get_set_test(2, "Configuration|Embedded Assets|[0]|Distance Between Filter (Radius)|Radius Min", 1.0) + point_a_success = helper.wait_for_condition(lambda: dynveg.validate_instance_count(instance_query_point_a, 0.5, 1), 5.0) + point_b_success = helper.wait_for_condition(lambda: dynveg.validate_instance_count(instance_query_point_b, 0.5, 0), 5.0) + point_c_success = helper.wait_for_condition(lambda: dynveg.validate_instance_count(instance_query_point_c, 0.5, 1), 5.0) + Report.result(Tests.instance_counts_1m, point_a_success and point_b_success and point_c_success) + + # 7) Change Radius Min to 2.0, refresh, and verify instance counts are accurate + spawner_entity.get_set_test(2, "Configuration|Embedded Assets|[0]|Distance Between Filter (Radius)|Radius Min", 2.0) + point_a_success = helper.wait_for_condition(lambda: dynveg.validate_instance_count(instance_query_point_a, 0.5, 1), 5.0) + point_b_success = helper.wait_for_condition(lambda: dynveg.validate_instance_count(instance_query_point_b, 0.5, 0), 5.0) + point_c_success = helper.wait_for_condition(lambda: dynveg.validate_instance_count(instance_query_point_c, 0.5, 0), 5.0) + Report.result(Tests.instance_counts_2m, point_a_success and point_b_success and point_c_success) + + # 8) Change Radius Min to 16.0, refresh, and verify instance counts are accurate, only a single instance should plant + spawner_entity.get_set_test(2, "Configuration|Embedded Assets|[0]|Distance Between Filter (Radius)|Radius Min", 16.0) + num_expected_instances = 1 + final_check_success = helper.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, num_expected_instances), 5.0) + Report.result(Tests.instance_counts_16m, final_check_success) -test = TestDistanceBetweenFilterComponentOverrides() -test.run() +if __name__ == "__main__": + + from editor_python_test_tools.utils import Report + Report.start_test(DistanceBetweenFilterOverrides_InstancesPlantAtSpecifiedRadius) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/DistanceBetweenFilter_InstancesPlantAtSpecifiedRadius.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/DistanceBetweenFilter_InstancesPlantAtSpecifiedRadius.py index 5b978c6212..de0d9a14fe 100755 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/DistanceBetweenFilter_InstancesPlantAtSpecifiedRadius.py +++ b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/DistanceBetweenFilter_InstancesPlantAtSpecifiedRadius.py @@ -5,104 +5,113 @@ For complete copyright and license terms please see the LICENSE at the root of t SPDX-License-Identifier: Apache-2.0 OR MIT """ -import os -import sys -sys.path.append(os.path.dirname(os.path.abspath(__file__))) -import azlmbr.editor as editor -import azlmbr.legacy.general as general -import azlmbr.bus as bus -import azlmbr.math as math -import azlmbr.paths - -sys.path.append(os.path.join(azlmbr.paths.devroot, 'AutomatedTesting', 'Gem', 'PythonTests')) -import editor_python_test_tools.hydra_editor_utils as hydra -from editor_python_test_tools.editor_test_helper import EditorTestHelper -from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg +class Tests: + initial_instance_counts = ( + "Initial instance counts are as expected", + "Unexpected number of initial instances found" + ) + instance_counts_1m = ( + "Instance counts with 1 meters between instances are as expected", + "Unexpected number of instances found with 1 meters between instances" + ) + instance_counts_2m = ( + "Instance counts with 2 meters between instances are as expected", + "Unexpected number of instances found with 2 meters between instances" + ) + instance_counts_16m = ( + "Instance counts with 16 meters between instances are as expected", + "Unexpected number of instances found with 16 meters between instances" + ) -class TestDistanceBetweenFilterComponent(EditorTestHelper): - def __init__(self): - EditorTestHelper.__init__(self, log_prefix="DistanceBetweenFilterComponent", args=["level"]) +def DistanceBetweenFilter_InstancesPlantAtSpecifiedRadius(): + """ + Summary: Creates a level with a simple vegetation area. A Vegetation Distance Between Filter is + added and the min radius is changed. Instance counts at specific points are validated. - def run_test(self): - """ - Summary: Creates a level with a simple vegetation area. A Vegetation Distance Between Filter is - added and the min radius is changed. Instance counts at specific points are validated. + Test Steps: + 1) Open a simple level + 2) Create a vegetation area + 3) Create a surface for planting + 4) Add the Vegetation System Settings component and setup for the test + 5-8) Add the Distance Between Filter, and validate expected instance counts with a few different Radius values - Test Steps: - 1) Create a new level - 2) Create a vegetation area - 3) Create a surface for planting - 4) Add the Vegetation System Settings component and setup for the test - 5-8) Add the Distance Between Filter, and validate expected instance counts with a few different Radius values + Note: + - This test file must be called from the Open 3D Engine Editor command terminal + - Any passed and failed tests are written to the Editor.log file. + Parsing the file or running a log_monitor are required to observe the test results. - Note: - - This test file must be called from the Open 3D Engine Editor command terminal - - Any passed and failed tests are written to the Editor.log file. - Parsing the file or running a log_monitor are required to observe the test results. + :return: None + """ - :return: None - """ - instance_query_point_a = math.Vector3(512.5, 512.5, 32.0) - instance_query_point_b = math.Vector3(514.0, 512.5, 32.0) - instance_query_point_c = math.Vector3(515.0, 512.5, 32.0) + import os - # 1) Create a new, temporary level - self.test_success = self.create_level( - self.args["level"], - heightmap_resolution=1024, - heightmap_meters_per_pixel=1, - terrain_texture_resolution=4096, - use_terrain=False, - ) + import azlmbr.editor as editor + import azlmbr.legacy.general as general + import azlmbr.bus as bus + import azlmbr.math as math - general.set_current_view_position(512.0, 480.0, 38.0) + import editor_python_test_tools.hydra_editor_utils as hydra + from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg + from editor_python_test_tools.utils import Report + from editor_python_test_tools.utils import TestHelper as helper - # 2) Create a new entity with required vegetation area components - spawner_center_point = math.Vector3(520.0, 520.0, 32.0) - asset_path = os.path.join("Slices", "1m_cube.dynamicslice") - spawner_entity = dynveg.create_vegetation_area("Instance Spawner", spawner_center_point, 16.0, 16.0, 16.0, - asset_path) + instance_query_point_a = math.Vector3(512.5, 512.5, 32.0) + instance_query_point_b = math.Vector3(514.0, 512.5, 32.0) + instance_query_point_c = math.Vector3(515.0, 512.5, 32.0) - # 3) Create a surface to plant on - surface_center_point = math.Vector3(512.0, 512.0, 32.0) - dynveg.create_surface_entity("Planting Surface", surface_center_point, 128.0, 128.0, 1.0) + # 1) Open an existing simple level + helper.init_idle() + helper.open_level("Physics", "Base") - # 4) Add a Vegetation System Settings Level component and set Sector Point Snap Mode to Center - veg_system_settings_component = hydra.add_level_component("Vegetation System Settings") - editor.EditorComponentAPIBus(bus.Broadcast, "SetComponentProperty", veg_system_settings_component, - 'Configuration|Area System Settings|Sector Point Snap Mode', 1) - editor.EditorComponentAPIBus(bus.Broadcast, "SetComponentProperty", veg_system_settings_component, - 'Configuration|Area System Settings|Sector Point Density', 16) + general.set_current_view_position(512.0, 480.0, 38.0) - # 5) Add a Vegetation Distance Between Filter and verify initial instance counts are accurate - spawner_entity.add_component("Vegetation Distance Between Filter") - num_expected = 16 * 16 - num_expected = 16 * 16 - initial_success = self.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, num_expected), 5.0) - self.test_success = self.test_success and initial_success + # 2) Create a new entity with required vegetation area components + spawner_center_point = math.Vector3(520.0, 520.0, 32.0) + asset_path = os.path.join("Slices", "1m_cube.dynamicslice") + spawner_entity = dynveg.create_vegetation_area("Instance Spawner", spawner_center_point, 16.0, 16.0, 16.0, + asset_path) - # 6) Change Radius Min to 1.0, refresh, and verify instance counts are accurate - spawner_entity.get_set_test(3, "Configuration|Radius Min", 1.0) - point_a_success = self.wait_for_condition(lambda: dynveg.validate_instance_count(instance_query_point_a, 0.5, 1), 5.0) - point_b_success = self.wait_for_condition(lambda: dynveg.validate_instance_count(instance_query_point_b, 0.5, 0), 5.0) - point_c_success = self.wait_for_condition(lambda: dynveg.validate_instance_count(instance_query_point_c, 0.5, 1), 5.0) - self.test_success = self.test_success and point_a_success and point_b_success and point_c_success + # 3) Create a surface to plant on + surface_center_point = math.Vector3(512.0, 512.0, 32.0) + dynveg.create_surface_entity("Planting Surface", surface_center_point, 128.0, 128.0, 1.0) - # 7) Change Radius Min to 2.0, refresh, and verify instance counts are accurate - spawner_entity.get_set_test(3, "Configuration|Radius Min", 2.0) - point_a_success = self.wait_for_condition(lambda: dynveg.validate_instance_count(instance_query_point_a, 0.5, 1), 5.0) - point_b_success = self.wait_for_condition(lambda: dynveg.validate_instance_count(instance_query_point_b, 0.5, 0), 5.0) - point_c_success = self.wait_for_condition(lambda: dynveg.validate_instance_count(instance_query_point_c, 0.5, 0), 5.0) - self.test_success = self.test_success and point_a_success and point_b_success and point_c_success + # 4) Add a Vegetation System Settings Level component and set Sector Point Snap Mode to Center + veg_system_settings_component = hydra.add_level_component("Vegetation System Settings") + editor.EditorComponentAPIBus(bus.Broadcast, "SetComponentProperty", veg_system_settings_component, + 'Configuration|Area System Settings|Sector Point Snap Mode', 1) + editor.EditorComponentAPIBus(bus.Broadcast, "SetComponentProperty", veg_system_settings_component, + 'Configuration|Area System Settings|Sector Point Density', 16) - # 8) Change Radius Min to 16.0, refresh, and verify instance counts are accurate - spawner_entity.get_set_test(3, "Configuration|Radius Min", 16.0) - num_expected_instances = 1 - final_check_success = self.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, num_expected_instances), 5.0) - self.test_success = final_check_success and self.test_success + # 5) Add a Vegetation Distance Between Filter and verify initial instance counts are accurate + spawner_entity.add_component("Vegetation Distance Between Filter") + num_expected = 16 * 16 + initial_success = helper.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, num_expected), 5.0) + Report.result(Tests.initial_instance_counts, initial_success) + + # 6) Change Radius Min to 1.0, refresh, and verify instance counts are accurate + spawner_entity.get_set_test(3, "Configuration|Radius Min", 1.0) + point_a_success = helper.wait_for_condition(lambda: dynveg.validate_instance_count(instance_query_point_a, 0.5, 1), 5.0) + point_b_success = helper.wait_for_condition(lambda: dynveg.validate_instance_count(instance_query_point_b, 0.5, 0), 5.0) + point_c_success = helper.wait_for_condition(lambda: dynveg.validate_instance_count(instance_query_point_c, 0.5, 1), 5.0) + Report.result(Tests.instance_counts_1m, point_a_success and point_b_success and point_c_success) + + # 7) Change Radius Min to 2.0, refresh, and verify instance counts are accurate + spawner_entity.get_set_test(3, "Configuration|Radius Min", 2.0) + point_a_success = helper.wait_for_condition(lambda: dynveg.validate_instance_count(instance_query_point_a, 0.5, 1), 5.0) + point_b_success = helper.wait_for_condition(lambda: dynveg.validate_instance_count(instance_query_point_b, 0.5, 0), 5.0) + point_c_success = helper.wait_for_condition(lambda: dynveg.validate_instance_count(instance_query_point_c, 0.5, 0), 5.0) + Report.result(Tests.instance_counts_2m, point_a_success and point_b_success and point_c_success) + + # 8) Change Radius Min to 16.0, refresh, and verify instance counts are accurate + spawner_entity.get_set_test(3, "Configuration|Radius Min", 16.0) + num_expected_instances = 1 + final_check_success = helper.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, num_expected_instances), 5.0) + Report.result(Tests.instance_counts_16m, final_check_success) -test = TestDistanceBetweenFilterComponent() -test.run() +if __name__ == "__main__": + + from editor_python_test_tools.utils import Report + Report.start_test(DistanceBetweenFilter_InstancesPlantAtSpecifiedRadius) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/DynamicSliceInstanceSpawner_DynamicSliceSpawnerWorks.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/DynamicSliceInstanceSpawner_DynamicSliceSpawnerWorks.py index 173be62829..fcbf85c59d 100755 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/DynamicSliceInstanceSpawner_DynamicSliceSpawnerWorks.py +++ b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/DynamicSliceInstanceSpawner_DynamicSliceSpawnerWorks.py @@ -5,140 +5,178 @@ For complete copyright and license terms please see the LICENSE at the root of t SPDX-License-Identifier: Apache-2.0 OR MIT """ -import os -import sys -import azlmbr.legacy.general as general -import azlmbr.bus as bus -import azlmbr.math as math -import azlmbr.paths - -sys.path.append(os.path.join(azlmbr.paths.devroot, 'AutomatedTesting', 'Gem', 'PythonTests')) -import editor_python_test_tools.hydra_editor_utils as hydra -from editor_python_test_tools.editor_test_helper import EditorTestHelper -from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg +class BehaviorContextTests: + spawner_initialized = ( + "Successfully initialized a Dynamic Slice Instance Spawner", + "Failed to initialize a Dynamic Slice Instance Spawner" + ) + spawner_slice_asset_path_set = ( + "Successfully set a Dynamic Slice asset path", + "Failed to set a Dynamic Slice asset path" + ) + spawner_empty_slice_asset_path_set = ( + "Successfully set an empty Dynamic Slice asset path", + "Failed to set an empty Dynamic Slice asset path" + ) + desc_spawnertype_sets_spawner = ( + "Setting spawnerType sets spawner too", + "Setting spawnerType failed to set spawner to expected value" + ) + desc_spawner_sets_spawnertype = ( + "Setting spawner sets spawnerType too", + "Setting spawner failed to set spawnerType to expected value" + ) -class TestDynamicSliceInstanceSpawner(EditorTestHelper): - def __init__(self): - EditorTestHelper.__init__(self, log_prefix="DynamicSliceInstanceSpawner", args=["level"]) +class PropertyTreeTests: + entity_created = ( + "Spawner entity created successfully", + "Failed to create spawner entity" + ) + spawner_type_set = ( + "Successfully set spawner type", + "Failed to set spawner type" + ) + empty_instance_count_validation = ( + "Expected number of empty instances planted", + "Unexpected number of empty instances planted" + ) + no_instances_when_empty_disallowed = ( + "No empty instances found when Empty Assets are not allowed", + "Unexpectedly found empty instances when Empty Assets are not allowed" + ) + nonempty_asset_instance_count_validation = ( + "Expected number of instances planted", + "Unexpected number of instances planted" + ) - def run_test(self): - """ - Summary: - Test aspects of the DynamicSliceInstanceSpawner through the BehaviorContext and the Property Tree. - :return: None - """ - # 1) Open an empty level - self.test_success = self.create_level( - self.args["level"], - heightmap_resolution=1024, - heightmap_meters_per_pixel=1, - terrain_texture_resolution=4096, - use_terrain=False, +def DynamicSliceInstanceSpawner_DynamicSliceSpawnerWorks(): + """ + Summary: + Test aspects of the DynamicSliceInstanceSpawner through the BehaviorContext and the Property Tree. + + :return: None + """ + + import os + + import azlmbr.legacy.general as general + import azlmbr.math as math + + import editor_python_test_tools.hydra_editor_utils as hydra + from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg + from editor_python_test_tools.utils import Report + from editor_python_test_tools.utils import TestHelper as helper + + # 1) Open an existing simple level + helper.init_idle() + helper.open_level("Physics", "Base") + general.set_current_view_position(512.0, 480.0, 38.0) + + # Grab the UUID that we need for creating an Dynamic Slice Instance Spawner + dynamic_slice_spawner_uuid = azlmbr.math.Uuid_CreateString('{BBA5CC1E-B4CA-4792-89F7-93711E98FBD1}', 0) + + # Grab a path to a test dynamic slice asset + test_slice_asset_path = os.path.join("Slices", "PinkFlower.dynamicslice") + + # 2) Test DynamicSliceInstanceSpawner BehaviorContext + behavior_context_test_success = True + dynamic_slice_spawner = azlmbr.vegetation.DynamicSliceInstanceSpawner() + behavior_context_test_success = behavior_context_test_success and (dynamic_slice_spawner is not None) + behavior_context_test_success = behavior_context_test_success and (dynamic_slice_spawner.typename == 'DynamicSliceInstanceSpawner') + Report.critical_result(BehaviorContextTests.spawner_initialized, behavior_context_test_success) + # Try to get/set the slice asset path with a valid asset + dynamic_slice_spawner.SetSliceAssetPath(test_slice_asset_path) + validate_path = dynamic_slice_spawner.GetSliceAssetPath() + # We expect the path to get lowercased and normalized with a forward slash, so we compare our result + # vs that instead of directly against test_slice_asset_path. + behavior_context_test_success = behavior_context_test_success and hydra.compare_values('slices/pinkflower.dynamicslice', validate_path, 'GetSliceAssetPath - valid') + Report.result(BehaviorContextTests.spawner_slice_asset_path_set, behavior_context_test_success) + # Try to get/set the slice asset path with an empty path + dynamic_slice_spawner.SetSliceAssetPath('') + validate_path = dynamic_slice_spawner.GetSliceAssetPath() + behavior_context_test_success = behavior_context_test_success and hydra.compare_values('', validate_path, 'GetSliceAssetPath - empty') + Report.result(BehaviorContextTests.spawner_empty_slice_asset_path_set, behavior_context_test_success) + Report.info(f'DynamicSliceInstanceSpawner() BehaviorContext test: {behavior_context_test_success}') + + # 3) Test Descriptor BehaviorContext - setting spawnerType sets spawner too + spawner_type_test_success = True + descriptor = azlmbr.vegetation.Descriptor() + spawner_type_test_success = spawner_type_test_success and hydra.get_set_property_test(descriptor, 'spawnerType', dynamic_slice_spawner_uuid) + spawner_type_test_success = spawner_type_test_success and (descriptor.spawner.typename == 'DynamicSliceInstanceSpawner') + Report.result(BehaviorContextTests.desc_spawnertype_sets_spawner, spawner_type_test_success) + Report.info(f'Descriptor() BehaviorContext spawnerType test: {spawner_type_test_success}') + + # 4) Test Descriptor BehaviorContext - setting spawner sets spawnerType too + spawner_test_success = True + descriptor = azlmbr.vegetation.Descriptor() + descriptor.spawner = dynamic_slice_spawner + spawner_test_success = spawner_test_success and (descriptor.spawnerType.Equal(dynamic_slice_spawner_uuid)) + spawner_test_success = spawner_test_success and (descriptor.spawner.typename == 'DynamicSliceInstanceSpawner') + Report.result(BehaviorContextTests.desc_spawner_sets_spawnertype, spawner_test_success) + Report.info(f'Descriptor() BehaviorContext spawner test: {spawner_test_success}') + + ### Setup for Property Tree set of tests + + # Create a new entity with required vegetation area components + spawner_entity = hydra.Entity("Veg Area") + spawner_entity.create_entity( + math.Vector3(512.0, 512.0, 32.0), + ["Vegetation Layer Spawner", "Box Shape", "Vegetation Asset List"] ) - general.idle_wait(1.0) - general.set_current_view_position(512.0, 480.0, 38.0) + Report.critical_result(PropertyTreeTests.entity_created, spawner_entity.id.IsValid()) - # Grab the UUID that we need for creating an Dynamic Slice Instance Spawner - dynamic_slice_spawner_uuid = azlmbr.math.Uuid_CreateString('{BBA5CC1E-B4CA-4792-89F7-93711E98FBD1}', 0) + # Resize the Box Shape component + new_box_dimensions = math.Vector3(16.0, 16.0, 16.0) + box_dimensions_path = "Box Shape|Box Configuration|Dimensions" + spawner_entity.get_set_test(1, box_dimensions_path, new_box_dimensions) - # Grab a path to a test dynamic slice asset - test_slice_asset_path = os.path.join("Slices", "PinkFlower.dynamicslice") + # Create a surface to plant on + dynveg.create_surface_entity("Surface Entity", math.Vector3(512.0, 512.0, 32.0), 1024.0, 1024.0, 1.0) - # 2) Test DynamicSliceInstanceSpawner BehaviorContext - behavior_context_test_success = True - dynamic_slice_spawner = azlmbr.vegetation.DynamicSliceInstanceSpawner() - behavior_context_test_success = behavior_context_test_success and (dynamic_slice_spawner is not None) - behavior_context_test_success = behavior_context_test_success and (dynamic_slice_spawner.typename == 'DynamicSliceInstanceSpawner') - # Try to get/set the slice asset path with a valid asset - dynamic_slice_spawner.SetSliceAssetPath(test_slice_asset_path) - validate_path = dynamic_slice_spawner.GetSliceAssetPath() - # We expect the path to get lowercased and normalized with a forward slash, so we compare our result - # vs that instead of directly against test_slice_asset_path. - behavior_context_test_success = behavior_context_test_success and hydra.compare_values('slices/pinkflower.dynamicslice', validate_path, 'GetSliceAssetPath - valid') - # Try to get/set the slice asset path with an empty path - dynamic_slice_spawner.SetSliceAssetPath('') - validate_path = dynamic_slice_spawner.GetSliceAssetPath() - behavior_context_test_success = behavior_context_test_success and hydra.compare_values('', validate_path, 'GetSliceAssetPath - empty') - self.test_success = self.test_success and behavior_context_test_success - self.log(f'DynamicSliceInstanceSpawner() BehaviorContext test: {behavior_context_test_success}') + # 5) Descriptor Property Tree test: spawner type can be set - # 3) Test Descriptor BehaviorContext - setting spawnerType sets spawner too - spawner_type_test_success = True - descriptor = azlmbr.vegetation.Descriptor() - spawner_type_test_success = spawner_type_test_success and hydra.get_set_property_test(descriptor, 'spawnerType', dynamic_slice_spawner_uuid) - spawner_type_test_success = spawner_type_test_success and (descriptor.spawner.typename == 'DynamicSliceInstanceSpawner') - self.test_success = self.test_success and spawner_type_test_success - self.log(f'Descriptor() BehaviorContext spawnerType test: {spawner_type_test_success}') + # - Validate the dynamic slice spawner type can be set correctly. + property_tree_success = True + property_tree_success = property_tree_success and spawner_entity.get_set_test(2, 'Configuration|Embedded Assets|[0]|Instance Spawner', dynamic_slice_spawner_uuid) + Report.result(PropertyTreeTests.spawner_type_set, property_tree_success) - # 4) Test Descriptor BehaviorContext - setting spawner sets spawnerType too - spawner_test_success = True - descriptor = azlmbr.vegetation.Descriptor() - descriptor.spawner = dynamic_slice_spawner - spawner_test_success = spawner_test_success and (descriptor.spawnerType.Equal(dynamic_slice_spawner_uuid)) - spawner_test_success = spawner_test_success and (descriptor.spawner.typename == 'DynamicSliceInstanceSpawner') - self.test_success = self.test_success and spawner_test_success - self.log(f'Descriptor() BehaviorContext spawner test: {spawner_test_success}') + # This should result in 400 instances, since our box is 16 m x 16 m and by default the veg system plants + # 20 instances per 16 meters + spawner_entity.get_set_test(0, 'Configuration|Allow Empty Assets', True) + num_expected_instances = 20 * 20 + property_tree_success = property_tree_success and helper.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, num_expected_instances), 5.0) + Report.result(PropertyTreeTests.empty_instance_count_validation, property_tree_success) + Report.info(f'Property Tree spawner type test: {property_tree_success}') - ### Setup for Property Tree set of tests + # 6) Validate that the "Allow Empty Assets" setting affects the DynamicSliceInstanceSpawner + allow_empty_assets_success = True + # Since we have an empty slice path, we should have 0 instances once we disable 'Allow Empty Assets' + num_expected_instances = 0 + allow_empty_assets_success = allow_empty_assets_success and spawner_entity.get_set_test(0, 'Configuration|Allow Empty Assets', False) + Report.info('Allow Empty Assets test:') + allow_empty_assets_success = allow_empty_assets_success and helper.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, num_expected_instances), 5.0) + Report.result(PropertyTreeTests.no_instances_when_empty_disallowed, allow_empty_assets_success) + Report.info(f'Allow Empty Assets test: {allow_empty_assets_success}') - # Create a new entity with required vegetation area components - spawner_entity = hydra.Entity("Veg Area") - spawner_entity.create_entity( - math.Vector3(512.0, 512.0, 32.0), - ["Vegetation Layer Spawner", "Box Shape", "Vegetation Asset List"] - ) - if (spawner_entity.id.IsValid()): - self.log(f"'{spawner_entity.name}' created") - - # Resize the Box Shape component - new_box_dimensions = math.Vector3(16.0, 16.0, 16.0) - box_dimensions_path = "Box Shape|Box Configuration|Dimensions" - spawner_entity.get_set_test(1, box_dimensions_path, new_box_dimensions) - - # Create a surface to plant on - dynveg.create_surface_entity("Surface Entity", math.Vector3(512.0, 512.0, 32.0), 1024.0, 1024.0, 1.0) - - # 5) Descriptor Property Tree test: spawner type can be set - - # - Validate the dynamic slice spawner type can be set correctly. - property_tree_success = True - property_tree_success = property_tree_success and spawner_entity.get_set_test(2, 'Configuration|Embedded Assets|[0]|Instance Spawner', dynamic_slice_spawner_uuid) - - # This should result in 400 instances, since our box is 16 m x 16 m and by default the veg system plants - # 20 instances per 16 meters - spawner_entity.get_set_test(0, 'Configuration|Allow Empty Assets', True) - num_expected_instances = 20 * 20 - property_tree_success = property_tree_success and self.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, num_expected_instances), 5.0) - self.test_success = self.test_success and property_tree_success - self.log(f'Property Tree spawner type test: {property_tree_success}') - - # 6) Validate that the "Allow Empty Assets" setting affects the DynamicSliceInstanceSpawner - allow_empty_assets_success = True - # Since we have an empty slice path, we should have 0 instances once we disable 'Allow Empty Assets' - num_expected_instances = 0 - allow_empty_assets_success = allow_empty_assets_success and spawner_entity.get_set_test(0, 'Configuration|Allow Empty Assets', False) - self.log('Allow Empty Assets test:') - allow_empty_assets_success = allow_empty_assets_success and self.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, num_expected_instances), 5.0) - self.test_success = self.test_success and allow_empty_assets_success - self.log(f'Allow Empty Assets test: {allow_empty_assets_success}') - - # 7) Validate that with 'Allow Empty Assets' set to False, a non-empty slice asset gives us the number - # of instances we expect. - spawns_slices_success = True - num_expected_instances = 20 * 20 - dynamic_slice_spawner.SetSliceAssetPath(test_slice_asset_path) - spawns_slices_success = spawns_slices_success and spawner_entity.get_set_test(0, 'Configuration|Allow Empty Assets', False) - descriptor = hydra.get_component_property_value(spawner_entity.components[2], 'Configuration|Embedded Assets|[0]') - descriptor.spawner = dynamic_slice_spawner - spawns_slices_success = spawns_slices_success and spawner_entity.get_set_test(2, "Configuration|Embedded Assets|[0]", descriptor) - self.log('Spawn dynamic slices test:') - spawns_slices_success = spawns_slices_success and self.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, num_expected_instances), 5.0) - self.test_success = self.test_success and spawns_slices_success - self.log(f'Spawn dynamic slices test: {spawns_slices_success}') + # 7) Validate that with 'Allow Empty Assets' set to False, a non-empty slice asset gives us the number + # of instances we expect. + spawns_slices_success = True + num_expected_instances = 20 * 20 + dynamic_slice_spawner.SetSliceAssetPath(test_slice_asset_path) + spawns_slices_success = spawns_slices_success and spawner_entity.get_set_test(0, 'Configuration|Allow Empty Assets', False) + descriptor = hydra.get_component_property_value(spawner_entity.components[2], 'Configuration|Embedded Assets|[0]') + descriptor.spawner = dynamic_slice_spawner + spawns_slices_success = spawns_slices_success and spawner_entity.get_set_test(2, "Configuration|Embedded Assets|[0]", descriptor) + Report.info('Spawn dynamic slices test:') + spawns_slices_success = spawns_slices_success and helper.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, num_expected_instances), 5.0) + Report.result(PropertyTreeTests.nonempty_asset_instance_count_validation, spawns_slices_success) + Report.info(f'Spawn dynamic slices test: {spawns_slices_success}') -test = TestDynamicSliceInstanceSpawner() -test.run() +if __name__ == "__main__": + + from editor_python_test_tools.utils import Report + Report.start_test(DynamicSliceInstanceSpawner_DynamicSliceSpawnerWorks) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/DynamicSliceInstanceSpawner_Embedded_E2E.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/DynamicSliceInstanceSpawner_Embedded_E2E.py index 6f2e711387..e51be58ec6 100755 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/DynamicSliceInstanceSpawner_Embedded_E2E.py +++ b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/DynamicSliceInstanceSpawner_Embedded_E2E.py @@ -5,99 +5,115 @@ For complete copyright and license terms please see the LICENSE at the root of t SPDX-License-Identifier: Apache-2.0 OR MIT """ -import os -import sys -sys.path.append(os.path.dirname(os.path.abspath(__file__))) -import azlmbr.asset as asset -import azlmbr.components as components -import azlmbr.legacy.general as general -import azlmbr.bus as bus -import azlmbr.entity as entity -import azlmbr.editor as editor -import azlmbr.math as math -import azlmbr.paths - -sys.path.append(os.path.join(azlmbr.paths.devroot, 'AutomatedTesting', 'Gem', 'PythonTests')) -import editor_python_test_tools.hydra_editor_utils as hydra -from editor_python_test_tools.editor_test_helper import EditorTestHelper -from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg +class Tests: + level_created = ( + "Successfully created level", + "Failed to create level" + ) + spawner_entity_created = ( + "Spawner entity created successfully", + "Failed to create spawner entity" + ) + surface_entity_created = ( + "Surface entity created successfully", + "Failed to create surface entity" + ) + instance_count = ( + "Found the expected number of instances", + "Found an unexpected number of instances" + ) + saved_and_exported = ( + "Saved and exported level successfully", + "Failed to save and export level" + ) -class TestDynamicSliceInstanceSpawnerEmbeddedEditor(EditorTestHelper): - def __init__(self): - EditorTestHelper.__init__(self, log_prefix="DynamicSliceInstanceSpawnerEmbeddedEditor", args=["level"]) +def DynamicSliceInstanceSpawner_Embedded_E2E(): + """ + Summary: + A new temporary level is created. Surface for planting is created. Simple vegetation area is created using + Dynamic Slice Instance Spawner type. - def run_test(self): - """ - Summary: - A new temporary level is created. Surface for planting is created. Simple vegetation area is created using - Dynamic Slice Instance Spawner type. + Expected Behavior: + Instances plant as expected in the assigned area. - Expected Behavior: - Instances plant as expected in the assigned area. + Test Steps: + 1) Create level + 2) Create a Vegetation Layer Spawner setup using Dynamic Slice Instance Spawner type assets + 3) Create a surface to plant on + 4) Verify expected instance counts + 5) Add a camera component looking at the planting area for visual debugging + 6) Save and export to engine - Test Steps: - 1) Create level - 2) Create a Vegetation Layer Spawner setup using Dynamic Slice Instance Spawner type assets - 3) Create a surface to plant on - 4) Verify expected instance counts - 5) Add a camera component looking at the planting area for visual debugging - 6) Save and export to engine + Note: + - This test file must be called from the Open 3D Engine Editor command terminal + - Any passed and failed tests are written to the Editor.log file. + Parsing the file or running a log_monitor are required to observe the test results. - Note: - - This test file must be called from the Open 3D Engine Editor command terminal - - Any passed and failed tests are written to the Editor.log file. - Parsing the file or running a log_monitor are required to observe the test results. + :return: None + """ - :return: None - """ + import os - # 1) Create a new, temporary level - self.test_success = self.create_level( - self.args["level"], - heightmap_resolution=1024, - heightmap_meters_per_pixel=1, - terrain_texture_resolution=4096, - use_terrain=False, - ) + import azlmbr.asset as asset + import azlmbr.legacy.general as general + import azlmbr.bus as bus + import azlmbr.components as components + import azlmbr.entity as entity + import azlmbr.math as math + import azlmbr.paths as paths - general.set_current_view_position(512.0, 480.0, 38.0) + import editor_python_test_tools.hydra_editor_utils as hydra + from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg + from editor_python_test_tools.utils import Report + from editor_python_test_tools.utils import TestHelper as helper - # 2) Create a new entity with required vegetation area components and Script Canvas component for launcher test - center_point = math.Vector3(512.0, 512.0, 32.0) - asset_path = os.path.join("Slices", "PinkFlower.dynamicslice") - spawner_entity = dynveg.create_vegetation_area("Instance Spawner", center_point, 16.0, 16.0, 1.0, asset_path) - spawner_entity.add_component("Script Canvas") - instance_counter_path = os.path.join("scriptcanvas", "instance_counter.scriptcanvas") - instance_counter_script = asset.AssetCatalogRequestBus(bus.Broadcast, "GetAssetIdByPath", instance_counter_path, - math.Uuid(), False) - spawner_entity.get_set_test(3, "Script Canvas Asset|Script Canvas Asset", instance_counter_script) + # 1) Create a new, temporary level + lvl_name = "tmp_level" + helper.init_idle() + level_created = general.create_level_no_prompt(lvl_name, 1024, 1, 4096, False) + general.idle_wait(1.0) + Report.critical_result(Tests.level_created, level_created == 0) + general.set_current_view_position(512.0, 480.0, 38.0) - # 3) Create a surface to plant on - dynveg.create_surface_entity("Planting Surface", center_point, 128.0, 128.0, 1.0) + # 2) Create a new entity with required vegetation area components and Script Canvas component for launcher test + center_point = math.Vector3(512.0, 512.0, 32.0) + asset_path = os.path.join("Slices", "PinkFlower.dynamicslice") + spawner_entity = dynveg.create_vegetation_area("Instance Spawner", center_point, 16.0, 16.0, 1.0, asset_path) + spawner_entity.add_component("Script Canvas") + instance_counter_path = os.path.join("scriptcanvas", "instance_counter.scriptcanvas") + instance_counter_script = asset.AssetCatalogRequestBus(bus.Broadcast, "GetAssetIdByPath", instance_counter_path, + math.Uuid(), False) + spawner_entity.get_set_test(3, "Script Canvas Asset|Script Canvas Asset", instance_counter_script) + Report.result(Tests.spawner_entity_created, spawner_entity.id.IsValid() and hydra.has_components(spawner_entity.id, + ["Script Canvas"])) - # 4) Verify instance counts are accurate - general.idle_wait(3.0) # Allow a few seconds for instances to spawn - num_expected_instances = 20 * 20 - box = azlmbr.shape.ShapeComponentRequestsBus(bus.Event, 'GetEncompassingAabb', spawner_entity.id) - num_found = azlmbr.areasystem.AreaSystemRequestBus(bus.Broadcast, 'GetInstanceCountInAabb', box) - self.log(f"Expected {num_expected_instances} instances - Found {num_found} instances") - self.test_success = self.test_success and num_found == num_expected_instances + # 3) Create a surface to plant on + surface_entity = dynveg.create_surface_entity("Planting Surface", center_point, 128.0, 128.0, 1.0) + Report.result(Tests.surface_entity_created, surface_entity.id.IsValid()) - # 5) Move the default Camera entity for testing in the launcher - cam_position = math.Vector3(512.0, 500.0, 35.0) - search_filter = entity.SearchFilter() - search_filter.names = ["Camera"] - search_entity_ids = entity.SearchBus(bus.Broadcast, 'SearchEntities', search_filter) - components.TransformBus(bus.Event, "MoveEntity", search_entity_ids[0], cam_position) + # 4) Verify instance counts are accurate + num_expected_instances = 20 * 20 + success = helper.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, + num_expected_instances), 5.0) + Report.result(Tests.instance_count, success) - # 6) Save and export to engine - general.save_level() - general.idle_wait(1.0) - general.export_to_engine() - general.idle_wait(1.0) + # 5) Move the default Camera entity for testing in the launcher + cam_position = math.Vector3(512.0, 500.0, 35.0) + search_filter = entity.SearchFilter() + search_filter.names = ["Camera"] + search_entity_ids = entity.SearchBus(bus.Broadcast, 'SearchEntities', search_filter) + components.TransformBus(bus.Event, "MoveEntity", search_entity_ids[0], cam_position) + + # 6) Save and export to engine + general.save_level() + general.export_to_engine() + pak_path = os.path.join(paths.devroot, "AutomatedTesting", "cache", "pc", "levels", lvl_name, "level.pak") + Report.result(Tests.saved_and_exported, os.path.exists(pak_path)) -test = TestDynamicSliceInstanceSpawnerEmbeddedEditor() -test.run() +if __name__ == "__main__": + + from editor_python_test_tools.utils import Report + Report.start_test(DynamicSliceInstanceSpawner_Embedded_E2E) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/DynamicSliceInstanceSpawner_External_E2E.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/DynamicSliceInstanceSpawner_External_E2E.py index dfa4d8191e..7a0abdd969 100755 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/DynamicSliceInstanceSpawner_External_E2E.py +++ b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/DynamicSliceInstanceSpawner_External_E2E.py @@ -5,122 +5,137 @@ For complete copyright and license terms please see the LICENSE at the root of t SPDX-License-Identifier: Apache-2.0 OR MIT """ -import os -import sys -sys.path.append(os.path.dirname(os.path.abspath(__file__))) -import azlmbr.legacy.general as general -import azlmbr.asset as asset -import azlmbr.bus as bus -import azlmbr.components as components -import azlmbr.entity as entity -import azlmbr.editor as editor -import azlmbr.math as math -import azlmbr.paths - -sys.path.append(os.path.join(azlmbr.paths.devroot, 'AutomatedTesting', 'Gem', 'PythonTests')) -import editor_python_test_tools.hydra_editor_utils as hydra -from editor_python_test_tools.editor_test_helper import EditorTestHelper -from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg +class Tests: + level_created = ( + "Successfully created level", + "Failed to create level" + ) + spawner_entity_created = ( + "Spawner entity created successfully", + "Failed to create spawner entity" + ) + surface_entity_created = ( + "Surface entity created successfully", + "Failed to create surface entity" + ) + instance_count = ( + "Found the expected number of instances", + "Found an unexpected number of instances" + ) + saved_and_exported = ( + "Saved and exported level successfully", + "Failed to save and export level" + ) -class TestDynamicSliceInstanceSpawnerExternalEditor(EditorTestHelper): - def __init__(self): - EditorTestHelper.__init__(self, log_prefix="DynamicSliceInstanceSpawnerExternalEditor", args=["level"]) +def DynamicSliceInstanceSpawner_External_E2E(): + """ + Summary: + A new temporary level is created. Surface for planting is created. Simple vegetation area is created using + Dynamic Slice Instance Spawner type using external assets. - def run_test(self): - """ - Summary: - A new temporary level is created. Surface for planting is created. Simple vegetation area is created using - Dynamic Slice Instance Spawner type using external assets. + Expected Behavior: + Instances plant as expected in the assigned area. - Expected Behavior: - Instances plant as expected in the assigned area. + Test Steps: + 1) Create level + 2) Create a Vegetation Layer Spawner setup using Dynamic Slice Instance Spawner type assets + 3) Create a surface to plant on + 4) Verify expected instance counts + 5) Add a camera component looking at the planting area for visual debugging + 6) Save and export to engine - Test Steps: - 1) Create level - 2) Create a Vegetation Layer Spawner setup using Dynamic Slice Instance Spawner type assets - 3) Create a surface to plant on - 4) Verify expected instance counts - 5) Add a camera component looking at the planting area for visual debugging - 6) Save and export to engine + Note: + - This test file must be called from the Open 3D Engine Editor command terminal + - Any passed and failed tests are written to the Editor.log file. + Parsing the file or running a log_monitor are required to observe the test results. - Note: - - This test file must be called from the Open 3D Engine Editor command terminal - - Any passed and failed tests are written to the Editor.log file. - Parsing the file or running a log_monitor are required to observe the test results. + :return: None + """ - :return: None - """ + import os - # 1) Create a new, temporary level - self.test_success = self.create_level( - self.args["level"], - heightmap_resolution=1024, - heightmap_meters_per_pixel=1, - terrain_texture_resolution=4096, - use_terrain=False, - ) + import azlmbr.asset as asset + import azlmbr.components as components + import azlmbr.editor as editor + import azlmbr.entity as entity + import azlmbr.legacy.general as general + import azlmbr.bus as bus + import azlmbr.math as math + import azlmbr.paths as paths - general.set_current_view_position(512.0, 480.0, 38.0) + import editor_python_test_tools.hydra_editor_utils as hydra + from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg + from editor_python_test_tools.utils import Report + from editor_python_test_tools.utils import TestHelper as helper - # 2) Create a new entity with required vegetation area components and switch the Vegetation Asset List Source - # Type to External - entity_position = math.Vector3(512.0, 512.0, 32.0) - veg_area_required_components = ["Vegetation Layer Spawner", "Box Shape", "Vegetation Asset List", - "Script Canvas"] - new_entity_id = editor.ToolsApplicationRequestBus( - bus.Broadcast, "CreateNewEntityAtPosition", entity_position, entity.EntityId() - ) - if new_entity_id.IsValid(): - self.log("Spawner entity created") - spawner_entity = hydra.Entity("Spawner Entity", new_entity_id) - spawner_entity.components = [] - for component in veg_area_required_components: - spawner_entity.components.append(hydra.add_component(component, new_entity_id)) - hydra.get_set_test(spawner_entity, 2, "Configuration|Source Type", 1) + # 1) Create a new, temporary level + lvl_name = "tmp_level" + helper.init_idle() + level_created = general.create_level_no_prompt(lvl_name, 1024, 1, 4096, False) + general.idle_wait(1.0) + Report.critical_result(Tests.level_created, level_created == 0) + general.set_current_view_position(512.0, 480.0, 38.0) - # Add a Script Canvas component with instance_counter script for launcher tests - instance_counter_path = os.path.join("scriptcanvas", "instance_counter.scriptcanvas") - instance_counter_script = asset.AssetCatalogRequestBus(bus.Broadcast, "GetAssetIdByPath", instance_counter_path, - math.Uuid(), False) - spawner_entity.get_set_test(3, "Script Canvas Asset|Script Canvas Asset", instance_counter_script) + # 2) Create a new entity with required vegetation area components and switch the Vegetation Asset List Source + # Type to External + entity_position = math.Vector3(512.0, 512.0, 32.0) + veg_area_required_components = ["Vegetation Layer Spawner", "Box Shape", "Vegetation Asset List", + "Script Canvas"] + new_entity_id = editor.ToolsApplicationRequestBus( + bus.Broadcast, "CreateNewEntityAtPosition", entity_position, entity.EntityId() + ) + spawner_entity = hydra.Entity("Spawner Entity", new_entity_id) + spawner_entity.components = [] + for component in veg_area_required_components: + spawner_entity.components.append(hydra.add_component(component, new_entity_id)) + hydra.get_set_test(spawner_entity, 2, "Configuration|Source Type", 1) - # Assign a Vegetation Descriptor List asset to the Vegetation Asset List component - descriptor_asset = asset.AssetCatalogRequestBus( - bus.Broadcast, "GetAssetIdByPath", os.path.join("Assets", "VegDescriptorLists", "flower_pink.vegdescriptorlist"), math.Uuid(), - False) - hydra.get_set_test(spawner_entity, 2, "Configuration|External Assets", descriptor_asset) + # Add a Script Canvas component with instance_counter script for launcher tests + instance_counter_path = os.path.join("scriptcanvas", "instance_counter.scriptcanvas") + instance_counter_script = asset.AssetCatalogRequestBus(bus.Broadcast, "GetAssetIdByPath", instance_counter_path, + math.Uuid(), False) + spawner_entity.get_set_test(3, "Script Canvas Asset|Script Canvas Asset", instance_counter_script) + Report.result(Tests.spawner_entity_created, spawner_entity.id.IsValid() and hydra.has_components(spawner_entity.id, + ["Script Canvas"])) - # Resize the Box Shape component - new_box_dimensions = math.Vector3(16.0, 16.0, 16.0) - box_dimensions_path = "Box Shape|Box Configuration|Dimensions" - hydra.get_set_test(spawner_entity, 1, box_dimensions_path, new_box_dimensions) + # Assign a Vegetation Descriptor List asset to the Vegetation Asset List component + descriptor_asset = asset.AssetCatalogRequestBus( + bus.Broadcast, "GetAssetIdByPath", os.path.join("Assets", "VegDescriptorLists", "flower_pink.vegdescriptorlist"), math.Uuid(), + False) + hydra.get_set_test(spawner_entity, 2, "Configuration|External Assets", descriptor_asset) - # 3) Create a surface to plant on - dynveg.create_surface_entity("Planting Surface", entity_position, 128.0, 128.0, 1.0) + # Resize the Box Shape component + new_box_dimensions = math.Vector3(16.0, 16.0, 16.0) + box_dimensions_path = "Box Shape|Box Configuration|Dimensions" + hydra.get_set_test(spawner_entity, 1, box_dimensions_path, new_box_dimensions) - # 4) Verify instance counts are accurate - general.idle_wait(3.0) # Allow a few seconds for instances to spawn - num_expected_instances = 20 * 20 - box = azlmbr.shape.ShapeComponentRequestsBus(bus.Event, 'GetEncompassingAabb', spawner_entity.id) - num_found = azlmbr.areasystem.AreaSystemRequestBus(bus.Broadcast, 'GetInstanceCountInAabb', box) - self.log(f"Expected {num_expected_instances} instances - Found {num_found} instances") - self.test_success = self.test_success and num_found == num_expected_instances + # 3) Create a surface to plant on + surface_entity = dynveg.create_surface_entity("Planting Surface", entity_position, 128.0, 128.0, 1.0) + Report.result(Tests.surface_entity_created, surface_entity.id.IsValid()) - # 5) Move the default Camera entity for testing in the launcher - cam_position = math.Vector3(512.0, 500.0, 35.0) - search_filter = entity.SearchFilter() - search_filter.names = ["Camera"] - search_entity_ids = entity.SearchBus(bus.Broadcast, 'SearchEntities', search_filter) - components.TransformBus(bus.Event, "MoveEntity", search_entity_ids[0], cam_position) + # 4) Verify instance counts are accurate + num_expected_instances = 20 * 20 + success = helper.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, + num_expected_instances), 5.0) + Report.result(Tests.instance_count, success) - # 6) Save and export to engine - general.save_level() - general.idle_wait(1.0) - general.export_to_engine() - general.idle_wait(1.0) + # 5) Move the default Camera entity for testing in the launcher + cam_position = math.Vector3(512.0, 500.0, 35.0) + search_filter = entity.SearchFilter() + search_filter.names = ["Camera"] + search_entity_ids = entity.SearchBus(bus.Broadcast, 'SearchEntities', search_filter) + components.TransformBus(bus.Event, "MoveEntity", search_entity_ids[0], cam_position) + + # 6) Save and export to engine + general.save_level() + general.export_to_engine() + pak_path = os.path.join(paths.devroot, "AutomatedTesting", "cache", "pc", "levels", lvl_name, "level.pak") + Report.result(Tests.saved_and_exported, os.path.exists(pak_path)) -test = TestDynamicSliceInstanceSpawnerExternalEditor() -test.run() +if __name__ == "__main__": + + from editor_python_test_tools.utils import Report + Report.start_test(DynamicSliceInstanceSpawner_External_E2E) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/EmptyInstanceSpawner_EmptySpawnerWorks.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/EmptyInstanceSpawner_EmptySpawnerWorks.py index 721d06f858..d0a51809b4 100755 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/EmptyInstanceSpawner_EmptySpawnerWorks.py +++ b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/EmptyInstanceSpawner_EmptySpawnerWorks.py @@ -5,109 +5,131 @@ For complete copyright and license terms please see the LICENSE at the root of t SPDX-License-Identifier: Apache-2.0 OR MIT """ -import os -import sys -import azlmbr.legacy.general as general -import azlmbr.bus as bus -import azlmbr.math as math -import azlmbr.paths - -sys.path.append(os.path.join(azlmbr.paths.devroot, 'AutomatedTesting', 'Gem', 'PythonTests')) -import editor_python_test_tools.hydra_editor_utils as hydra -from editor_python_test_tools.editor_test_helper import EditorTestHelper -from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg +class BehaviorContextTests: + spawner_initialized = ( + "Successfully initialized an Empty Instance Spawner", + "Failed to initialize an Empty Instance Spawner" + ) + desc_spawnertype_sets_spawner = ( + "Setting spawnerType sets spawner too", + "Setting spawnerType failed to set spawner to expected value" + ) + desc_spawner_sets_spawnertype = ( + "Setting spawner sets spawnerType too", + "Setting spawner failed to set spawnerType to expected value" + ) -class TestEmptyInstanceSpawner(EditorTestHelper): - def __init__(self): - EditorTestHelper.__init__(self, log_prefix="EmptyInstanceSpawner", args=["level"]) +class PropertyTreeTests: + entity_created = ( + "Spawner entity created successfully", + "Failed to create spawner entity" + ) + spawner_type_set = ( + "Successfully set spawner type", + "Failed to set spawner type" + ) + empty_instance_count_validation = ( + "Expected number of empty instances planted", + "Unexpected number of empty instances planted" + ) + not_affected_by_allow_empty_assets = ( + "Instance count unaffected by Allow Empty Assets toggle", + "Instance count was unexpectedly affected by Allow Empty Assets toggle" + ) - def run_test(self): - """ - Summary: - Test aspects of the EmptyInstanceSpawner through the BehaviorContext and the Property Tree. - :return: None - """ - # 1) Open an empty level - self.test_success = self.create_level( - self.args["level"], - heightmap_resolution=1024, - heightmap_meters_per_pixel=1, - terrain_texture_resolution=4096, - use_terrain=False, +def EmptyInstanceSpawner_EmptySpawnerWorks(): + """ + Summary: + Test aspects of the EmptyInstanceSpawner through the BehaviorContext and the Property Tree. + + :return: None + """ + + import azlmbr.legacy.general as general + import azlmbr.math as math + + import editor_python_test_tools.hydra_editor_utils as hydra + from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg + from editor_python_test_tools.utils import Report + from editor_python_test_tools.utils import TestHelper as helper + + # 1) Open an existing simple level + helper.init_idle() + helper.open_level("Physics", "Base") + general.set_current_view_position(512.0, 480.0, 38.0) + + # Grab the UUID that we need for creating an Empty Spawner + empty_spawner_uuid = azlmbr.math.Uuid_CreateString('{23C40FD4-A55F-4BD3-BE5B-DC5423F217C2}', 0) + + # 2) Test EmptyInstanceSpawner BehaviorContext + behavior_context_test_success = True + empty_spawner = azlmbr.vegetation.EmptyInstanceSpawner() + behavior_context_test_success = behavior_context_test_success and (empty_spawner is not None) + behavior_context_test_success = behavior_context_test_success and (empty_spawner.typename == 'EmptyInstanceSpawner') + Report.critical_result(BehaviorContextTests.spawner_initialized, behavior_context_test_success) + Report.info(f'EmptyInstanceSpawner() BehaviorContext test: {behavior_context_test_success}') + + # 3) Test Descriptor BehaviorContext - setting spawnerType sets spawner too + spawner_type_test_success = True + descriptor = azlmbr.vegetation.Descriptor() + spawner_type_test_success = spawner_type_test_success and hydra.get_set_property_test(descriptor, 'spawnerType', empty_spawner_uuid) + spawner_type_test_success = spawner_type_test_success and (descriptor.spawner.typename == 'EmptyInstanceSpawner') + Report.result(BehaviorContextTests.desc_spawnertype_sets_spawner, spawner_type_test_success) + Report.info(f'Descriptor() BehaviorContext spawnerType test: {spawner_type_test_success}') + + # 4) Test Descriptor BehaviorContext - setting spawner sets spawnerType too + spawner_test_success = True + descriptor = azlmbr.vegetation.Descriptor() + descriptor.spawner = empty_spawner + spawner_test_success = spawner_test_success and (descriptor.spawnerType.Equal(empty_spawner_uuid)) + spawner_test_success = spawner_test_success and (descriptor.spawner.typename == 'EmptyInstanceSpawner') + Report.result(BehaviorContextTests.desc_spawner_sets_spawnertype, spawner_test_success) + Report.info(f'Descriptor() BehaviorContext spawner test: {spawner_test_success}') + + ### Setup for Property Tree set of tests + + # Create a new entity with required vegetation area components + spawner_entity = hydra.Entity("Veg Area") + spawner_entity.create_entity( + math.Vector3(512.0, 512.0, 32.0), + ["Vegetation Layer Spawner", "Box Shape", "Vegetation Asset List"] ) - general.idle_wait(1.0) - general.set_current_view_position(512.0, 480.0, 38.0) + Report.critical_result(PropertyTreeTests.entity_created, spawner_entity.id.IsValid()) - # Grab the UUID that we need for creating an Empty Spawner - empty_spawner_uuid = azlmbr.math.Uuid_CreateString('{23C40FD4-A55F-4BD3-BE5B-DC5423F217C2}', 0) + # Resize the Box Shape component + new_box_dimensions = math.Vector3(16.0, 16.0, 16.0) + box_dimensions_path = "Box Shape|Box Configuration|Dimensions" + spawner_entity.get_set_test(1, box_dimensions_path, new_box_dimensions) - # 2) Test EmptyInstanceSpawner BehaviorContext - behavior_context_test_success = True - empty_spawner = azlmbr.vegetation.EmptyInstanceSpawner() - behavior_context_test_success = behavior_context_test_success and (empty_spawner is not None) - behavior_context_test_success = behavior_context_test_success and (empty_spawner.typename == 'EmptyInstanceSpawner') - self.test_success = self.test_success and behavior_context_test_success - self.log(f'EmptyInstanceSpawner() BehaviorContext test: {behavior_context_test_success}') + # Create a surface to plant on + dynveg.create_surface_entity("Surface Entity", math.Vector3(512.0, 512.0, 32.0), 1024.0, 1024.0, 1.0) - # 3) Test Descriptor BehaviorContext - setting spawnerType sets spawner too - spawner_type_test_success = True - descriptor = azlmbr.vegetation.Descriptor() - spawner_type_test_success = spawner_type_test_success and hydra.get_set_property_test(descriptor, 'spawnerType', empty_spawner_uuid) - spawner_type_test_success = spawner_type_test_success and (descriptor.spawner.typename == 'EmptyInstanceSpawner') - self.test_success = self.test_success and spawner_type_test_success - self.log(f'Descriptor() BehaviorContext spawnerType test: {spawner_type_test_success}') + # 5) Descriptor Property Tree test: spawner type can be set - # 4) Test Descriptor BehaviorContext - setting spawner sets spawnerType too - spawner_test_success = True - descriptor = azlmbr.vegetation.Descriptor() - descriptor.spawner = empty_spawner - spawner_test_success = spawner_test_success and (descriptor.spawnerType.Equal(empty_spawner_uuid)) - spawner_test_success = spawner_test_success and (descriptor.spawner.typename == 'EmptyInstanceSpawner') - self.test_success = self.test_success and spawner_test_success - self.log(f'Descriptor() BehaviorContext spawner test: {spawner_test_success}') + # - Validate the empty spawner type can be set correctly. + property_tree_success = True + property_tree_success = property_tree_success and spawner_entity.get_set_test(2, 'Configuration|Embedded Assets|[0]|Instance Spawner', empty_spawner_uuid) + Report.result(PropertyTreeTests.spawner_type_set, property_tree_success) - ### Setup for Property Tree set of tests + # This should result in 400 instances, since our box is 16 m x 16 m and by default the veg system plants + # 20 instances per 16 meters + num_expected_instances = 20 * 20 + property_tree_success = property_tree_success and helper.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, num_expected_instances), 5.0) + Report.result(PropertyTreeTests.empty_instance_count_validation, property_tree_success) + Report.info(f'Property Tree spawner type test: {property_tree_success}') - # Create a new entity with required vegetation area components - spawner_entity = hydra.Entity("Veg Area") - spawner_entity.create_entity( - math.Vector3(512.0, 512.0, 32.0), - ["Vegetation Layer Spawner", "Box Shape", "Vegetation Asset List"] - ) - if spawner_entity.id.IsValid(): - self.log(f"'{spawner_entity.name}' created") - - # Resize the Box Shape component - new_box_dimensions = math.Vector3(16.0, 16.0, 16.0) - box_dimensions_path = "Box Shape|Box Configuration|Dimensions" - spawner_entity.get_set_test(1, box_dimensions_path, new_box_dimensions) - - # Create a surface to plant on - dynveg.create_surface_entity("Surface Entity", math.Vector3(512.0, 512.0, 32.0), 1024.0, 1024.0, 1.0) - - # 5) Descriptor Property Tree test: spawner type can be set - - # - Validate the empty spawner type can be set correctly. - property_tree_success = True - property_tree_success = property_tree_success and spawner_entity.get_set_test(2, 'Configuration|Embedded Assets|[0]|Instance Spawner', empty_spawner_uuid) - - # This should result in 400 instances, since our box is 16 m x 16 m and by default the veg system plants - # 20 instances per 16 meters - num_expected_instances = 20 * 20 - property_tree_success = property_tree_success and self.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, num_expected_instances), 5.0) - self.test_success = self.test_success and property_tree_success - self.log(f'Property Tree spawner type test: {property_tree_success}') - - # 6) Validate that the "Allow Empty Assets" setting doesn't affect the EmptyInstanceSpawner - allow_empty_assets_success = True - spawner_entity.get_set_test(0, 'Configuration|Allow Empty Assets', False) - allow_empty_assets_success = allow_empty_assets_success and self.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, num_expected_instances), 5.0) - self.test_success = self.test_success and allow_empty_assets_success - self.log(f'Allow Empty Assets test: {allow_empty_assets_success}') + # 6) Validate that the "Allow Empty Assets" setting doesn't affect the EmptyInstanceSpawner + allow_empty_assets_success = True + spawner_entity.get_set_test(0, 'Configuration|Allow Empty Assets', False) + allow_empty_assets_success = allow_empty_assets_success and helper.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, num_expected_instances), 5.0) + Report.result(PropertyTreeTests.not_affected_by_allow_empty_assets, allow_empty_assets_success) + Report.info(f'Allow Empty Assets test: {allow_empty_assets_success}') -test = TestEmptyInstanceSpawner() -test.run() +if __name__ == "__main__": + + from editor_python_test_tools.utils import Report + Report.start_test(EmptyInstanceSpawner_EmptySpawnerWorks) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/InstanceSpawnerPriority_LayerAndSubPriority.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/InstanceSpawnerPriority_LayerAndSubPriority.py index e5ff0bf86f..98418c2432 100755 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/InstanceSpawnerPriority_LayerAndSubPriority.py +++ b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/InstanceSpawnerPriority_LayerAndSubPriority.py @@ -5,111 +5,115 @@ For complete copyright and license terms please see the LICENSE at the root of t SPDX-License-Identifier: Apache-2.0 OR MIT """ -import os -import sys -sys.path.append(os.path.dirname(os.path.abspath(__file__))) -import azlmbr.editor as editor -import azlmbr.legacy.general as general -import azlmbr.bus as bus -import azlmbr.math as math -import azlmbr.paths -import azlmbr.vegetation as vegetation - -sys.path.append(os.path.join(azlmbr.paths.devroot, 'AutomatedTesting', 'Gem', 'PythonTests')) -import editor_python_test_tools.hydra_editor_utils as hydra -from editor_python_test_tools.editor_test_helper import EditorTestHelper -from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg +class Tests: + initial_instance_count = ( + "Initial instance count is as expected", + "Initial instance count does not match expected results" + ) + layer_priority_instance_count = ( + "Instance count is as expected after updating layer priorities", + "Instance count does not match expected results after updating layer priorities" + ) + sub_priority_instance_count = ( + "Instance count is as expected after updating sub priorities", + "Instance count does not match expected results after updating sub priorities" + ) -class TestInstanceSpawnerPriority(EditorTestHelper): - def __init__(self): - EditorTestHelper.__init__(self, log_prefix="InstanceSpawnerPriority", args=["level"]) +def InstanceSpawnerPriority_LayerAndSubPriority(): + """ + Summary: + A new level is created. An instance spawner area and blocker area are setup to overlap. Instance counts are + verified with the initial setup. Layer priority on the blocker area is set to lower than the instance spawner + area, and instance counts are re-verified. - def run_test(self): - """ - Summary: - A new level is created. An instance spawner area and blocker area are setup to overlap. Instance counts are - verified with the initial setup. Layer priority on the blocker area is set to lower than the instance spawner - area, and instance counts are re-verified. + Expected Behavior: + Vegetation areas with a higher Layer Priority plant over those with a lower Layer Priority - Expected Behavior: - Vegetation areas with a higher Layer Priority plant over those with a lower Layer Priority + Test Steps: + 1) Open a simple level + 2) Create overlapping instance spawner and blocker areas + 3) Create a surface to plant on + 4) Validate initial instance counts in the spawner area + 5) Reduce the Layer Priority of the blocker area + 6) Validate instance counts in the spawner area - Test Steps: - 1) Create a new level - 2) Create overlapping instance spawner and blocker areas - 3) Create a surface to plant on - 4) Validate initial instance counts in the spawner area - 5) Reduce the Layer Priority of the blocker area - 6) Validate instance counts in the spawner area - - Note: - - This test file must be called from the Open 3D Engine Editor command terminal - - Any passed and failed tests are written to the Editor.log file. - Parsing the file or running a log_monitor are required to observe the test results. + Note: + - This test file must be called from the Open 3D Engine Editor command terminal + - Any passed and failed tests are written to the Editor.log file. + Parsing the file or running a log_monitor are required to observe the test results. - :return: None - """ + :return: None + """ - # 1) Create a new, temporary level - self.test_success = self.create_level( - self.args["level"], - heightmap_resolution=1024, - heightmap_meters_per_pixel=1, - terrain_texture_resolution=4096, - use_terrain=False, - ) + import os - # Set view of planting area for visual debugging - general.set_current_view_position(512.0, 500.0, 38.0) - general.set_current_view_rotation(-20.0, 0.0, 0.0) + import azlmbr.editor as editor + import azlmbr.legacy.general as general + import azlmbr.bus as bus + import azlmbr.math as math - # 2) Create overlapping areas: 1 instance spawner area, and 1 blocker area - spawner_center_point = math.Vector3(508.0, 508.0, 32.0) - asset_path = os.path.join("Slices", "PinkFlower.dynamicslice") - spawner_entity = dynveg.create_vegetation_area("Instance Spawner", spawner_center_point, 16.0, 16.0, 1.0, - asset_path) - blocker_center_point = math.Vector3(516.0, 516.0, 32.0) - blocker_entity = dynveg.create_blocker_area("Instance Blocker", blocker_center_point, 16.0, 16.0, 1.0) + import editor_python_test_tools.hydra_editor_utils as hydra + from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg + from editor_python_test_tools.utils import Report + from editor_python_test_tools.utils import TestHelper as helper - # 3) Create a surface for planting - planting_surface_center_point = math.Vector3(512.0, 512.0, 32.0) - dynveg.create_surface_entity("Planting Surface", planting_surface_center_point, 64.0, 64.0, 1.0) + # 1) Open an existing simple level + helper.init_idle() + helper.open_level("Physics", "Base") - # Set instances to spawn on a center snap point to avoid unexpected instances around the edges of the box shape - veg_system_settings_component = hydra.add_level_component("Vegetation System Settings") - editor.EditorComponentAPIBus(bus.Broadcast, "SetComponentProperty", veg_system_settings_component, - 'Configuration|Area System Settings|Sector Point Snap Mode', 1) + # Set view of planting area for visual debugging + general.set_current_view_position(512.0, 500.0, 38.0) + general.set_current_view_rotation(-20.0, 0.0, 0.0) - # 4) Validate the expected instance count with initial setup. GetAreaProductCount is used as - # GetInstanceCountInAabb does not filter out blocked instances - num_expected = (20 * 20) - (10 * 10) # 20 instances per 16m per side minus 1 blocked quadrant - result = self.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, - num_expected), 5.0) - self.test_success = self.test_success and result + # 2) Create overlapping areas: 1 instance spawner area, and 1 blocker area + spawner_center_point = math.Vector3(508.0, 508.0, 32.0) + asset_path = os.path.join("Slices", "PinkFlower.dynamicslice") + spawner_entity = dynveg.create_vegetation_area("Instance Spawner", spawner_center_point, 16.0, 16.0, 1.0, + asset_path) + blocker_center_point = math.Vector3(516.0, 516.0, 32.0) + blocker_entity = dynveg.create_blocker_area("Instance Blocker", blocker_center_point, 16.0, 16.0, 1.0) - # 5) Change the Instance Spawner area to a higher layer priority than the Instance Blocker - blocker_entity.get_set_test(0, 'Configuration|Layer Priority', 0) + # 3) Create a surface for planting + planting_surface_center_point = math.Vector3(512.0, 512.0, 32.0) + dynveg.create_surface_entity("Planting Surface", planting_surface_center_point, 64.0, 64.0, 1.0) - # 6) Validate the expected instance count with changed area priorities - num_expected = 20 * 20 # 20 instances per 16m per side, no instances should be blocked at this point - result = self.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, - num_expected), 5.0) - self.test_success = self.test_success and result + # Set instances to spawn on a center snap point to avoid unexpected instances around the edges of the box shape + veg_system_settings_component = hydra.add_level_component("Vegetation System Settings") + editor.EditorComponentAPIBus(bus.Broadcast, "SetComponentProperty", veg_system_settings_component, + 'Configuration|Area System Settings|Sector Point Snap Mode', 1) - # 7) Revert Layer Priority changes so both areas are equal, and change Sub Priority to a higher value on the - # Instance Spawner area - blocker_entity.get_set_test(0, 'Configuration|Layer Priority', 1) - spawner_entity.get_set_test(0, 'Configuration|Sub Priority', 100) - blocker_entity.get_set_test(0, 'Configuration|Sub Priority', 1) + # 4) Validate the expected instance count with initial setup. GetAreaProductCount is used as + # GetInstanceCountInAabb does not filter out blocked instances + num_expected = (20 * 20) - (10 * 10) # 20 instances per 16m per side minus 1 blocked quadrant + result = helper.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, + num_expected), 5.0) + Report.result(Tests.initial_instance_count, result) - # 8) Validate the expected instance count with changed area priorities - num_expected = 20 * 20 # 20 instances per 16m per side, no instances should be blocked at this point - result = self.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, - num_expected), 5.0) - self.test_success = self.test_success and result + # 5) Change the Instance Spawner area to a higher layer priority than the Instance Blocker + blocker_entity.get_set_test(0, 'Configuration|Layer Priority', 0) + + # 6) Validate the expected instance count with changed area priorities + num_expected = 20 * 20 # 20 instances per 16m per side, no instances should be blocked at this point + result = helper.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, + num_expected), 5.0) + Report.result(Tests.layer_priority_instance_count, result) + + # 7) Revert Layer Priority changes so both areas are equal, and change Sub Priority to a higher value on the + # Instance Spawner area + blocker_entity.get_set_test(0, 'Configuration|Layer Priority', 1) + spawner_entity.get_set_test(0, 'Configuration|Sub Priority', 100) + blocker_entity.get_set_test(0, 'Configuration|Sub Priority', 1) + + # 8) Validate the expected instance count with changed area priorities + num_expected = 20 * 20 # 20 instances per 16m per side, no instances should be blocked at this point + result = helper.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, + num_expected), 5.0) + Report.result(Tests.sub_priority_instance_count, result) -test = TestInstanceSpawnerPriority() -test.run() +if __name__ == "__main__": + + from editor_python_test_tools.utils import Report + Report.start_test(InstanceSpawnerPriority_LayerAndSubPriority) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/LayerBlender_E2E_Editor.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/LayerBlender_E2E_Editor.py index f03ea29613..f56c0b836e 100755 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/LayerBlender_E2E_Editor.py +++ b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/LayerBlender_E2E_Editor.py @@ -5,151 +5,161 @@ For complete copyright and license terms please see the LICENSE at the root of t SPDX-License-Identifier: Apache-2.0 OR MIT """ -""" -C2627906: A simple Vegetation Layer Blender area can be created -""" -import os -from math import radians -import sys - -sys.path.append(os.path.dirname(os.path.abspath(__file__))) -import azlmbr.asset as asset -import azlmbr.areasystem as areasystem -import azlmbr.legacy.general as general -import azlmbr -import azlmbr.bus as bus -import azlmbr.components as components -import azlmbr.math as math -import azlmbr.entity as entity -import azlmbr.paths - -sys.path.append(os.path.join(azlmbr.paths.devroot, 'AutomatedTesting', 'Gem', 'PythonTests')) -import editor_python_test_tools.hydra_editor_utils as hydra -from editor_python_test_tools.editor_test_helper import EditorTestHelper -from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg +class Tests: + level_created = ( + "Successfully created level", + "Failed to create level" + ) + blender_entity_created = ( + "Blender entity created successfully", + "Failed to create Blender entity" + ) + instance_count = ( + "Found the expected number of instances in the Blender area", + "Found an unexpected number of instances in the Blender area" + ) + instances_blended = ( + "Instances from each spawner are blended as expected", + "Found an unexpected number of instances from each spawner" + ) + saved_and_exported = ( + "Saved and exported level successfully", + "Failed to save and export level" + ) -class TestVegLayerBlenderCreated(EditorTestHelper): - def __init__(self): - EditorTestHelper.__init__(self, log_prefix="LayerBlender_E2E_Editor", args=["level"]) - self.screenshot_count = 0 +def LayerBlender_E2E_Editor(): + """ + Summary: + A temporary level is loaded. Two vegetation areas with different meshes are added and then + pinned to a vegetation blender. Screenshots are taken in the editor normal mode and in game mode. - def run_test(self): - """ - Summary: - A temporary level is loaded. Two vegetation areas with different meshes are added and then - pinned to a vegetation blender. Screenshots are taken in the editor normal mode and in game mode. + Expected Behavior: + The specified assets plant in the specified blend area and are visible in the Viewport in + Edit Mode, Game Mode. - Expected Behavior: - The specified assets plant in the specified blend area and are visible in the Viewport in - Edit Mode, Game Mode. + Test Steps: + 1) Create level + 2) Create 2 vegetation areas with different meshes + 3) Create Blender entity and pin the vegetation areas + 4) Take screenshot in normal mode + 5) Create a new entity with a Camera component for testing in the launcher + 6) Save level and take screenshot in game mode + 7) Export to engine - Test Steps: - 1) Create level - 2) Create 2 vegetation areas with different meshes - 3) Create Blender entity and pin the vegetation areas - 4) Take screenshot in normal mode - 5) Create a new entity with a Camera component for testing in the launcher - 6) Save level and take screenshot in game mode - 7) Export to engine + Note: + - This test file must be called from the Open 3D Engine Editor command terminal + - Any passed and failed tests are written to the Editor.log file. + Parsing the file or running a log_monitor are required to observe the test results. - Note: - - This test file must be called from the Open 3D Engine Editor command terminal - - Any passed and failed tests are written to the Editor.log file. - Parsing the file or running a log_monitor are required to observe the test results. + :return: None + """ - :return: None - """ + import os + from math import radians - # 1) Create/prepare a new level and set an appropriate view of blender area - self.test_success = self.create_level( - self.args["level"], - heightmap_resolution=1024, - heightmap_meters_per_pixel=1, - terrain_texture_resolution=4096, - use_terrain=False, - ) + import azlmbr.asset as asset + import azlmbr.areasystem as areasystem + import azlmbr.legacy.general as general + import azlmbr.paths as paths + import azlmbr.bus as bus + import azlmbr.components as components + import azlmbr.math as math + import azlmbr.entity as entity - general.set_current_view_position(500.49, 498.69, 46.66) - general.set_current_view_rotation(-42.05, 0.00, -36.33) + import editor_python_test_tools.hydra_editor_utils as hydra + from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg + from editor_python_test_tools.utils import Report + from editor_python_test_tools.utils import TestHelper as helper - # 2) Create 2 vegetation areas with different meshes - purple_position = math.Vector3(504.0, 512.0, 32.0) - purple_asset_path = os.path.join("Slices", "PurpleFlower.dynamicslice") - spawner_entity_1 = dynveg.create_vegetation_area("Purple Spawner", - purple_position, - 16.0, 16.0, 1.0, - purple_asset_path) + # 1) Create a new, temporary level + lvl_name = "tmp_level" + helper.init_idle() + level_created = general.create_level_no_prompt(lvl_name, 1024, 1, 4096, False) + general.idle_wait(1.0) + Report.critical_result(Tests.level_created, level_created == 0) - pink_position = math.Vector3(520.0, 512.0, 32.0) - pink_asset_path = os.path.join("Slices", "PinkFlower.dynamicslice") - spawner_entity_2 = dynveg.create_vegetation_area("Pink Spawner", - pink_position, - 16.0, 16.0, 1.0, - pink_asset_path) + general.set_current_view_position(500.49, 498.69, 46.66) + general.set_current_view_rotation(-42.05, 0.00, -36.33) - base_position = math.Vector3(512.0, 512.0, 32.0) - dynveg.create_surface_entity("Surface Entity", - base_position, - 16.0, 16.0, 1.0) + # 2) Create 2 vegetation areas with different meshes + purple_position = math.Vector3(504.0, 512.0, 32.0) + purple_asset_path = os.path.join("Slices", "PurpleFlower.dynamicslice") + spawner_entity_1 = dynveg.create_vegetation_area("Purple Spawner", + purple_position, + 16.0, 16.0, 1.0, + purple_asset_path) - hydra.add_level_component("Vegetation Debugger") + pink_position = math.Vector3(520.0, 512.0, 32.0) + pink_asset_path = os.path.join("Slices", "PinkFlower.dynamicslice") + spawner_entity_2 = dynveg.create_vegetation_area("Pink Spawner", + pink_position, + 16.0, 16.0, 1.0, + pink_asset_path) - # 3) Create Blender entity and pin the vegetation areas. We also add and attach a Lua script to validate in the - # launcher for the follow-up test - blender_entity = hydra.Entity("Blender") - blender_entity.create_entity( - base_position, - ["Box Shape", "Vegetation Layer Blender", "Lua Script"] - ) - if blender_entity.id.IsValid(): - print(f"'{blender_entity.name}' created") + base_position = math.Vector3(512.0, 512.0, 32.0) + dynveg.create_surface_entity("Surface Entity", + base_position, + 16.0, 16.0, 1.0) - blender_entity.get_set_test(0, "Box Shape|Box Configuration|Dimensions", math.Vector3(16.0, 16.0, 1.0)) - blender_entity.get_set_test(1, "Configuration|Vegetation Areas", [spawner_entity_1.id, spawner_entity_2.id]) - instance_counter_path = os.path.join("luascripts", "instance_counter_blender.lua") - instance_counter_script = asset.AssetCatalogRequestBus(bus.Broadcast, "GetAssetIdByPath", instance_counter_path, - math.Uuid(), False) - blender_entity.get_set_test(2, "Script properties|Asset", instance_counter_script) + hydra.add_level_component("Vegetation Debugger") - # 4) Verify instances in blender area are equally represented by both descriptors + # 3) Create Blender entity and pin the vegetation areas. We also add and attach a Lua script to validate in the + # launcher for the follow-up test + blender_entity = hydra.Entity("Blender") + blender_entity.create_entity( + base_position, + ["Box Shape", "Vegetation Layer Blender", "Lua Script"] + ) + Report.result(Tests.blender_entity_created, blender_entity.id.IsValid()) - # Wait for instances to spawn - general.run_console('veg_debugClearAllAreas') - num_expected = 20 * 20 - self.test_success = self.test_success and self.wait_for_condition( - lambda: dynveg.validate_instance_count(base_position, 8.0, num_expected), 5.0) + blender_entity.get_set_test(0, "Box Shape|Box Configuration|Dimensions", math.Vector3(16.0, 16.0, 1.0)) + blender_entity.get_set_test(1, "Configuration|Vegetation Areas", [spawner_entity_1.id, spawner_entity_2.id]) + instance_counter_path = os.path.join("luascripts", "instance_counter_blender.lua") + instance_counter_script = asset.AssetCatalogRequestBus(bus.Broadcast, "GetAssetIdByPath", instance_counter_path, + math.Uuid(), False) + blender_entity.get_set_test(2, "Script properties|Asset", instance_counter_script) - if self.test_success: - box = math.Aabb_CreateCenterRadius(base_position, 8.0) - instances = areasystem.AreaSystemRequestBus(bus.Broadcast, 'GetInstancesInAabb', box) - pink_count = 0 - purple_count = 0 - for instance in instances: - purple_asset_path = purple_asset_path.replace("\\", "/").lower() - pink_asset_path = pink_asset_path.replace("\\", "/").lower() - if instance.descriptor.spawner.GetSliceAssetPath() == pink_asset_path: - pink_count += 1 - elif instance.descriptor.spawner.GetSliceAssetPath() == purple_asset_path: - purple_count += 1 - self.test_success = pink_count == purple_count and (pink_count + purple_count == num_expected) and self.test_success + # 4) Verify instances in blender area are equally represented by both descriptors - # 5) Move the default Camera entity for testing in the launcher - cam_position = math.Vector3(500.0, 500.0, 47.0) - cam_rot_degrees_vector = math.Vector3(radians(-55.0), radians(28.5), radians(-17.0)) - search_filter = entity.SearchFilter() - search_filter.names = ["Camera"] - search_entity_ids = entity.SearchBus(bus.Broadcast, 'SearchEntities', search_filter) - components.TransformBus(bus.Event, "MoveEntity", search_entity_ids[0], cam_position) - azlmbr.components.TransformBus(bus.Event, "SetLocalRotation", search_entity_ids[0], cam_rot_degrees_vector) + # Wait for instances to spawn + general.run_console('veg_debugClearAllAreas') + num_expected = 20 * 20 + success = helper.wait_for_condition( + lambda: dynveg.validate_instance_count(base_position, 8.0, num_expected), 5.0) + Report.critical_result(Tests.instance_count, success) - # 6) Save and export level - general.save_level() - general.idle_wait(1.0) - general.export_to_engine() - general.idle_wait(1.0) + box = math.Aabb_CreateCenterRadius(base_position, 8.0) + instances = areasystem.AreaSystemRequestBus(bus.Broadcast, 'GetInstancesInAabb', box) + pink_count = 0 + purple_count = 0 + for instance in instances: + purple_asset_path = purple_asset_path.replace("\\", "/").lower() + pink_asset_path = pink_asset_path.replace("\\", "/").lower() + if instance.descriptor.spawner.GetSliceAssetPath() == pink_asset_path: + pink_count += 1 + elif instance.descriptor.spawner.GetSliceAssetPath() == purple_asset_path: + purple_count += 1 + Report.result(Tests.instances_blended, pink_count == purple_count and (pink_count + purple_count == num_expected)) + + # 5) Move the default Camera entity for testing in the launcher + cam_position = math.Vector3(500.0, 500.0, 47.0) + cam_rot_degrees_vector = math.Vector3(radians(-55.0), radians(28.5), radians(-17.0)) + search_filter = entity.SearchFilter() + search_filter.names = ["Camera"] + search_entity_ids = entity.SearchBus(bus.Broadcast, 'SearchEntities', search_filter) + components.TransformBus(bus.Event, "MoveEntity", search_entity_ids[0], cam_position) + azlmbr.components.TransformBus(bus.Event, "SetLocalRotation", search_entity_ids[0], cam_rot_degrees_vector) + + # 6) Save and export to engine + general.save_level() + general.export_to_engine() + pak_path = os.path.join(paths.devroot, "AutomatedTesting", "cache", "pc", "levels", lvl_name, "level.pak") + Report.result(Tests.saved_and_exported, os.path.exists(pak_path)) -test = TestVegLayerBlenderCreated() -test.run() +if __name__ == "__main__": + + from editor_python_test_tools.utils import Report + Report.start_test(LayerBlender_E2E_Editor) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/LayerBlocker_InstancesBlockedInConfiguredArea.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/LayerBlocker_InstancesBlockedInConfiguredArea.py index 138ac27700..625b7d2265 100755 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/LayerBlocker_InstancesBlockedInConfiguredArea.py +++ b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/LayerBlocker_InstancesBlockedInConfiguredArea.py @@ -5,102 +5,105 @@ For complete copyright and license terms please see the LICENSE at the root of t SPDX-License-Identifier: Apache-2.0 OR MIT """ -import os -import sys -import azlmbr.bus as bus -import azlmbr.editor as editor -import azlmbr.math as math -import azlmbr.legacy.general as general -import azlmbr.paths - -sys.path.append(os.path.join(azlmbr.paths.devroot, "AutomatedTesting", "Gem", "PythonTests")) -import editor_python_test_tools.hydra_editor_utils as hydra -from editor_python_test_tools.editor_test_helper import EditorTestHelper -from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg +class Tests: + initial_instance_count = ( + "Initial instance count is as expected", + "Unexpected number of initial instances found" + ) + blocked_instance_count = ( + "Expected number of instances found after configuring Blocker", + "Unexpected number of instances found after configuring Blocker" + ) -class TestLayerBlocker(EditorTestHelper): - def __init__(self): - EditorTestHelper.__init__(self, log_prefix="LayerBlocker_InstancesBlocked", args=["level"]) - def run_test(self): - """ - Summary: - An empty level is created. A Vegetation Layer Spawner area is configured. A Vegetation Layer Blocker area is - configured to block instances in the spawner area. +def LayerBlocker_InstancesBlockedInConfiguredArea(): + """ + Summary: + An empty level is created. A Vegetation Layer Spawner area is configured. A Vegetation Layer Blocker area is + configured to block instances in the spawner area. - Expected Behavior: - Vegetation is blocked by the configured Blocker area. + Expected Behavior: + Vegetation is blocked by the configured Blocker area. - Test Steps: - 1. A new level is created - 2. Vegetation Layer Spawner area is created - 3. Planting surface is created - 4. Vegetation System Settings level component is added, and Snap Mode set to center to ensure expected instance - counts are accurate in the configured vegetation area - 5. Initial instance counts pre-blocker are validated - 6. A Vegetation Layer Blocker area is created, overlapping the spawner area - 7. Post-blocker instance counts are validated + Test Steps: + 1. A simple level is opened + 2. Vegetation Layer Spawner area is created + 3. Planting surface is created + 4. Vegetation System Settings level component is added, and Snap Mode set to center to ensure expected instance + counts are accurate in the configured vegetation area + 5. Initial instance counts pre-blocker are validated + 6. A Vegetation Layer Blocker area is created, overlapping the spawner area + 7. Post-blocker instance counts are validated - Note: - - This test file must be called from the Open 3D Engine Editor command terminal - - Any passed and failed tests are written to the Editor.log file. - Parsing the file or running a log_monitor are required to observe the test results. + Note: + - This test file must be called from the Open 3D Engine Editor command terminal + - Any passed and failed tests are written to the Editor.log file. + Parsing the file or running a log_monitor are required to observe the test results. - :return: None - """ + :return: None + """ - # 1) Create a new, temporary level - self.test_success = self.create_level( - self.args["level"], - heightmap_resolution=1024, - heightmap_meters_per_pixel=1, - terrain_texture_resolution=4096, - use_terrain=False, - ) + import os - # Set view of planting area for visual debugging - general.set_current_view_position(512.0, 500.0, 38.0) - general.set_current_view_rotation(-20.0, 0.0, 0.0) + import azlmbr.bus as bus + import azlmbr.editor as editor + import azlmbr.math as math + import azlmbr.legacy.general as general - # 2) Create a new instance spawner entity - spawner_center_point = math.Vector3(512.0, 512.0, 32.0) - asset_path = os.path.join("Slices", "PinkFlower.dynamicslice") - spawner_entity = dynveg.create_vegetation_area("Instance Spawner", spawner_center_point, 16.0, 16.0, 16.0, - asset_path) + import editor_python_test_tools.hydra_editor_utils as hydra + from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg + from editor_python_test_tools.utils import Report + from editor_python_test_tools.utils import TestHelper as helper - # 3) Create surface for planting on - dynveg.create_surface_entity("Surface Entity", spawner_center_point, 32.0, 32.0, 1.0) + # 1) Open an existing simple level + helper.init_idle() + helper.open_level("Physics", "Base") - # 4) Add a Vegetation System Settings Level component and set Sector Point Snap Mode to Center - veg_system_settings_component = hydra.add_level_component("Vegetation System Settings") - editor.EditorComponentAPIBus(bus.Broadcast, "SetComponentProperty", veg_system_settings_component, - 'Configuration|Area System Settings|Sector Point Snap Mode', 1) + # Set view of planting area for visual debugging + general.set_current_view_position(512.0, 500.0, 38.0) + general.set_current_view_rotation(-20.0, 0.0, 0.0) - # 5) Verify initial instance counts - num_expected = 20 * 20 - success = self.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, - num_expected), 5.0) - self.test_success = success and self.test_success + # 2) Create a new instance spawner entity + spawner_center_point = math.Vector3(512.0, 512.0, 32.0) + asset_path = os.path.join("Slices", "PinkFlower.dynamicslice") + spawner_entity = dynveg.create_vegetation_area("Instance Spawner", spawner_center_point, 16.0, 16.0, 16.0, + asset_path) - # 6) Create a new Vegetation Layer Blocker area overlapping the spawner area - blocker_entity = hydra.Entity("Blocker Area") - blocker_entity.create_entity( - spawner_center_point, - ["Vegetation Layer Blocker", "Box Shape"] - ) - if blocker_entity.id.IsValid(): - print(f"'{blocker_entity.name}' created") - blocker_entity.get_set_test(1, "Box Shape|Box Configuration|Dimensions", - math.Vector3(3.0, 3.0, 3.0)) + # 3) Create surface for planting on + dynveg.create_surface_entity("Surface Entity", spawner_center_point, 32.0, 32.0, 1.0) - # 7) Validate instance counts post-blocker. 16 instances should now be blocked in the center of the spawner area - num_expected = (20 * 20) - 16 - success = self.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, - num_expected), 5.0) - self.test_success = success and self.test_success + # 4) Add a Vegetation System Settings Level component and set Sector Point Snap Mode to Center + veg_system_settings_component = hydra.add_level_component("Vegetation System Settings") + editor.EditorComponentAPIBus(bus.Broadcast, "SetComponentProperty", veg_system_settings_component, + 'Configuration|Area System Settings|Sector Point Snap Mode', 1) + + # 5) Verify initial instance counts + num_expected = 20 * 20 + success = helper.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, + num_expected), 5.0) + Report.result(Tests.initial_instance_count, success) + + # 6) Create a new Vegetation Layer Blocker area overlapping the spawner area + blocker_entity = hydra.Entity("Blocker Area") + blocker_entity.create_entity( + spawner_center_point, + ["Vegetation Layer Blocker", "Box Shape"] + ) + if blocker_entity.id.IsValid(): + print(f"'{blocker_entity.name}' created") + blocker_entity.get_set_test(1, "Box Shape|Box Configuration|Dimensions", + math.Vector3(3.0, 3.0, 3.0)) + + # 7) Validate instance counts post-blocker. 16 instances should now be blocked in the center of the spawner area + num_expected = (20 * 20) - 16 + success = helper.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, + num_expected), 5.0) + Report.result(Tests.blocked_instance_count, success) -test = TestLayerBlocker() -test.run() +if __name__ == "__main__": + + from editor_python_test_tools.utils import Report + Report.start_test(LayerBlocker_InstancesBlockedInConfiguredArea) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/LayerSpawner_FilterStageToggle.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/LayerSpawner_FilterStageToggle.py index a52f91ae5f..8592692c4b 100755 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/LayerSpawner_FilterStageToggle.py +++ b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/LayerSpawner_FilterStageToggle.py @@ -5,85 +5,83 @@ For complete copyright and license terms please see the LICENSE at the root of t SPDX-License-Identifier: Apache-2.0 OR MIT """ -import os -import sys -import azlmbr.bus as bus -import azlmbr.editor as editor -import azlmbr.legacy.general as general -import azlmbr.math as math -import azlmbr.paths - -sys.path.append(os.path.join(azlmbr.paths.devroot, "AutomatedTesting", "Gem", "PythonTests")) -import editor_python_test_tools.hydra_editor_utils as hydra -from editor_python_test_tools.editor_test_helper import EditorTestHelper -from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg +class Tests: + preprocess_instance_count = ( + "Preprocess filter stage vegetation instance count is as expected", + "Preprocess filter stage instance count found an unexpected number of instances" + ) + postprocess_instance_count = ( + "Postprocess filter stage vegetation instance count is as expected", + "Postprocess filter stage instance count found an unexpected number of instances" + ) -class TestLayerSpawnerFilterStageToggle(EditorTestHelper): - def __init__(self): - EditorTestHelper.__init__(self, log_prefix="LayerSpawner_FilterStageToggle", args=["level"]) +def LayerSpawner_FilterStageToggle(): + """ + Summary: + Filter Stage toggle affects final vegetation position. - def run_test(self): - """ - Summary: - C4765973 Filter Stage toggle affects final vegetation position. + Expected Result: + Vegetation instances plant differently depending on the Filter Stage setting. - Expected Result: - Vegetation instances plant differently depending on the Filter Stage setting. + :return: None + """ - :return: None - """ + import os - PREPROCESS_INSTANCE_COUNT = 21 - POSTPROCESS_INSTANCE_COUNT = 19 + import azlmbr.legacy.general as general + import azlmbr.math as math - # Create empty level - self.test_success = self.create_level( - self.args["level"], - heightmap_resolution=1024, - heightmap_meters_per_pixel=1, - terrain_texture_resolution=4096, - use_terrain=False, - ) + import editor_python_test_tools.hydra_editor_utils as hydra + from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg + from editor_python_test_tools.utils import Report + from editor_python_test_tools.utils import TestHelper as helper - general.set_current_view_position(500.49, 498.69, 46.66) - general.set_current_view_rotation(-42.05, 0.00, -36.33) + PREPROCESS_INSTANCE_COUNT = 21 + POSTPROCESS_INSTANCE_COUNT = 19 - # Create a vegetation area with all needed components - position = math.Vector3(512.0, 512.0, 32.0) - asset_path = os.path.join("Slices", "PinkFlower.dynamicslice") - vegetation_entity = dynveg.create_vegetation_area("vegetation", position, 16.0, 16.0, 16.0, asset_path) - vegetation_entity.add_component("Vegetation Altitude Filter") - vegetation_entity.add_component("Vegetation Position Modifier") + # Open an existing simple level + helper.init_idle() + helper.open_level("Physics", "Base") - # Create a child entity under vegetation area - child_entity = hydra.Entity("child_entity") - components_to_add = ["Random Noise Gradient", "Gradient Transform Modifier", "Box Shape"] - child_entity.create_entity(position, components_to_add, vegetation_entity.id) + general.set_current_view_position(500.49, 498.69, 46.66) + general.set_current_view_rotation(-42.05, 0.00, -36.33) - # Set the Gradient Id in X and Y direction - vegetation_entity.get_set_test(4, "Configuration|Position X|Gradient|Gradient Entity Id", child_entity.id) - vegetation_entity.get_set_test(4, "Configuration|Position Y|Gradient|Gradient Entity Id", child_entity.id) + # Create a vegetation area with all needed components + position = math.Vector3(512.0, 512.0, 32.0) + asset_path = os.path.join("Slices", "PinkFlower.dynamicslice") + vegetation_entity = dynveg.create_vegetation_area("vegetation", position, 16.0, 16.0, 16.0, asset_path) + vegetation_entity.add_component("Vegetation Altitude Filter") + vegetation_entity.add_component("Vegetation Position Modifier") - # Set the min and max values for Altitude Filter - vegetation_entity.get_set_test(3, "Configuration|Altitude Min", 34.0) - vegetation_entity.get_set_test(3, "Configuration|Altitude Max", 38.0) + # Create a child entity under vegetation area + child_entity = hydra.Entity("child_entity") + components_to_add = ["Random Noise Gradient", "Gradient Transform Modifier", "Box Shape"] + child_entity.create_entity(position, components_to_add, vegetation_entity.id) - # Add entity with Mesh to replicate creation of hills and a flat surface to plant on - dynveg.create_surface_entity("Flat Surface", position, 32.0, 32.0, 1.0) - hill_entity = dynveg.create_mesh_surface_entity_with_slopes("hill", position, 4.0) + # Set the Gradient Id in X and Y direction + vegetation_entity.get_set_test(4, "Configuration|Position X|Gradient|Gradient Entity Id", child_entity.id) + vegetation_entity.get_set_test(4, "Configuration|Position Y|Gradient|Gradient Entity Id", child_entity.id) - # Set the filter stage to preprocess and postprocess respectively and verify instance count - vegetation_entity.get_set_test(0, "Configuration|Filter Stage", 1) - self.wait_for_condition(lambda: dynveg.validate_instance_count(position, 16.0, PREPROCESS_INSTANCE_COUNT), 3.0) - result = dynveg.validate_instance_count(position, 16.0, PREPROCESS_INSTANCE_COUNT) - self.log(f"Preprocess filter stage vegetation instance count is as expected: {result}") - vegetation_entity.get_set_test(0, "Configuration|Filter Stage", 2) - self.wait_for_condition(lambda: dynveg.validate_instance_count(position, 16.0, POSTPROCESS_INSTANCE_COUNT), 3.0) - result = dynveg.validate_instance_count(position, 16.0, POSTPROCESS_INSTANCE_COUNT) - self.log(f"Postprocess filter vegetation instance stage count is as expected: {result}") + # Set the min and max values for Altitude Filter + vegetation_entity.get_set_test(3, "Configuration|Altitude Min", 34.0) + vegetation_entity.get_set_test(3, "Configuration|Altitude Max", 38.0) + + # Add entity with Mesh to replicate creation of hills and a flat surface to plant on + dynveg.create_surface_entity("Flat Surface", position, 32.0, 32.0, 1.0) + hill_entity = dynveg.create_mesh_surface_entity_with_slopes("hill", position, 4.0) + + # Set the filter stage to preprocess and postprocess respectively and verify instance count + vegetation_entity.get_set_test(0, "Configuration|Filter Stage", 1) + result = helper.wait_for_condition(lambda: dynveg.validate_instance_count(position, 16.0, PREPROCESS_INSTANCE_COUNT), 3.0) + Report.result(Tests.preprocess_instance_count, result) + vegetation_entity.get_set_test(0, "Configuration|Filter Stage", 2) + result = helper.wait_for_condition(lambda: dynveg.validate_instance_count(position, 16.0, POSTPROCESS_INSTANCE_COUNT), 3.0) + Report.result(Tests.postprocess_instance_count, result) -test = TestLayerSpawnerFilterStageToggle() -test.run() +if __name__ == "__main__": + + from editor_python_test_tools.utils import Report + Report.start_test(LayerSpawner_FilterStageToggle) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/LayerSpawner_InheritBehaviorFlag.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/LayerSpawner_InheritBehaviorFlag.py index 5e5c6410b0..649c7d0776 100755 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/LayerSpawner_InheritBehaviorFlag.py +++ b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/LayerSpawner_InheritBehaviorFlag.py @@ -5,118 +5,116 @@ For complete copyright and license terms please see the LICENSE at the root of t SPDX-License-Identifier: Apache-2.0 OR MIT """ -import os -import sys -import azlmbr.math as math -import azlmbr.legacy.general as general -import azlmbr.paths -import azlmbr.surface_data as surface_data -import azlmbr.vegetation as vegetation - -sys.path.append(os.path.join(azlmbr.paths.devroot, "AutomatedTesting", "Gem", "PythonTests")) -import editor_python_test_tools.hydra_editor_utils as hydra -from editor_python_test_tools.editor_test_helper import EditorTestHelper -from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg +class Tests: + inherit_behavior_checked = ( + "Found no instances with Inherit Behavior checked as expected", + "Unexpectedly found instances with Inherit Behavior checked" + ) + inherit_behavior_unchecked = ( + "Found instances with Inherit Behavior unchecked as expected", + "Unexpectedly found no instances with Inherit Behavior unchecked" + ) -class TestLayerSpawnerInheritBehavior(EditorTestHelper): - def __init__(self): - EditorTestHelper.__init__(self, log_prefix="LayerSpawner_InheritBehavior", args=["level"]) +def LayerSpawner_InheritBehaviorFlag(): + """ + Summary: + Verifies if Inherit Behavior Flag works as expected. - def run_test(self): - """ - Summary: - C4762381 Verifies if Inherit Behavior Flag works as expected. + Expected Result: + The spawner with Inherit Behavior toggled off no longer obeys + Vegetation Surface Mask Filter of the Vegetation Layer Blender entity and plants on the surface. - Expected Result: - The spawner with Inherit Behavior toggled off no longer obeys - Vegetation Surface Mask Filter of the Vegetation Layer Blender entity and plants on the surface. + :return: None + """ + import os - :return: None - """ + import azlmbr.math as math + import azlmbr.legacy.general as general + import azlmbr.surface_data as surface_data + import azlmbr.vegetation as vegetation - SURFACE_TAG = "test_tag" + import editor_python_test_tools.hydra_editor_utils as hydra + from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg + from editor_python_test_tools.utils import Report + from editor_python_test_tools.utils import TestHelper as helper - def set_dynamic_slice_asset(entity_obj, component_index, dynamic_slice_asset_path): - dynamic_slice_spawner = vegetation.DynamicSliceInstanceSpawner() - dynamic_slice_spawner.SetSliceAssetPath(dynamic_slice_asset_path) - descriptor = hydra.get_component_property_value( - entity_obj.components[component_index], "Configuration|Embedded Assets|[0]" - ) - descriptor.spawner = dynamic_slice_spawner - entity_obj.get_set_test(2, "Configuration|Embedded Assets|[0]", descriptor) + SURFACE_TAG = "test_tag" - # Create empty level - self.test_success = self.create_level( - self.args["level"], - heightmap_resolution=1024, - heightmap_meters_per_pixel=1, - terrain_texture_resolution=4096, - use_terrain=False, + def set_dynamic_slice_asset(entity_obj, component_index, dynamic_slice_asset_path): + dynamic_slice_spawner = vegetation.DynamicSliceInstanceSpawner() + dynamic_slice_spawner.SetSliceAssetPath(dynamic_slice_asset_path) + descriptor = hydra.get_component_property_value( + entity_obj.components[component_index], "Configuration|Embedded Assets|[0]" ) + descriptor.spawner = dynamic_slice_spawner + entity_obj.get_set_test(2, "Configuration|Embedded Assets|[0]", descriptor) - general.set_current_view_position(512.0, 480.0, 38.0) + # Open an existing simple level + helper.init_idle() + helper.open_level("Physics", "Base") - # Create Emitter entity and add the required components - position = math.Vector3(512.0, 512.0, 32.0) - emitter_entity = dynveg.create_surface_entity("emitter_entity", position, 16.0, 16.0, 1.0) + general.set_current_view_position(512.0, 480.0, 38.0) - # Add surface tag to the Surface Tag Emitter - tag = surface_data.SurfaceTag() - tag.SetTag(SURFACE_TAG) - pte = hydra.get_property_tree(emitter_entity.components[1]) - path = "Configuration|Generated Tags" - pte.add_container_item(path, 0, tag) - emitter_entity.get_set_test(1, "Configuration|Generated Tags|[0]", tag) + # Create Emitter entity and add the required components + position = math.Vector3(512.0, 512.0, 32.0) + emitter_entity = dynveg.create_surface_entity("emitter_entity", position, 16.0, 16.0, 1.0) - # Create Blender entity and add required components - components_to_add = ["Box Shape", "Vegetation Layer Blender"] - blender_entity = hydra.Entity("blender_entity") - blender_entity.create_entity(position, components_to_add) - blender_entity.get_set_test(0, "Box Shape|Box Configuration|Dimensions", math.Vector3(16.0, 16.0, 1.0)) + # Add surface tag to the Surface Tag Emitter + tag = surface_data.SurfaceTag() + tag.SetTag(SURFACE_TAG) + pte = hydra.get_property_tree(emitter_entity.components[1]) + path = "Configuration|Generated Tags" + pte.add_container_item(path, 0, tag) + emitter_entity.get_set_test(1, "Configuration|Generated Tags|[0]", tag) - # Create Vegetation area and assign a valid asset - veg_1 = hydra.Entity("veg_1") - veg_1.create_entity( - position, ["Vegetation Layer Spawner", "Vegetation Reference Shape", "Vegetation Asset List"] - ) - set_dynamic_slice_asset(veg_1, 2, os.path.join("Slices", "PinkFlower.dynamicslice")) - veg_1.get_set_test(1, "Configuration|Shape Entity Id", blender_entity.id) + # Create Blender entity and add required components + components_to_add = ["Box Shape", "Vegetation Layer Blender"] + blender_entity = hydra.Entity("blender_entity") + blender_entity.create_entity(position, components_to_add) + blender_entity.get_set_test(0, "Box Shape|Box Configuration|Dimensions", math.Vector3(16.0, 16.0, 1.0)) - # Create second vegetation area and assign a valid asset - veg_2 = hydra.Entity("veg_2") - veg_2.create_entity( - position, ["Vegetation Layer Spawner", "Vegetation Reference Shape", "Vegetation Asset List"] - ) - set_dynamic_slice_asset(veg_2, 2, os.path.join("Slices", "PurpleFlower.dynamicslice")) - veg_2.get_set_test(1, "Configuration|Shape Entity Id", blender_entity.id) + # Create Vegetation area and assign a valid asset + veg_1 = hydra.Entity("veg_1") + veg_1.create_entity( + position, ["Vegetation Layer Spawner", "Vegetation Reference Shape", "Vegetation Asset List"] + ) + set_dynamic_slice_asset(veg_1, 2, os.path.join("Slices", "PinkFlower.dynamicslice")) + veg_1.get_set_test(1, "Configuration|Shape Entity Id", blender_entity.id) - # Assign the vegetation areas to the Blender entity - pte = hydra.get_property_tree(blender_entity.components[1]) - path = "Configuration|Vegetation Areas" - pte.update_container_item(path, 0, veg_1.id) - pte.add_container_item(path, 1, veg_2.id) + # Create second vegetation area and assign a valid asset + veg_2 = hydra.Entity("veg_2") + veg_2.create_entity( + position, ["Vegetation Layer Spawner", "Vegetation Reference Shape", "Vegetation Asset List"] + ) + set_dynamic_slice_asset(veg_2, 2, os.path.join("Slices", "PurpleFlower.dynamicslice")) + veg_2.get_set_test(1, "Configuration|Shape Entity Id", blender_entity.id) - # Add Vegetation Surface Mask Filter to the blender entity and add a Exclusion tag - tag = surface_data.SurfaceTag() - tag.SetTag(SURFACE_TAG) - blender_entity.add_component("Vegetation Surface Mask Filter") - pte = hydra.get_property_tree(blender_entity.components[2]) - path = "Configuration|Exclusion|Surface Tags" - pte.add_container_item(path, 0, tag) - blender_entity.get_set_test(2, "Configuration|Exclusion|Surface Tags|[0]", tag) + # Assign the vegetation areas to the Blender entity + pte = hydra.get_property_tree(blender_entity.components[1]) + path = "Configuration|Vegetation Areas" + pte.update_container_item(path, 0, veg_1.id) + pte.add_container_item(path, 1, veg_2.id) - # Toggle Inherit Behavior flag and verify vegetation instances - self.log( - f"Vegetation is not planted when Inherit Behavior flag is checked: {dynveg.validate_instance_count(position, 16.0, 0)}" - ) - veg_1.get_set_test(0, "Configuration|Inherit Behavior", False) - self.wait_for_condition(lambda: dynveg.validate_instance_count(position, 16.0, 400), 2.0) - self.log( - f"Vegetation plant when Inherit Behavior flag is unchecked: {dynveg.validate_instance_count(position, 16.0, 400)}" - ) + # Add Vegetation Surface Mask Filter to the blender entity and add a Exclusion tag + tag = surface_data.SurfaceTag() + tag.SetTag(SURFACE_TAG) + blender_entity.add_component("Vegetation Surface Mask Filter") + pte = hydra.get_property_tree(blender_entity.components[2]) + path = "Configuration|Exclusion|Surface Tags" + pte.add_container_item(path, 0, tag) + blender_entity.get_set_test(2, "Configuration|Exclusion|Surface Tags|[0]", tag) + + # Toggle Inherit Behavior flag and verify vegetation instances + flag_checked_instance_count = helper.wait_for_condition(lambda: dynveg.validate_instance_count(position, 16.0, 0), 2.0) + Report.result(Tests.inherit_behavior_checked, flag_checked_instance_count) + veg_1.get_set_test(0, "Configuration|Inherit Behavior", False) + flag_unchecked_instance_count = helper.wait_for_condition(lambda: dynveg.validate_instance_count(position, 16.0, 400), 2.0) + Report.result(Tests.inherit_behavior_unchecked, flag_unchecked_instance_count) -test = TestLayerSpawnerInheritBehavior() -test.run() +if __name__ == "__main__": + + from editor_python_test_tools.utils import Report + Report.start_test(LayerSpawner_InheritBehaviorFlag) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/LayerSpawner_InstancesPlantInAllSupportedShapes.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/LayerSpawner_InstancesPlantInAllSupportedShapes.py index 8310583fe6..0da200d87a 100755 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/LayerSpawner_InstancesPlantInAllSupportedShapes.py +++ b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/LayerSpawner_InstancesPlantInAllSupportedShapes.py @@ -5,134 +5,128 @@ For complete copyright and license terms please see the LICENSE at the root of t SPDX-License-Identifier: Apache-2.0 OR MIT """ -import os -import sys -sys.path.append(os.path.dirname(os.path.abspath(__file__))) -import azlmbr -import azlmbr.legacy.general as general -import azlmbr.entity as EntityId -import azlmbr.math as math +def LayerSpawner_InstancesPlantInAllSupportedShapes(): + """ + Summary: + The level is loaded and vegetation area is created. Then the Vegetation Reference Shape + component of vegetation area is pinned with entities of different shape components to check + if the vegetation plants in different shaped areas. -sys.path.append(os.path.join(azlmbr.paths.devroot, 'AutomatedTesting', 'Gem', 'PythonTests')) -import editor_python_test_tools.hydra_editor_utils as hydra -from editor_python_test_tools.editor_test_helper import EditorTestHelper -from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg + Expected Behavior: + Vegetation properly plants in areas of any shape. + Test Steps: + 1) Open a level + 2) Create basic vegetation area entity and set the properties + 3) Box Shape Entity: create, set properties and pin to vegetation + 4) Capsule Shape Entity: create, set properties and pin to vegetation + 5) Tube Shape Entity: create, set properties and pin to vegetation + 6) Sphere Shape Entity: create, set properties and pin to vegetation + 7) Cylinder Shape Entity: create, set properties and pin to vegetation + 8) Prism Shape Entity: create, set properties and pin to vegetation + 9) Compound Shape Entity: create, set properties and pin to vegetation -class TestLayerSpawner_AllShapesPlant(EditorTestHelper): - def __init__(self): - EditorTestHelper.__init__(self, log_prefix="TestLayerSpawner_AllShapesPlant", args=["level"]) + Note: + - Any passed and failed tests are written to the Editor.log file. + Parsing the file or running a log_monitor are required to observe the test results. - def run_test(self): - """ - Summary: - The level is loaded and vegetation area is created. Then the Vegetation Reference Shape - component of vegetation area is pinned with entities of different shape components to check - if the vegetation plants in different shaped areas. + :return: None + """ - Expected Behavior: - Vegetation properly plants in areas of any shape. + import os - Test Steps: - 1) Create level - 2) Create basic vegetation area entity and set the properties - 3) Box Shape Entity: create, set properties and pin to vegetation - 4) Capsule Shape Entity: create, set properties and pin to vegetation - 5) Tube Shape Entity: create, set properties and pin to vegetation - 6) Sphere Shape Entity: create, set properties and pin to vegetation - 7) Cylinder Shape Entity: create, set properties and pin to vegetation - 8) Prism Shape Entity: create, set properties and pin to vegetation - 9) Compound Shape Entity: create, set properties and pin to vegetation + import azlmbr.legacy.general as general + import azlmbr.entity as EntityId + import azlmbr.math as math - Note: - - Any passed and failed tests are written to the Editor.log file. - Parsing the file or running a log_monitor are required to observe the test results. + import editor_python_test_tools.hydra_editor_utils as hydra + from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg + from editor_python_test_tools.utils import Report + from editor_python_test_tools.utils import TestHelper as helper - :return: None - """ - - def pin_shape_and_check_count(entity_id, count): - hydra.get_set_test(vegetation, 2, "Configuration|Shape Entity Id", entity_id) - result = self.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(vegetation.id, - count), 2.0) - self.test_success = self.test_success and result - - # 1) Create level - self.test_success = self.create_level( - self.args["level"], - heightmap_resolution=1024, - heightmap_meters_per_pixel=1, - terrain_texture_resolution=4096, - use_terrain=False, + def pin_shape_and_check_count(entity, count): + hydra.get_set_test(vegetation, 2, "Configuration|Shape Entity Id", entity.id) + result = helper.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(vegetation.id, + count), 2.0) + success = ( + f"Found the expected number of instances in {entity.name} shape", + f"Unexpected number of instances found in {entity.name} shape" ) + Report.result(success, result) - # 2) Create basic vegetation area entity and set the properties - entity_position = math.Vector3(125.0, 136.0, 32.0) - asset_path = os.path.join("Slices", "PurpleFlower.dynamicslice") - vegetation = dynveg.create_vegetation_area("Instance Spawner", - entity_position, - 10.0, 10.0, 10.0, - asset_path) - vegetation.remove_component("Box Shape") - vegetation.add_component("Vegetation Reference Shape") + # 1) Open an existing simple level + helper.init_idle() + helper.open_level("Physics", "Base") - # Create surface for planting on - dynveg.create_surface_entity("Surface Entity", entity_position, 60.0, 60.0, 1.0) + # 2) Create basic vegetation area entity and set the properties + entity_position = math.Vector3(125.0, 136.0, 32.0) + asset_path = os.path.join("Slices", "PurpleFlower.dynamicslice") + vegetation = dynveg.create_vegetation_area("Instance Spawner", + entity_position, + 10.0, 10.0, 10.0, + asset_path) + vegetation.remove_component("Box Shape") + vegetation.add_component("Vegetation Reference Shape") - # Adjust camera to be close to the vegetation entity - general.set_current_view_position(135.0, 102.0, 39.0) - general.set_current_view_rotation(-15.0, 0, 0) + # Create surface for planting on + dynveg.create_surface_entity("Surface Entity", entity_position, 60.0, 60.0, 1.0) - # 3) Box Shape Entity: create, set properties and pin to vegetation - box = hydra.Entity("box") - box.create_entity(math.Vector3(124.0, 126.0, 32.0), ["Box Shape"]) - new_box_dimension = math.Vector3(10.0, 10.0, 1.0) - hydra.get_set_test(box, 0, "Box Shape|Box Configuration|Dimensions", new_box_dimension) - # This and subsequent counts are the number of "PurpleFlower" that spawn in the shape with given dimensions - pin_shape_and_check_count(box.id, 156) + # Adjust camera to be close to the vegetation entity + general.set_current_view_position(135.0, 102.0, 39.0) + general.set_current_view_rotation(-15.0, 0, 0) - # 4) Capsule Shape Entity: create, set properties and pin to vegetation - capsule = hydra.Entity("capsule") - capsule.create_entity(math.Vector3(120.0, 142.0, 32.0), ["Capsule Shape"]) - hydra.get_set_test(capsule, 0, "Capsule Shape|Capsule Configuration|Height", 10.0) - hydra.get_set_test(capsule, 0, "Capsule Shape|Capsule Configuration|Radius", 2.0) - pin_shape_and_check_count(capsule.id, 20) + # 3) Box Shape Entity: create, set properties and pin to vegetation + box = hydra.Entity("Box") + box.create_entity(math.Vector3(124.0, 126.0, 32.0), ["Box Shape"]) + new_box_dimension = math.Vector3(10.0, 10.0, 1.0) + hydra.get_set_test(box, 0, "Box Shape|Box Configuration|Dimensions", new_box_dimension) + # This and subsequent counts are the number of "PurpleFlower" that spawn in the shape with given dimensions + pin_shape_and_check_count(box, 156) - # 5) Tube Shape Entity: create, set properties and pin to vegetation - tube = hydra.Entity("tube") - tube.create_entity(math.Vector3(124.0, 136.0, 32.0), ["Tube Shape", "Spline"]) - pin_shape_and_check_count(tube.id, 27) + # 4) Capsule Shape Entity: create, set properties and pin to vegetation + capsule = hydra.Entity("Capsule") + capsule.create_entity(math.Vector3(120.0, 142.0, 32.0), ["Capsule Shape"]) + hydra.get_set_test(capsule, 0, "Capsule Shape|Capsule Configuration|Height", 10.0) + hydra.get_set_test(capsule, 0, "Capsule Shape|Capsule Configuration|Radius", 2.0) + pin_shape_and_check_count(capsule, 20) - # 6) Sphere Shape Entity: create, set properties and pin to vegetation - sphere = hydra.Entity("sphere") - sphere.create_entity(math.Vector3(112.0, 143.0, 32.0), ["Sphere Shape"]) - hydra.get_set_test(sphere, 0, "Sphere Shape|Sphere Configuration|Radius", 5.0) - pin_shape_and_check_count(sphere.id, 122) + # 5) Tube Shape Entity: create, set properties and pin to vegetation + tube = hydra.Entity("Tube") + tube.create_entity(math.Vector3(124.0, 136.0, 32.0), ["Tube Shape", "Spline"]) + pin_shape_and_check_count(tube, 27) - # 7) Cylinder Shape Entity: create, set properties and pin to vegetation - cylinder = hydra.Entity("cylinder") - cylinder.create_entity(math.Vector3(136.0, 143.0, 32.0), ["Cylinder Shape"]) - hydra.get_set_test(cylinder, 0, "Cylinder Shape|Cylinder Configuration|Radius", 5.0) - hydra.get_set_test(cylinder, 0, "Cylinder Shape|Cylinder Configuration|Height", 5.0) - pin_shape_and_check_count(cylinder.id, 124) + # 6) Sphere Shape Entity: create, set properties and pin to vegetation + sphere = hydra.Entity("Sphere") + sphere.create_entity(math.Vector3(112.0, 143.0, 32.0), ["Sphere Shape"]) + hydra.get_set_test(sphere, 0, "Sphere Shape|Sphere Configuration|Radius", 5.0) + pin_shape_and_check_count(sphere, 122) - # 8) Prism Shape Entity: create, set properties and pin to vegetation - polygon_prism = hydra.Entity("polygonprism") - polygon_prism.create_entity(math.Vector3(127.0, 142.0, 32.0), ["Polygon Prism Shape"]) - pin_shape_and_check_count(polygon_prism.id, 20) + # 7) Cylinder Shape Entity: create, set properties and pin to vegetation + cylinder = hydra.Entity("Cylinder") + cylinder.create_entity(math.Vector3(136.0, 143.0, 32.0), ["Cylinder Shape"]) + hydra.get_set_test(cylinder, 0, "Cylinder Shape|Cylinder Configuration|Radius", 5.0) + hydra.get_set_test(cylinder, 0, "Cylinder Shape|Cylinder Configuration|Height", 5.0) + pin_shape_and_check_count(cylinder, 124) - # 9) Compound Shape Entity: create, set properties and pin to vegetation - compound = hydra.Entity("Compound") - compound.create_entity(math.Vector3(125.0, 136.0, 32.0), ["Compound Shape"]) - pte = hydra.get_property_tree(compound.components[0]) - shapes = [box.id, capsule.id, tube.id, sphere.id, cylinder.id, polygon_prism.id] - for index in range(6): - pte.add_container_item("Configuration|Child Shape Entities", index, EntityId.EntityId()) - for index, element in enumerate(shapes): - hydra.get_set_test(compound, 0, f"Configuration|Child Shape Entities|[{index}]", element) - pin_shape_and_check_count(compound.id, 469) + # 8) Prism Shape Entity: create, set properties and pin to vegetation + polygon_prism = hydra.Entity("Polygon Prism") + polygon_prism.create_entity(math.Vector3(127.0, 142.0, 32.0), ["Polygon Prism Shape"]) + pin_shape_and_check_count(polygon_prism, 20) + + # 9) Compound Shape Entity: create, set properties and pin to vegetation + compound = hydra.Entity("Compound") + compound.create_entity(math.Vector3(125.0, 136.0, 32.0), ["Compound Shape"]) + pte = hydra.get_property_tree(compound.components[0]) + shapes = [box.id, capsule.id, tube.id, sphere.id, cylinder.id, polygon_prism.id] + for index in range(6): + pte.add_container_item("Configuration|Child Shape Entities", index, EntityId.EntityId()) + for index, element in enumerate(shapes): + hydra.get_set_test(compound, 0, f"Configuration|Child Shape Entities|[{index}]", element) + pin_shape_and_check_count(compound, 469) -test = TestLayerSpawner_AllShapesPlant() -test.run() +if __name__ == "__main__": + + from editor_python_test_tools.utils import Report + Report.start_test(LayerSpawner_InstancesPlantInAllSupportedShapes) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/LayerSpawner_InstancesRefreshUsingCorrectViewportCamera.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/LayerSpawner_InstancesRefreshUsingCorrectViewportCamera.py index f17956e066..02d30fb0f3 100755 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/LayerSpawner_InstancesRefreshUsingCorrectViewportCamera.py +++ b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/LayerSpawner_InstancesRefreshUsingCorrectViewportCamera.py @@ -5,115 +5,131 @@ For complete copyright and license terms please see the LICENSE at the root of t SPDX-License-Identifier: Apache-2.0 OR MIT """ -import os -import sys -sys.path.append(os.path.dirname(os.path.abspath(__file__))) -import azlmbr -import azlmbr.legacy.general as general -import azlmbr.math as math - -sys.path.append(os.path.join(azlmbr.paths.devroot, 'AutomatedTesting', 'Gem', 'PythonTests')) -from editor_python_test_tools.editor_test_helper import EditorTestHelper -from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg +class Tests: + viewport_config_updated = ( + "Viewport is now configured for test", + "Failed to configure viewport for test" + ) + first_viewport_active_instance_count = ( + "Expected number of instances found in left viewport", + "Unexpected number of instances found in left viewport" + ) + second_viewport_inactive_instance_count = ( + "No instances found in right viewport", + "Unexpectedly found instances in right viewport while not active" + ) + first_viewport_inactive_instance_count = ( + "No instances found in left viewport", + "Unexpectedly found instances in left viewport while not active" + ) + second_viewport_active_instance_count = ( + "Expected number of instances found in right viewport", + "Unexpected number of instances found in right viewport" + ) -class TestLayerSpawnerInstanceCameraRefresh(EditorTestHelper): - def __init__(self): - EditorTestHelper.__init__(self, log_prefix="LayerSpawner_InstanceCameraRefresh", args=["level"]) +def LayerSpawner_InstancesRefreshUsingCorrectViewportCamera(): + """ + Summary: + Test that the Dynamic Vegetation System is using the current Editor viewport camera as the center + of the spawn area for vegetation. To verify this, we create two separate Editor viewports pointed + at two different vegetation areas, and verify that as we switch between active viewports, only the + area directly underneath that viewport's camera has vegetation. + """ - def run_test(self): - """ - Summary: - Test that the Dynamic Vegetation System is using the current Editor viewport camera as the center - of the spawn area for vegetation. To verify this, we create two separate Editor viewports pointed - at two different vegetation areas, and verify that as we switch between active viewports, only the - area directly underneath that viewport's camera has vegetation. - """ - # Create an empty level - self.test_success = self.create_level( - self.args["level"], - heightmap_resolution=128, - heightmap_meters_per_pixel=1, - terrain_texture_resolution=4096, - use_terrain=False, - ) - # Set up a test environment to validate that switching viewports correctly changes which camera - # the vegetation system uses. - # The test environment consists of the following: - # - two 32 x 32 x 1 box shapes located far apart that emit a surface with no tags - # - two 32 x 32 x 32 vegetation areas that place vegetation on the boxes + import os - # Initialize some constants for our test. - # The boxes are intentionally shifted by 0.5 meters to ensure that we get a predictable number - # of vegetation points. By default, vegetation plants on grid corners, so if our boxes are aligned - # with grid corner points, the right/bottom edges will include more points than we might intuitively expect. - # By shifting by 0.5 meters, the vegetation grid points don't fall on the box edges, making the total count - # more predictable. - first_entity_center_point = math.Vector3(0.5, 0.5, 100.0) - # The second box needs to be far enough away from the first that the vegetation system will never spawn instances - # in both at the same time. - second_entity_center_point = math.Vector3(1024.5, 1024.5, 100.0) - box_size = 32.0 - surface_height = 1.0 - # By default, vegetation spawns 20 instances per 16 meters, so for our box of 32 meters, we should have - # ((20 instances / 16 m) * 32 m) ^ 2 instances. - filled_vegetation_area_instance_count = (20 * 2) * (20 * 2) + import azlmbr.legacy.general as general + import azlmbr.math as math - # Change the Editor view to contain two viewports - general.set_view_pane_layout(1) - get_view_pane_layout_success = self.wait_for_condition(lambda: (general.get_view_pane_layout() == 1), 2) - get_viewport_count_success = self.wait_for_condition(lambda: (general.get_viewport_count() == 2), 2) - self.test_success = get_view_pane_layout_success and self.test_success - self.test_success = get_viewport_count_success and self.test_success + from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg + from editor_python_test_tools.utils import Report + from editor_python_test_tools.utils import TestHelper as helper - # Set the view in the first viewport to point down at the first box - general.set_active_viewport(0) - self.wait_for_condition(lambda: general.get_active_viewport() == 0, 2) - general.set_current_view_position(first_entity_center_point.x, first_entity_center_point.y, - first_entity_center_point.z + 30.0) - general.set_current_view_rotation(-85.0, 0.0, 0.0) + # Open an existing simple level + helper.init_idle() + helper.open_level("Physics", "Base") - # Set the view in the second viewport to point down at the second box - general.set_active_viewport(1) - self.wait_for_condition(lambda: general.get_active_viewport() == 1, 2) - general.set_current_view_position(second_entity_center_point.x, second_entity_center_point.y, - second_entity_center_point.z + 30.0) - general.set_current_view_rotation(-85.0, 0.0, 0.0) + # Set up a test environment to validate that switching viewports correctly changes which camera + # the vegetation system uses. + # The test environment consists of the following: + # - two 32 x 32 x 1 box shapes located far apart that emit a surface with no tags + # - two 32 x 32 x 32 vegetation areas that place vegetation on the boxes - # Create the "flat surface" entities to use as our vegetation surfaces - first_surface_entity = dynveg.create_surface_entity("Surface 1", first_entity_center_point, box_size, box_size, - surface_height) - second_surface_entity = dynveg.create_surface_entity("Surface 2", second_entity_center_point, box_size, box_size, - surface_height) + # Initialize some constants for our test. + # The boxes are intentionally shifted by 0.5 meters to ensure that we get a predictable number + # of vegetation points. By default, vegetation plants on grid corners, so if our boxes are aligned + # with grid corner points, the right/bottom edges will include more points than we might intuitively expect. + # By shifting by 0.5 meters, the vegetation grid points don't fall on the box edges, making the total count + # more predictable. + first_entity_center_point = math.Vector3(0.5, 0.5, 100.0) + # The second box needs to be far enough away from the first that the vegetation system will never spawn instances + # in both at the same time. + second_entity_center_point = math.Vector3(1024.5, 1024.5, 100.0) + box_size = 32.0 + surface_height = 1.0 + # By default, vegetation spawns 20 instances per 16 meters, so for our box of 32 meters, we should have + # ((20 instances / 16 m) * 32 m) ^ 2 instances. + filled_vegetation_area_instance_count = (20 * 2) * (20 * 2) - # Create the two vegetation areas - test_slice_asset_path = os.path.join("Slices", "PurpleFlower.dynamicslice") - first_veg_entity = dynveg.create_vegetation_area("Veg Area 1", first_entity_center_point, box_size, box_size, - box_size, test_slice_asset_path) - second_veg_entity = dynveg.create_vegetation_area("Veg Area 2", second_entity_center_point, box_size, box_size, - box_size, test_slice_asset_path) + # Change the Editor view to contain two viewports + general.set_view_pane_layout(1) + get_view_pane_layout_success = helper.wait_for_condition(lambda: (general.get_view_pane_layout() == 1), 2) + get_viewport_count_success = helper.wait_for_condition(lambda: (general.get_viewport_count() == 2), 2) + Report.critical_result(Tests.viewport_config_updated, get_view_pane_layout_success and get_viewport_count_success) - # When the first viewport is active, the first area should be full of instances, and the second should be empty - general.set_active_viewport(0) - viewport_0_success = self.wait_for_condition(lambda: dynveg.validate_instance_count(first_entity_center_point, - box_size / 2.0, - filled_vegetation_area_instance_count), 5) - self.test_success = viewport_0_success and self.test_success - viewport_1_success = self.wait_for_condition(lambda: dynveg.validate_instance_count(second_entity_center_point, - box_size / 2.0, 0), 5) - self.test_success = viewport_1_success and self.test_success + # Set the view in the first viewport to point down at the first box + general.set_active_viewport(0) + helper.wait_for_condition(lambda: general.get_active_viewport() == 0, 2) + general.set_current_view_position(first_entity_center_point.x, first_entity_center_point.y, + first_entity_center_point.z + 30.0) + general.set_current_view_rotation(-85.0, 0.0, 0.0) - # When the second viewport is active, the second area should be full of instances, and the first should be empty - general.set_active_viewport(1) - viewport_0_success = self.wait_for_condition(lambda: dynveg.validate_instance_count(first_entity_center_point, - box_size / 2.0, 0), 5) - self.test_success = viewport_0_success and self.test_success - viewport_1_success = self.wait_for_condition(lambda: dynveg.validate_instance_count(second_entity_center_point, - box_size / 2.0, - filled_vegetation_area_instance_count), 5) - self.test_success = viewport_1_success and self.test_success + # Set the view in the second viewport to point down at the second box + general.set_active_viewport(1) + helper.wait_for_condition(lambda: general.get_active_viewport() == 1, 2) + general.set_current_view_position(second_entity_center_point.x, second_entity_center_point.y, + second_entity_center_point.z + 30.0) + general.set_current_view_rotation(-85.0, 0.0, 0.0) + + # Create the "flat surface" entities to use as our vegetation surfaces + first_surface_entity = dynveg.create_surface_entity("Surface 1", first_entity_center_point, box_size, box_size, + surface_height) + second_surface_entity = dynveg.create_surface_entity("Surface 2", second_entity_center_point, box_size, box_size, + surface_height) + + # Create the two vegetation areas + test_slice_asset_path = os.path.join("Slices", "PurpleFlower.dynamicslice") + first_veg_entity = dynveg.create_vegetation_area("Veg Area 1", first_entity_center_point, box_size, box_size, + box_size, test_slice_asset_path) + second_veg_entity = dynveg.create_vegetation_area("Veg Area 2", second_entity_center_point, box_size, box_size, + box_size, test_slice_asset_path) + + # When the first viewport is active, the first area should be full of instances, and the second should be empty + general.set_active_viewport(0) + helper.wait_for_condition(lambda: general.get_active_viewport() == 0, 2) + viewport_0_success = helper.wait_for_condition(lambda: dynveg.validate_instance_count(first_entity_center_point, + box_size / 2.0, + filled_vegetation_area_instance_count), 5) + viewport_1_success = helper.wait_for_condition(lambda: dynveg.validate_instance_count(second_entity_center_point, + box_size / 2.0, 0), 5) + Report.result(Tests.first_viewport_active_instance_count, viewport_0_success) + Report.result(Tests.second_viewport_inactive_instance_count, viewport_1_success) + + # When the second viewport is active, the second area should be full of instances, and the first should be empty + general.set_active_viewport(1) + helper.wait_for_condition(lambda: general.get_active_viewport() == 1, 2) + viewport_0_success = helper.wait_for_condition(lambda: dynveg.validate_instance_count(first_entity_center_point, + box_size / 2.0, 0), 5) + Report.result(Tests.first_viewport_inactive_instance_count, viewport_0_success) + viewport_1_success = helper.wait_for_condition(lambda: dynveg.validate_instance_count(second_entity_center_point, + box_size / 2.0, + filled_vegetation_area_instance_count), 5) + Report.result(Tests.second_viewport_active_instance_count, viewport_1_success) -test = TestLayerSpawnerInstanceCameraRefresh() -test.run() +if __name__ == "__main__": + + from editor_python_test_tools.utils import Report + Report.start_test(LayerSpawner_InstancesRefreshUsingCorrectViewportCamera) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/MeshBlocker_InstancesBlockedByMesh.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/MeshBlocker_InstancesBlockedByMesh.py index 47e9c857c2..415673c215 100755 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/MeshBlocker_InstancesBlockedByMesh.py +++ b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/MeshBlocker_InstancesBlockedByMesh.py @@ -5,92 +5,90 @@ For complete copyright and license terms please see the LICENSE at the root of t SPDX-License-Identifier: Apache-2.0 OR MIT """ -import os, sys -sys.path.append(os.path.dirname(os.path.abspath(__file__))) -import azlmbr.asset as asset -import azlmbr.bus as bus -import azlmbr.components as components -import azlmbr.editor as editor -import azlmbr.legacy.general as general -import azlmbr.math as math - -sys.path.append(os.path.join(azlmbr.paths.devroot, 'AutomatedTesting', 'Gem', 'PythonTests')) -import editor_python_test_tools.hydra_editor_utils as hydra -from editor_python_test_tools.editor_test_helper import EditorTestHelper -from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg +class Tests: + blocked_instance_count = ( + "Instance count is as expected wih a Blocker setup", + "Found unexpected instances with a Blocker setup" + ) -class test_MeshBlocker_InstancesBlockedByMesh(EditorTestHelper): - def __init__(self): - EditorTestHelper.__init__(self, log_prefix="MeshBlocker_InstancesBlockedByMesh", args=["level"]) +def MeshBlocker_InstancesBlockedByMesh(): + """ + Summary: + Level is created. An entity with a vegetation spawner and entity with vegetation blocker (Mesh) component are + added. Finally, the instance counts are checked to verify expected numbers after blocker is applied. - def run_test(self): - """ - Summary: - Level is created. An entity with a vegetation spawner and entity with vegetation blocker (Mesh) component are - added. Finally, the instance counts are checked to verify expected numbers after blocker is applied. + Expected Behavior: + The vegetation planted in the Spawner area is blocked by the Mesh of the Vegetation Blocker Mesh component. - Expected Behavior: - The vegetation planted in the Spawner area is blocked by the Mesh of the Vegetation Blocker Mesh component. + Test Steps: + --> Open a level + --> Create Spawner Entity + --> Create Surface Entity to spawn vegetation instances on + --> Create Blocker Entity with cube mesh + --> Verify spawned vegetation instance counts - Test Steps: - --> Create level - --> Create Spawner Entity - --> Create Surface Entity to spawn vegetation instances on - --> Create Blocker Entity with cube mesh - --> Verify spawned vegetation instance counts + Note: + - Any passed and failed tests are written to the Editor.log file. + Parsing the file or running a log_monitor are required to observe the test results. - Note: - - Any passed and failed tests are written to the Editor.log file. - Parsing the file or running a log_monitor are required to observe the test results. + :return: None + """ - :return: None - """ + import os - # Create a new level - self.test_success = self.create_level( - self.args["level"], - heightmap_resolution=1024, - heightmap_meters_per_pixel=1, - terrain_texture_resolution=4096, - use_terrain=False, - ) + import azlmbr.asset as asset + import azlmbr.bus as bus + import azlmbr.components as components + import azlmbr.legacy.general as general + import azlmbr.math as math - general.set_current_view_position(500.49, 498.69, 46.66) - general.set_current_view_rotation(-42.05, 0.00, -36.33) + import editor_python_test_tools.hydra_editor_utils as hydra + from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg + from editor_python_test_tools.utils import Report + from editor_python_test_tools.utils import TestHelper as helper - # Create entity with components "Vegetation Layer Spawner", "Vegetation Asset List", "Box Shape" - entity_position = math.Vector3(512.0, 512.0, 32.0) - asset_path = os.path.join("Slices", "PurpleFlower.dynamicslice") - spawner_entity = dynveg.create_vegetation_area("Instance Spawner", - entity_position, - 10.0, 10.0, 10.0, - asset_path) + # Open an existing simple level + helper.init_idle() + helper.open_level("Physics", "Base") - # Create surface entity to plant on - dynveg.create_surface_entity("Surface Entity", entity_position, 10.0, 10.0, 1.0) + general.set_current_view_position(500.49, 498.69, 46.66) + general.set_current_view_rotation(-42.05, 0.00, -36.33) - # Create blocker entity with cube mesh - mesh_type_id = azlmbr.globals.property.EditorMeshComponentTypeId - blocker_entity = hydra.Entity("Blocker Entity") - blocker_entity.create_entity(entity_position, - ["Vegetation Layer Blocker (Mesh)"]) - blocker_entity.add_component_of_type(mesh_type_id) - if blocker_entity.id.IsValid(): - print(f"'{blocker_entity.name}' created") - cubeId = asset.AssetCatalogRequestBus( - bus.Broadcast, "GetAssetIdByPath", os.path.join("objects", "_primitives", "_box_1x1.azmodel"), math.Uuid(), - False) - blocker_entity.get_set_test(1, "Controller|Configuration|Mesh Asset", cubeId) - components.TransformBus(bus.Event, "SetLocalUniformScale", blocker_entity.id, 2.0) + # Create entity with components "Vegetation Layer Spawner", "Vegetation Asset List", "Box Shape" + entity_position = math.Vector3(512.0, 512.0, 32.0) + asset_path = os.path.join("Slices", "PurpleFlower.dynamicslice") + spawner_entity = dynveg.create_vegetation_area("Instance Spawner", + entity_position, + 10.0, 10.0, 10.0, + asset_path) - # Verify spawned instance counts are accurate after addition of Blocker Entity - num_expected = 160 # Number of "PurpleFlower"s that plant on a 10 x 10 surface minus 2m blocker cube - result = self.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, - num_expected), 2.0) - self.test_success = self.test_success and result + # Create surface entity to plant on + dynveg.create_surface_entity("Surface Entity", entity_position, 10.0, 10.0, 1.0) + + # Create blocker entity with cube mesh + mesh_type_id = azlmbr.globals.property.EditorMeshComponentTypeId + blocker_entity = hydra.Entity("Blocker Entity") + blocker_entity.create_entity(entity_position, + ["Vegetation Layer Blocker (Mesh)"]) + blocker_entity.add_component_of_type(mesh_type_id) + if blocker_entity.id.IsValid(): + print(f"'{blocker_entity.name}' created") + cubeId = asset.AssetCatalogRequestBus( + bus.Broadcast, "GetAssetIdByPath", os.path.join("objects", "_primitives", "_box_1x1.azmodel"), math.Uuid(), + False) + blocker_entity.get_set_test(1, "Controller|Configuration|Mesh Asset", cubeId) + components.TransformBus(bus.Event, "SetLocalUniformScale", blocker_entity.id, 2.0) + + # Verify spawned instance counts are accurate after addition of Blocker Entity + num_expected = 160 # Number of "PurpleFlower"s that plant on a 10 x 10 surface minus 2m blocker cube + result = helper.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, + num_expected), 2.0) + Report.result(Tests.blocked_instance_count, result) -test = test_MeshBlocker_InstancesBlockedByMesh() -test.run() +if __name__ == "__main__": + + from editor_python_test_tools.utils import Report + Report.start_test(MeshBlocker_InstancesBlockedByMesh) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/MeshBlocker_InstancesBlockedByMeshHeightTuning.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/MeshBlocker_InstancesBlockedByMeshHeightTuning.py index f5dad6b64d..be15c9967c 100755 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/MeshBlocker_InstancesBlockedByMeshHeightTuning.py +++ b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/MeshBlocker_InstancesBlockedByMeshHeightTuning.py @@ -5,104 +5,100 @@ For complete copyright and license terms please see the LICENSE at the root of t SPDX-License-Identifier: Apache-2.0 OR MIT """ -import os -import math as pymath -import sys -sys.path.append(os.path.dirname(os.path.abspath(__file__))) -import azlmbr -import azlmbr.asset as asset -import azlmbr.bus as bus -import azlmbr.components as components -import azlmbr.editor as editor -import azlmbr.legacy.general as general -import azlmbr.math as math +class Tests: + blocked_instance_count = ( + "Instance count is as expected wih a Blocker setup", + "Found unexpected instances with a Blocker setup" + ) -sys.path.append(os.path.join(azlmbr.paths.devroot, 'AutomatedTesting', 'Gem', 'PythonTests')) -import editor_python_test_tools.hydra_editor_utils as hydra -from editor_python_test_tools.editor_test_helper import EditorTestHelper -from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg +def MeshBlocker_InstancesBlockedByMeshHeightTuning(): + """ + Summary: + A temporary level is created, then a simple vegetation area is created. A blocker area is created and it is + verified that the tuning of the height percent blocker setting works as expected. + + Expected Behavior: + Vegetation is blocked only around the trunk of the tree, while it still plants under the areas covered by branches. + + Test Steps: + 1) Open a level + 2) Create entity with components "Vegetation Layer Spawner", "Vegetation Asset List", "Box Shape" + 3) Create surface entity + 4) Create blocker entity with sphere mesh + 5) Adjust the height Min/Max percentage values of blocker + 6) Verify spawned instance counts are accurate after adjusting height Max percentage of Blocker Entity + + Note: + - Any passed and failed tests are written to the Editor.log file. + Parsing the file or running a log_monitor are required to observe the test results. + + :return: None + """ + + import os + import math as pymath + + import azlmbr + import azlmbr.asset as asset + import azlmbr.bus as bus + import azlmbr.components as components + import azlmbr.legacy.general as general + import azlmbr.math as math + + import editor_python_test_tools.hydra_editor_utils as hydra + from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg + from editor_python_test_tools.utils import Report + from editor_python_test_tools.utils import TestHelper as helper + + # 1) Open an existing simple level + helper.init_idle() + helper.open_level("Physics", "Base") + + general.set_current_view_position(500.49, 498.69, 46.66) + general.set_current_view_rotation(-42.05, 0.00, -36.33) + + # 2) Create entity with components "Vegetation Layer Spawner", "Vegetation Asset List", "Box Shape" + entity_position = math.Vector3(512.0, 512.0, 32.0) + asset_path = os.path.join("Slices", "PurpleFlower.dynamicslice") + spawner_entity = dynveg.create_vegetation_area("Instance Spawner", + entity_position, + 10.0, 10.0, 10.0, + asset_path) + + # 3) Create surface entity to plant on + dynveg.create_surface_entity("Surface Entity", entity_position, 10.0, 10.0, 1.0) + + # 4) Create blocker entity with rotated cube mesh + y_rotation = pymath.radians(45.0) + mesh_type_id = azlmbr.globals.property.EditorMeshComponentTypeId + blocker_entity = hydra.Entity("Blocker Entity") + blocker_entity.create_entity(entity_position, + ["Vegetation Layer Blocker (Mesh)"]) + blocker_entity.add_component_of_type(mesh_type_id) + if blocker_entity.id.IsValid(): + Report.info(f"'{blocker_entity.name}' created") + sphere_id = asset.AssetCatalogRequestBus( + bus.Broadcast, "GetAssetIdByPath", os.path.join("objects", "_primitives", "_box_1x1.azmodel"), math.Uuid(), + False) + blocker_entity.get_set_test(1, "Controller|Configuration|Mesh Asset", sphere_id) + components.TransformBus(bus.Event, "SetLocalUniformScale", blocker_entity.id, 5.0) + components.TransformBus(bus.Event, "SetLocalRotation", blocker_entity.id, math.Vector3(0.0, y_rotation, 0.0)) + + # 5) Adjust the height Max percentage values of blocker + blocker_entity.get_set_test(0, "Configuration|Mesh Height Percent Max", 0.8) + + # 6) Verify spawned instance counts are accurate after adjusting height Max percentage of Blocker Entity + # The number of "PurpleFlower" instances that plant on a 10 x 10 surface minus those blocked by the rotated at + # 80% max height factored in. + num_expected = 127 + result = helper.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, + num_expected), 5.0) + Report.result(Tests.blocked_instance_count, result) -class test_MeshBlocker_InstancesBlockedByMeshHeightTuning(EditorTestHelper): - def __init__(self): - EditorTestHelper.__init__(self, log_prefix="MeshBlocker_InstancesBlockedByMeshHeightTuning", args=["level"]) +if __name__ == "__main__": - def run_test(self): - """ - Summary: - A temporary level is created, then a simple vegetation area is created. A blocker area is created and it is - verified that the tuning of the height percent blocker setting works as expected. - - Expected Behavior: - Vegetation is blocked only around the trunk of the tree, while it still plants under the areas covered by branches. - - Test Steps: - 1) Create level - 2) Create entity with components "Vegetation Layer Spawner", "Vegetation Asset List", "Box Shape" - 3) Create surface entity - 4) Create blocker entity with sphere mesh - 5) Adjust the height Min/Max percentage values of blocker - 6) Verify spawned instance counts are accurate after adjusting height Max percentage of Blocker Entity - - Note: - - Any passed and failed tests are written to the Editor.log file. - Parsing the file or running a log_monitor are required to observe the test results. - - :return: None - """ - - # 1) Create level - self.test_success = self.create_level( - self.args["level"], - heightmap_resolution=1024, - heightmap_meters_per_pixel=1, - terrain_texture_resolution=4096, - use_terrain=False, - ) - - general.set_current_view_position(500.49, 498.69, 46.66) - general.set_current_view_rotation(-42.05, 0.00, -36.33) - - # 2) Create entity with components "Vegetation Layer Spawner", "Vegetation Asset List", "Box Shape" - entity_position = math.Vector3(512.0, 512.0, 32.0) - asset_path = os.path.join("Slices", "PurpleFlower.dynamicslice") - spawner_entity = dynveg.create_vegetation_area("Instance Spawner", - entity_position, - 10.0, 10.0, 10.0, - asset_path) - - # 3) Create surface entity to plant on - dynveg.create_surface_entity("Surface Entity", entity_position, 10.0, 10.0, 1.0) - - # 4) Create blocker entity with rotated cube mesh - y_rotation = pymath.radians(45.0) - mesh_type_id = azlmbr.globals.property.EditorMeshComponentTypeId - blocker_entity = hydra.Entity("Blocker Entity") - blocker_entity.create_entity(entity_position, - ["Vegetation Layer Blocker (Mesh)"]) - blocker_entity.add_component_of_type(mesh_type_id) - if blocker_entity.id.IsValid(): - print(f"'{blocker_entity.name}' created") - sphere_id = asset.AssetCatalogRequestBus( - bus.Broadcast, "GetAssetIdByPath", os.path.join("objects", "_primitives", "_box_1x1.azmodel"), math.Uuid(), - False) - blocker_entity.get_set_test(1, "Controller|Configuration|Mesh Asset", sphere_id) - components.TransformBus(bus.Event, "SetLocalUniformScale", blocker_entity.id, 5.0) - components.TransformBus(bus.Event, "SetLocalRotation", blocker_entity.id, math.Vector3(0.0, y_rotation, 0.0)) - - # 5) Adjust the height Max percentage values of blocker - blocker_entity.get_set_test(0, "Configuration|Mesh Height Percent Max", 0.8) - - # 6) Verify spawned instance counts are accurate after adjusting height Max percentage of Blocker Entity - # The number of "PurpleFlower" instances that plant on a 10 x 10 surface minus those blocked by the rotated at - # 80% max height factored in. - num_expected = 127 - result = self.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, - num_expected), 5.0) - self.test_success = self.test_success and result - - -test = test_MeshBlocker_InstancesBlockedByMeshHeightTuning() -test.run() + from editor_python_test_tools.utils import Report + Report.start_test(MeshBlocker_InstancesBlockedByMeshHeightTuning) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/MeshSurfaceTagEmitter_DependentOnMeshComponent.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/MeshSurfaceTagEmitter_DependentOnMeshComponent.py index c093529888..cee8ebe5b6 100755 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/MeshSurfaceTagEmitter_DependentOnMeshComponent.py +++ b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/MeshSurfaceTagEmitter_DependentOnMeshComponent.py @@ -5,93 +5,87 @@ For complete copyright and license terms please see the LICENSE at the root of t SPDX-License-Identifier: Apache-2.0 OR MIT """ -import os -import sys -sys.path.append(os.path.dirname(os.path.abspath(__file__))) -import azlmbr.bus as bus -import azlmbr.editor as editor -import azlmbr.entity as EntityId -import azlmbr.math as math -import azlmbr.paths - -sys.path.append(os.path.join(azlmbr.paths.devroot, 'AutomatedTesting', 'Gem', 'PythonTests')) -import editor_python_test_tools.hydra_editor_utils as hydra -from editor_python_test_tools.editor_test_helper import EditorTestHelper +class Tests: + new_entity_created = ( + "Successfully created new entity", + "Failed to create new entity" + ) + emitter_disabled_before_mesh = ( + "Mesh Surface Tag Emitter is disabled without a Mesh component", + "Mesh Surface Tag Emitter is unexpectedly enabled without a Mesh component" + ) + emitter_enabled_after_mesh = ( + "Mesh Surface Tag Emitter is enabled after adding a Mesh component", + "Mesh Surface Tag Emitter is unexpectedly disabled after adding a Mesh component" + ) -class TestMeshSurfaceTagEmitter(EditorTestHelper): - def __init__(self): - EditorTestHelper.__init__(self, log_prefix="MeshSurfaceTagEmitter_DependentOnMeshComponent", args=["level"]) +def MeshSurfaceTagEmitter_DependentOnMeshComponent(): + """ + Summary: + A New level is loaded. A New entity is created with component "Mesh Surface Tag Emitter". Adding a component + "Mesh" to the same entity. - def run_test(self): - """ - Summary: - A New level is loaded. A New entity is created with component "Mesh Surface Tag Emitter". Adding a component - "Mesh" to the same entity. + Expected Behavior: + Mesh Surface Tag Emitter is disabled until the required Mesh component is added to the entity. - Expected Behavior: - Mesh Surface Tag Emitter is disabled until the required Mesh component is added to the entity. + Test Steps: + 1) Open level + 2) Create a new entity with component "Mesh Surface Tag Emitter" + 3) Make sure Mesh Surface Tag Emitter is disabled + 4) Add Mesh to the same entity + 5) Make sure Mesh Surface Tag Emitter is enabled after adding Mesh - Test Steps: - 1) Open level - 2) Create a new entity with component "Mesh Surface Tag Emitter" - 3) Make sure Mesh Surface Tag Emitter is disabled - 4) Add Mesh to the same entity - 5) Make sure Mesh Surface Tag Emitter is enabled after adding Mesh + Note: + - This test file must be called from the Open 3D Engine Editor command terminal + - Any passed and failed tests are written to the Editor.log file. + Parsing the file or running a log_monitor are required to observe the test results. - Note: - - This test file must be called from the Open 3D Engine Editor command terminal - - Any passed and failed tests are written to the Editor.log file. - Parsing the file or running a log_monitor are required to observe the test results. + :return: None + """ - :return: None - """ + import azlmbr.bus as bus + import azlmbr.editor as editor + import azlmbr.entity as EntityId + import azlmbr.math as math - def is_component_enabled(EntityComponentIdPair): - return editor.EditorComponentAPIBus(bus.Broadcast, "IsComponentEnabled", EntityComponentIdPair) + import editor_python_test_tools.hydra_editor_utils as hydra + from editor_python_test_tools.utils import Report + from editor_python_test_tools.utils import TestHelper as helper - # 1) Open level - self.test_success = self.create_level( - self.args["level"], - heightmap_resolution=1024, - heightmap_meters_per_pixel=1, - terrain_texture_resolution=4096, - use_terrain=False, - ) + def is_component_enabled(EntityComponentIdPair): + return editor.EditorComponentAPIBus(bus.Broadcast, "IsComponentEnabled", EntityComponentIdPair) - # 2) Create a new entity with component "Mesh Surface Tag Emitter" - entity_position = math.Vector3(125.0, 136.0, 32.0) - component_to_add = "Mesh Surface Tag Emitter" - entity_id = editor.ToolsApplicationRequestBus( - bus.Broadcast, "CreateNewEntityAtPosition", entity_position, EntityId.EntityId() - ) - meshentity = hydra.Entity("meshentity", entity_id) - meshentity.components = [] - meshentity.components.append(hydra.add_component(component_to_add, entity_id)) - if entity_id.IsValid(): - print("New Entity Created") + # 1) Open an existing simple level + helper.init_idle() + helper.open_level("Physics", "Base") - # 3) Make sure Mesh Surface Tag Emitter is disabled - is_enabled = is_component_enabled(meshentity.components[0]) - self.test_success = self.test_success and not is_enabled - if not is_enabled: - print(f"{component_to_add} is Disabled") - elif is_enabled: - print(f"{component_to_add} is Enabled. But It should be disabled before adding Mesh") + # 2) Create a new entity with component "Mesh Surface Tag Emitter" + entity_position = math.Vector3(125.0, 136.0, 32.0) + component_to_add = "Mesh Surface Tag Emitter" + entity_id = editor.ToolsApplicationRequestBus( + bus.Broadcast, "CreateNewEntityAtPosition", entity_position, EntityId.EntityId() + ) + meshentity = hydra.Entity("meshentity", entity_id) + meshentity.components = [] + meshentity.components.append(hydra.add_component(component_to_add, entity_id)) + Report.critical_result(Tests.new_entity_created, entity_id.IsValid()) - # 4) Add Mesh to the same entity - component = "Mesh" - meshentity.components.append(hydra.add_component(component, entity_id)) + # 3) Make sure Mesh Surface Tag Emitter is disabled + is_enabled = is_component_enabled(meshentity.components[0]) + Report.result(Tests.emitter_disabled_before_mesh, not is_enabled) - # 5) Make sure Mesh Surface Tag Emitter is enabled after adding Mesh - is_enabled = is_component_enabled(meshentity.components[0]) - self.test_success = self.test_success and is_enabled - if is_enabled: - print(f"{component_to_add} is Enabled") - elif not is_enabled: - print(f"{component_to_add} is Disabled. But It should be enabled after adding Mesh") + # 4) Add Mesh to the same entity + component = "Mesh" + meshentity.components.append(hydra.add_component(component, entity_id)) + + # 5) Make sure Mesh Surface Tag Emitter is enabled after adding Mesh + is_enabled = is_component_enabled(meshentity.components[0]) + Report.result(Tests.emitter_enabled_after_mesh, is_enabled) -test = TestMeshSurfaceTagEmitter() -test.run() +if __name__ == "__main__": + + from editor_python_test_tools.utils import Report + Report.start_test(MeshSurfaceTagEmitter_DependentOnMeshComponent) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/MeshSurfaceTagEmitter_SurfaceTagsAddRemoveSuccessfully.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/MeshSurfaceTagEmitter_SurfaceTagsAddRemoveSuccessfully.py index 83beb5d0c2..d7abc66106 100755 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/MeshSurfaceTagEmitter_SurfaceTagsAddRemoveSuccessfully.py +++ b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/MeshSurfaceTagEmitter_SurfaceTagsAddRemoveSuccessfully.py @@ -5,75 +5,71 @@ For complete copyright and license terms please see the LICENSE at the root of t SPDX-License-Identifier: Apache-2.0 OR MIT """ -import os -import sys -sys.path.append(os.path.dirname(os.path.abspath(__file__))) -import azlmbr.math as math -import azlmbr.paths -import azlmbr.surface_data as surface_data - -sys.path.append(os.path.join(azlmbr.paths.devroot, 'AutomatedTesting', 'Gem', 'PythonTests')) -import editor_python_test_tools.hydra_editor_utils as hydra -from editor_python_test_tools.editor_test_helper import EditorTestHelper +class Tests: + add_surface_tag = ( + "Surface Tag added successfully", + "Failed to add Surface Tag" + ) + remove_surface_tag = ( + "Successfully removed Surface Tag", + "Failed to remove Surface Tag" + ) -class TestMeshSurfaceTagEmitter(EditorTestHelper): - def __init__(self): - EditorTestHelper.__init__(self, log_prefix="MeshSurfaceTagEmitter_SurfaceTagsAddRemoveSucessfully", - args=["level"]) +def MeshSurfaceTagEmitter_SurfaceTagsAddRemoveSuccessfully(): + """ + Summary: + An enity with Mesh Tag Emitter and a Mesh is added to the viewport to verify if we are able to + add/remove surface tags. - def run_test(self): - """ - Summary: - An enity with Mesh Tag Emitter and a Mesh is added to the viewport to verify if we are able to - add/remove surface tags. + Expected Behavior: + A new Surface Tag can be added and removed from the component. - Expected Behavior: - A new Surface Tag can be added and removed from the component. + Test Steps: + 1) Open level + 2) Create a new entity with components "Mesh Surface Tag Emitter", "Mesh" + 3) Add/ remove Surface Tags - Test Steps: - 1) Open level - 2) Create a new entity with components "Mesh Surface Tag Emitter", "Mesh" - 3) Add/ remove Surface Tags + Note: + - This test file must be called from the Open 3D Engine Editor command terminal + - Any passed and failed tests are written to the Editor.log file. + Parsing the file or running a log_monitor are required to observe the test results. - Note: - - This test file must be called from the Open 3D Engine Editor command terminal - - Any passed and failed tests are written to the Editor.log file. - Parsing the file or running a log_monitor are required to observe the test results. + :return: None + """ - :return: None - """ + import azlmbr.math as math + import azlmbr.surface_data as surface_data - # 1) Open level - self.test_success = self.create_level( - self.args["level"], - heightmap_resolution=1024, - heightmap_meters_per_pixel=1, - terrain_texture_resolution=4096, - use_terrain=False, - ) + import editor_python_test_tools.hydra_editor_utils as hydra + from editor_python_test_tools.utils import Report + from editor_python_test_tools.utils import TestHelper as helper - # 2) Create a new entity with components "Mesh Surface Tag Emitter", "Mesh" - entity_position = math.Vector3(125.0, 136.0, 32.0) - components_to_add = ["Mesh Surface Tag Emitter", "Mesh"] - entity = hydra.Entity("entity") - entity.create_entity(entity_position, components_to_add) + # 1) Open an existing simple level + helper.init_idle() + helper.open_level("Physics", "Base") - # 3) Add/ remove Surface Tags - tag = surface_data.SurfaceTag() - tag.SetTag("water") - pte = hydra.get_property_tree(entity.components[0]) - path = "Configuration|Generated Tags" - pte.add_container_item(path, 0, tag) - success = self.wait_for_condition(lambda: pte.get_container_count(path).GetValue() == 1, 5.0) - self.test_success = self.test_success and success - print(f"Added SurfaceTag: container count is {pte.get_container_count(path).GetValue()}") - pte.remove_container_item(path, 0) - success = self.wait_for_condition(lambda: pte.get_container_count(path).GetValue() == 0, 5.0) - self.test_success = self.test_success and success - print(f"Removed SurfaceTag: container count is {pte.get_container_count(path).GetValue()}") + # 2) Create a new entity with components "Mesh Surface Tag Emitter", "Mesh" + entity_position = math.Vector3(125.0, 136.0, 32.0) + components_to_add = ["Mesh Surface Tag Emitter", "Mesh"] + entity = hydra.Entity("entity") + entity.create_entity(entity_position, components_to_add) + + # 3) Add/ remove Surface Tags + tag = surface_data.SurfaceTag() + tag.SetTag("water") + pte = hydra.get_property_tree(entity.components[0]) + path = "Configuration|Generated Tags" + pte.add_container_item(path, 0, tag) + success = helper.wait_for_condition(lambda: pte.get_container_count(path).GetValue() == 1, 5.0) + Report.result(Tests.add_surface_tag, success) + pte.remove_container_item(path, 0) + success = helper.wait_for_condition(lambda: pte.get_container_count(path).GetValue() == 0, 5.0) + Report.result(Tests.remove_surface_tag, success) -test = TestMeshSurfaceTagEmitter() -test.run() +if __name__ == "__main__": + + from editor_python_test_tools.utils import Report + Report.start_test(MeshSurfaceTagEmitter_SurfaceTagsAddRemoveSuccessfully) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/PhysXColliderSurfaceTagEmitter_E2E_Editor.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/PhysXColliderSurfaceTagEmitter_E2E_Editor.py index 361dbdaea9..b5739c2386 100755 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/PhysXColliderSurfaceTagEmitter_E2E_Editor.py +++ b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/PhysXColliderSurfaceTagEmitter_E2E_Editor.py @@ -5,27 +5,30 @@ For complete copyright and license terms please see the LICENSE at the root of t SPDX-License-Identifier: Apache-2.0 OR MIT """ -import os -import sys -sys.path.append(os.path.dirname(os.path.abspath(__file__))) -import azlmbr.asset as asset -import azlmbr.editor as editor -import azlmbr.legacy.general as general -import azlmbr.bus as bus -import azlmbr.math as math +def PhysXColliderSurfaceTagEmitter_E2E_Editor(): + """ + Summary: + Test aspects of the PhysX Collider Surface Tag Emitter Component through the BehaviorContext and the Property + Tree. -sys.path.append(os.path.join(azlmbr.paths.devroot, 'AutomatedTesting', 'Gem', 'PythonTests')) -import editor_python_test_tools.hydra_editor_utils as hydra -from editor_python_test_tools.editor_test_helper import EditorTestHelper -from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg + :return: None + """ + import os -class TestPhysXColliderSurfaceTagEmitter(EditorTestHelper): - def __init__(self): - EditorTestHelper.__init__(self, log_prefix="PhysXColliderSurfaceTagEmitter_E2E_Editor", args=["level"]) + import azlmbr.asset as asset + import azlmbr.editor as editor + import azlmbr.legacy.general as general + import azlmbr.bus as bus + import azlmbr.math as math - def validate_behavior_context(self): + import editor_python_test_tools.hydra_editor_utils as hydra + from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg + from editor_python_test_tools.utils import Report + from editor_python_test_tools.utils import TestHelper as helper + + def validate_behavior_context(): # Verify that we can create the component through the BehaviorContext behavior_context_test_success = True test_component = azlmbr.surface_data.SurfaceDataColliderComponent() @@ -38,163 +41,181 @@ class TestPhysXColliderSurfaceTagEmitter(EditorTestHelper): provider_tag2 = azlmbr.surface_data.SurfaceTag('provider_tag2') modifier_tag1 = azlmbr.surface_data.SurfaceTag('modifier_tag1') modifier_tag2 = azlmbr.surface_data.SurfaceTag('modifier_tag2') - behavior_context_test_success = behavior_context_test_success and hydra.get_set_property_test(test_component, - 'providerTags', - [provider_tag1, - provider_tag2]) - behavior_context_test_success = behavior_context_test_success and hydra.get_set_property_test(test_component, - 'modifierTags', - [modifier_tag1, - modifier_tag2]) - self.log(f'SurfaceDataColliderComponent() BehaviorContext test: {behavior_context_test_success}') + behavior_context_test_success = behavior_context_test_success and hydra.get_set_property_test( + test_component, + 'providerTags', + [provider_tag1, + provider_tag2]) + behavior_context_test_success = behavior_context_test_success and hydra.get_set_property_test( + test_component, + 'modifierTags', + [modifier_tag1, + modifier_tag2]) + Report.info(f'SurfaceDataColliderComponent() BehaviorContext test: {behavior_context_test_success}') return behavior_context_test_success - def run_test(self): - """ - Summary: - Test aspects of the PhysX Collider Surface Tag Emitter Component through the BehaviorContext and the Property Tree. + # Open an existing simple level + helper.init_idle() + helper.open_level("Physics", "Base") - :return: None - """ - # Create an empty level - self.test_success = self.create_level( - self.args["level"], - heightmap_resolution=1024, - heightmap_meters_per_pixel=1, - terrain_texture_resolution=4096, - use_terrain=False, + # Verify all of the BehaviorContext API: + behavior_context = ( + "SurfaceDataColliderComponent() Behavior Context tests were successful", + "SurfaceDataColliderComponent() Behavior Context tests failed" + ) + Report.result(behavior_context, validate_behavior_context()) + + # Set up a test environment to validate the PhysX Collider Surface Tag Emitter Component. + # The test environment will consist of the following: + # - a 32 x 32 x 1 box shape that emits a surface with no tags + # - a 32 x 32 x 32 vegetation area that will only place vegetation on surfaces with the 'test' tag + # With this setup, no vegetation will appear until a Surface Tag Emitter either emits new points with + # the correct tag, or modifies points on our box shape to emit the correct tag. + + # Initialize some arbitrary constants for our test + entity_center_point = math.Vector3(512.0, 512.0, 100.0) + invalid_tag = azlmbr.surface_data.SurfaceTag('invalid') + surface_tag = azlmbr.surface_data.SurfaceTag('test') + test_box_size = 32.0 + baseline_surface_height = 1.0 + collider_radius = 4.0 + collider_diameter = collider_radius * 2.0 + + # Set viewport view of area under test, and toggle helpers back on + general.set_current_view_position(512.0, 485.0, 110.0) + general.set_current_view_rotation(-35.0, 0.0, 0.0) + general.toggle_helpers() + + # Create the "flat surface" entity to use as our baseline surface + dynveg.create_surface_entity("Baseline Surface", entity_center_point, 32.0, 32.0, 1.0) + + # Create a new entity with required vegetation area components + asset_path = os.path.join("Slices", "PinkFlower.dynamicslice") + spawner_entity = dynveg.create_vegetation_area("Veg Area", entity_center_point, 32.0, 32.0, 32.0, asset_path) + + # Add a Vegetation Surface Mask Filter component to the spawner entity and set it to include the "test" tag + spawner_entity.add_component("Vegetation Surface Mask Filter") + spawner_entity.get_set_test(3, "Configuration|Inclusion|Surface Tags", [surface_tag]) + + # At this point, there should be 0 instances within our entire veg area + initial_instance_count = ( + "Found no instances as expected with initial setup", + "Unexpected found instances with initial setup" + ) + initial_success = helper.wait_for_condition(lambda: dynveg.validate_instance_count(entity_center_point, 16.0, 0), + 5.0) + Report.result(initial_instance_count, initial_success) + + # Create an entity with a PhysX Collider and our PhysX Collider Surface Tag Emitter + collider_entity_created = ( + "Successfully created a Collider entity", + "Failed to create Collider entity" + ) + collider_entity = hydra.Entity("Collider Surface") + collider_entity.create_entity( + entity_center_point, + ["PhysX Collider", "PhysX Collider Surface Tag Emitter"] ) + Report.result(collider_entity_created, collider_entity.id.IsValid()) - # Verify all of the BehaviorContext API: - self.test_success = self.test_success and self.validate_behavior_context() + # Set up the PhysX Collider so that each shape type (sphere, box, capsule) has the same test height. + hydra.get_set_test(collider_entity, 0, "Shape Configuration|Sphere|Radius", collider_radius) + hydra.get_set_test(collider_entity, 0, "Shape Configuration|Box|Dimensions", math.Vector3(collider_diameter, + collider_diameter, + collider_diameter)) + hydra.get_set_test(collider_entity, 0, "Shape Configuration|Capsule|Height", collider_diameter) - # Set up a test environment to validate the PhysX Collider Surface Tag Emitter Component. - # The test environment will consist of the following: - # - a 32 x 32 x 1 box shape that emits a surface with no tags - # - a 32 x 32 x 32 vegetation area that will only place vegetation on surfaces with the 'test' tag - # With this setup, no vegetation will appear until a Surface Tag Emitter either emits new points with - # the correct tag, or modifies points on our box shape to emit the correct tag. - - # Initialize some arbitrary constants for our test - entity_center_point = math.Vector3(512.0, 512.0, 100.0) - invalid_tag = azlmbr.surface_data.SurfaceTag('invalid') - surface_tag = azlmbr.surface_data.SurfaceTag('test') - test_box_size = 32.0 - baseline_surface_height = 1.0 - collider_radius = 4.0 - collider_diameter = collider_radius * 2.0 - - # Set viewport view of area under test, and toggle helpers back on - general.set_current_view_position(512.0, 485.0, 110.0) - general.set_current_view_rotation(-35.0, 0.0, 0.0) - general.toggle_helpers() - - # Create the "flat surface" entity to use as our baseline surface - dynveg.create_surface_entity("Baseline Surface", entity_center_point, 32.0, 32.0, 1.0) - - # Create a new entity with required vegetation area components - asset_path = os.path.join("Slices", "PinkFlower.dynamicslice") - spawner_entity = dynveg.create_vegetation_area("Veg Area", entity_center_point, 32.0, 32.0, 32.0, asset_path) - - # Add a Vegetation Surface Mask Filter component to the spawner entity and set it to include the "test" tag - spawner_entity.add_component("Vegetation Surface Mask Filter") - spawner_entity.get_set_test(3, "Configuration|Inclusion|Surface Tags", [surface_tag]) - - # At this point, there should be 0 instances within our entire veg area - initial_success = self.wait_for_condition(lambda: dynveg.validate_instance_count(entity_center_point, 16.0, 0), - 5.0) - self.test_success = self.test_success and initial_success - - # Create an entity with a PhysX Collider and our PhysX Collider Surface Tag Emitter - collider_entity = hydra.Entity("Collider Surface") - collider_entity.create_entity( - entity_center_point, - ["PhysX Collider", "PhysX Collider Surface Tag Emitter"] - ) - if collider_entity.id.IsValid(): - self.log(f"'{collider_entity.name}' created") - - # Set up the PhysX Collider so that each shape type (sphere, box, capsule) has the same test height. - hydra.get_set_test(collider_entity, 0, "Shape Configuration|Sphere|Radius", collider_radius) - hydra.get_set_test(collider_entity, 0, "Shape Configuration|Box|Dimensions", math.Vector3(collider_diameter, - collider_diameter, - collider_diameter)) - hydra.get_set_test(collider_entity, 0, "Shape Configuration|Capsule|Height", collider_diameter) - - # Run through each collider shape type (sphere, box, capsule) and verify the surface generation - # and surface modification of the PhysX Collision Surface Tag Emitter Component. - for collider_shape in range(0, 3): - hydra.get_set_test(collider_entity, 0, "Shape Configuration|Shape", collider_shape) - - # Test: Generate a new surface on the collider. - # There should be one instance at the very top of the collider sphere, and none on the baseline surface - # (We use a small query box to only check for one placed instance point) - hydra.get_set_test(collider_entity, 1, "Configuration|Generated Tags", [surface_tag]) - hydra.get_set_test(collider_entity, 1, "Configuration|Extended Tags", [invalid_tag]) - top_point = math.Vector3(entity_center_point.x, entity_center_point.y, entity_center_point.z + - collider_radius) - baseline_surface_point = math.Vector3(entity_center_point.x, entity_center_point.y, entity_center_point.z + - (baseline_surface_height / 2.0)) - top_point_success = self.wait_for_condition(lambda: dynveg.validate_instance_count(top_point, 0.25, 1), 5.0) - self.test_success = self.test_success and top_point_success - baseline_success = self.wait_for_condition(lambda: dynveg.validate_instance_count(baseline_surface_point, - 0.25, 0), 5.0) - self.test_success = self.test_success and baseline_success - - # Test: Modify an existing surface inside the collider. - # There should be no instances at the very top of the collider sphere, and one on the baseline surface - # within our query box. - # (We use a small query box to only check for one placed instance point) - hydra.get_set_test(collider_entity, 1, "Configuration|Generated Tags", [invalid_tag]) - hydra.get_set_test(collider_entity, 1, "Configuration|Extended Tags", [surface_tag]) - top_point_success = self.wait_for_condition(lambda: dynveg.validate_instance_count(top_point, 0.25, 0), 5.0) - self.test_success = self.test_success and top_point_success - baseline_success = self.wait_for_condition(lambda: dynveg.validate_instance_count(baseline_surface_point, - 0.25, 1), 5.0) - self.test_success = self.test_success and baseline_success - - # Setup collider entity with a PhysX Mesh - test_physx_mesh_asset_id = asset.AssetCatalogRequestBus( - bus.Broadcast, "GetAssetIdByPath", os.path.join("levels", "physics", - "Material_PerFaceMaterialGetsCorrectMaterial", - "test.pxmesh"), math.Uuid(), False) - - # Remove/re-add component due to LYN-5496 - collider_entity.remove_component("PhysX Collider") - collider_entity.add_component("PhysX Collider") - self.wait_for_condition(lambda: editor.EditorComponentAPIBus(bus.Broadcast, 'IsComponentEnabled', - collider_entity.components[1]), 5.0) - hydra.get_set_test(collider_entity, 1, "Shape Configuration|Shape", 7) - hydra.get_set_test(collider_entity, 1, "Shape Configuration|Asset|PhysX Mesh", test_physx_mesh_asset_id) - - # Set the asset scale to match the test heights of the shapes tested - asset_scale = math.Vector3(1.0, 1.0, 9.0) - collider_entity.get_set_test(1, "Shape Configuration|Asset|Configuration|Asset Scale", asset_scale) + # Run through each collider shape type (sphere, box, capsule) and verify the surface generation + # and surface modification of the PhysX Collision Surface Tag Emitter Component. + for collider_shape in range(0, 3): + collider_shapes = {0: "Sphere", 1: "Box", 2: "Capsule"} + hydra.get_set_test(collider_entity, 0, "Shape Configuration|Shape", collider_shape) # Test: Generate a new surface on the collider. - # There should be one instance at the very top of the collider mesh, and none on the baseline surface + # There should be one instance at the very top of the collider sphere, and none on the baseline surface # (We use a small query box to only check for one placed instance point) - self.log("Starting PhysX Mesh Collider Test") - hydra.get_set_test(collider_entity, 0, "Configuration|Generated Tags", [surface_tag]) - hydra.get_set_test(collider_entity, 0, "Configuration|Extended Tags", [invalid_tag]) - top_point_success = self.wait_for_condition(lambda: dynveg.validate_instance_count(top_point, 0.25, 1), 5.0) - self.test_success = self.test_success and top_point_success - baseline_success = self.wait_for_condition(lambda: dynveg.validate_instance_count(baseline_surface_point, - 0.25, 0), 5.0) - self.test_success = self.test_success and baseline_success + on_collider_top_point_count = ( + f"Expected number of instances found on the top point for {collider_shapes[collider_shape]} shape", + f"Found an unexpected number of instances on the top point for {collider_shapes[collider_shape]} shape" + ) + on_collider_baseline_count = ( + f"Expected number of instances found on the baseline point for {collider_shapes[collider_shape]} shape", + f"Found an unexpected number of instances on the baseline point for {collider_shapes[collider_shape]} shape" + ) + hydra.get_set_test(collider_entity, 1, "Configuration|Generated Tags", [surface_tag]) + hydra.get_set_test(collider_entity, 1, "Configuration|Extended Tags", [invalid_tag]) + top_point = math.Vector3(entity_center_point.x, entity_center_point.y, entity_center_point.z + + collider_radius) + baseline_surface_point = math.Vector3(entity_center_point.x, entity_center_point.y, entity_center_point.z + + (baseline_surface_height / 2.0)) + top_point_success = helper.wait_for_condition(lambda: dynveg.validate_instance_count(top_point, 0.25, 1), 5.0) + Report.result(on_collider_top_point_count, top_point_success) + baseline_success = helper.wait_for_condition(lambda: dynveg.validate_instance_count(baseline_surface_point, + 0.25, 0), 5.0) + Report.result(on_collider_baseline_count, baseline_success) # Test: Modify an existing surface inside the collider. - # There should be no instances at the very top of the collider mesh, and none on the baseline surface within - # our query box as PhysX meshes are treated as hollow shells, not solid volumes. + # There should be no instances at the very top of the collider sphere, and one on the baseline surface + # within our query box. # (We use a small query box to only check for one placed instance point) - hydra.get_set_test(collider_entity, 0, "Configuration|Generated Tags", [invalid_tag]) - hydra.get_set_test(collider_entity, 0, "Configuration|Extended Tags", [surface_tag]) - top_point_success = self.wait_for_condition(lambda: dynveg.validate_instance_count(top_point, 0.25, 0), 5.0) - self.test_success = self.test_success and top_point_success - baseline_success = self.wait_for_condition(lambda: dynveg.validate_instance_count(baseline_surface_point, - 0.25, 0), 5.0) - self.test_success = self.test_success and baseline_success + hydra.get_set_test(collider_entity, 1, "Configuration|Generated Tags", [invalid_tag]) + hydra.get_set_test(collider_entity, 1, "Configuration|Extended Tags", [surface_tag]) + top_point_success = helper.wait_for_condition(lambda: dynveg.validate_instance_count(top_point, 0.25, 0), 5.0) + Report.result(on_collider_top_point_count, top_point_success) + baseline_success = helper.wait_for_condition(lambda: dynveg.validate_instance_count(baseline_surface_point, + 0.25, 1), 5.0) + Report.result(on_collider_baseline_count, baseline_success) + + # Setup collider entity with a PhysX Mesh + test_physx_mesh_asset_id = asset.AssetCatalogRequestBus( + bus.Broadcast, "GetAssetIdByPath", os.path.join("levels", "physics", + "Material_PerFaceMaterialGetsCorrectMaterial", + "test.pxmesh"), math.Uuid(), False) + collider_entity.remove_component("PhysX Collider") + collider_entity.add_component("PhysX Collider") + helper.wait_for_condition(lambda: editor.EditorComponentAPIBus(bus.Broadcast, 'IsComponentEnabled', + collider_entity.components[1]), 5.0) + hydra.get_set_test(collider_entity, 1, "Shape Configuration|Shape", 7) + hydra.get_set_test(collider_entity, 1, "Shape Configuration|Asset|PhysX Mesh", test_physx_mesh_asset_id) + + # Set the asset scale to match the test heights of the shapes tested + asset_scale = math.Vector3(1.0, 1.0, 9.0) + collider_entity.get_set_test(1, "Shape Configuration|Asset|Configuration|Asset Scale", asset_scale) + + # Test: Generate a new surface on the collider. + # There should be one instance at the very top of the collider mesh, and none on the baseline surface + # (We use a small query box to only check for one placed instance point) + Report.info("Starting PhysX Mesh Collider Test") + on_collider_top_point_count = ( + f"Expected number of instances found on the top point for a PhysX Mesh", + f"Found an unexpected number of instances on the top point for a PhysX Mesh" + ) + on_collider_baseline_count = ( + f"Expected number of instances found on the baseline point for a PhysX Mesh", + f"Found an unexpected number of instances on the baseline point for a PhysX Mesh" + ) + hydra.get_set_test(collider_entity, 0, "Configuration|Generated Tags", [surface_tag]) + hydra.get_set_test(collider_entity, 0, "Configuration|Extended Tags", [invalid_tag]) + top_point_success = helper.wait_for_condition(lambda: dynveg.validate_instance_count(top_point, 0.25, 1), 5.0) + Report.result(on_collider_top_point_count, top_point_success) + baseline_success = helper.wait_for_condition(lambda: dynveg.validate_instance_count(baseline_surface_point, + 0.25, 0), 5.0) + Report.result(on_collider_baseline_count, baseline_success) + + # Test: Modify an existing surface inside the collider. + # There should be no instances at the very top of the collider mesh, and none on the baseline surface within + # our query box as PhysX meshes are treated as hollow shells, not solid volumes. + # (We use a small query box to only check for one placed instance point) + hydra.get_set_test(collider_entity, 0, "Configuration|Generated Tags", [invalid_tag]) + hydra.get_set_test(collider_entity, 0, "Configuration|Extended Tags", [surface_tag]) + top_point_success = helper.wait_for_condition(lambda: dynveg.validate_instance_count(top_point, 0.25, 0), 5.0) + Report.result(on_collider_top_point_count, top_point_success) + baseline_success = helper.wait_for_condition(lambda: dynveg.validate_instance_count(baseline_surface_point, + 0.25, 0), 5.0) + Report.result(on_collider_baseline_count, baseline_success) -test = TestPhysXColliderSurfaceTagEmitter() -test.run() +if __name__ == "__main__": + + from editor_python_test_tools.utils import Report + Report.start_test(PhysXColliderSurfaceTagEmitter_E2E_Editor) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/PositionModifier_AutoSnapToSurfaceWorks.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/PositionModifier_AutoSnapToSurfaceWorks.py index a55e88488c..5a3ee70d22 100755 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/PositionModifier_AutoSnapToSurfaceWorks.py +++ b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/PositionModifier_AutoSnapToSurfaceWorks.py @@ -5,136 +5,133 @@ For complete copyright and license terms please see the LICENSE at the root of t SPDX-License-Identifier: Apache-2.0 OR MIT """ -import os -import sys -sys.path.append(os.path.dirname(os.path.abspath(__file__))) -import azlmbr.bus as bus -import azlmbr.legacy.general as general -import azlmbr.editor as editor -import azlmbr.math as math -import azlmbr.paths - -sys.path.append(os.path.join(azlmbr.paths.devroot, 'AutomatedTesting', 'Gem', 'PythonTests')) -import editor_python_test_tools.hydra_editor_utils as hydra -from editor_python_test_tools.editor_test_helper import EditorTestHelper -from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg +class Tests: + initial_instance_count = ( + "Initial instance count is as expected", + "Found an unexpected number of initial instances" + ) + autosnap_enabled_instance_count = ( + "Found the expected number of instances with Auto Snap to Surface enabled", + "Found an unexpected number of instances with Auto Snap to Surface enabled" + ) + autosnap_disabled_instance_count = ( + "Found the expected number of instances with Auto Snap to Surface disabled", + "Found an unexpected number of instances with Auto Snap to Surface disabled" + ) -class TestPositionModifierAutoSnapToSurface(EditorTestHelper): - def __init__(self): - EditorTestHelper.__init__(self, log_prefix="PositionModifier_AutoSnapToSurface", args=["level"]) +def PositionModifier_AutoSnapToSurfaceWorks(): + """ + Summary: + Instance spawner is setup to plant on a spherical mesh. Offsets are set on the x-axis, and checks are performed + to ensure instances plant where expected depending on the toggle setting. - def run_test(self): - """ - Summary: - Instance spawner is setup to plant on a spherical mesh. Offsets are set on the x-axis, and checks are performed - to ensure instances plant where expected depending on the toggle setting. + Expected Behavior: + Offset instances snap to the expected surface when Auto Snap to Surface is enabled, and offset away from surface + when it is disabled. - Expected Behavior: - Offset instances snap to the expected surface when Auto Snap to Surface is enabled, and offset away from surface - when it is disabled. + Test Steps: + 1) Open a simple level + 2) Create a new entity with required vegetation area components and a Position Modifier + 3) Create a spherical planting surface + 4) Verify initial instance counts pre-filter + 5) Create a child entity of the spawner entity with a Constant Gradient component and pin to spawner + 6) Set the Position Modifier offset to 5 on the x-axis + 7) Validate instance counts on top of and inside the sphere mesh with Auto Snap to Surface enabled + 8) Validate instance counts on top of and inside the sphere mesh with Auto Snap to Surface disabled - Test Steps: - 1) Create a new, temporary level - 2) Create a new entity with required vegetation area components and a Position Modifier - 3) Create a spherical planting surface - 4) Verify initial instance counts pre-filter - 5) Create a child entity of the spawner entity with a Constant Gradient component and pin to spawner - 6) Set the Position Modifier offset to 5 on the x-axis - 7) Validate instance counts on top of and inside the sphere mesh with Auto Snap to Surface enabled - 8) Validate instance counts on top of and inside the sphere mesh with Auto Snap to Surface disabled + Note: + - This test file must be called from the Open 3D Engine Editor command terminal + - Any passed and failed tests are written to the Editor.log file. + Parsing the file or running a log_monitor are required to observe the test results. - Note: - - This test file must be called from the Open 3D Engine Editor command terminal - - Any passed and failed tests are written to the Editor.log file. - Parsing the file or running a log_monitor are required to observe the test results. + :return: None + """ - :return: None - """ - position_modifier_paths = ['Configuration|Position X|Range Min', 'Configuration|Position X|Range Max', - 'Configuration|Position Y|Range Min', 'Configuration|Position Y|Range Max', - 'Configuration|Position Z|Range Min', 'Configuration|Position Z|Range Max'] + import os - # 1) Create a new, temporary level - self.test_success = self.create_level( - self.args["level"], - heightmap_resolution=1024, - heightmap_meters_per_pixel=1, - terrain_texture_resolution=4096, - use_terrain=False, - ) + import azlmbr.legacy.general as general + import azlmbr.math as math - # Set view of planting area for visual debugging - general.set_current_view_position(512.0, 500.0, 38.0) - general.set_current_view_rotation(-20.0, 0.0, 0.0) + import editor_python_test_tools.hydra_editor_utils as hydra + from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg + from editor_python_test_tools.utils import Report + from editor_python_test_tools.utils import TestHelper as helper - # 2) Create a new entity with required vegetation area components and a Position Modifier - spawner_center_point = math.Vector3(512.0, 512.0, 32.0) - asset_path = os.path.join("Slices", "PinkFlower.dynamicslice") - spawner_entity = dynveg.create_vegetation_area("Instance Spawner", spawner_center_point, 16.0, 16.0, 16.0, - asset_path) + position_modifier_paths = ['Configuration|Position X|Range Min', 'Configuration|Position X|Range Max', + 'Configuration|Position Y|Range Min', 'Configuration|Position Y|Range Max', + 'Configuration|Position Z|Range Min', 'Configuration|Position Z|Range Max'] - # Add a Vegetation Position Modifier and set offset values to 0 - spawner_entity.add_component("Vegetation Position Modifier") - for path in position_modifier_paths: - spawner_entity.get_set_test(3, path, 0) + # 1) Open an existing simple level + helper.init_idle() + helper.open_level("Physics", "Base") - # 3) Create a spherical planting surface and a flat surface - flat_entity = dynveg.create_surface_entity("Flat Surface", spawner_center_point, 32.0, 32.0, 1.0) - hill_entity = dynveg.create_mesh_surface_entity_with_slopes("Planting Surface", spawner_center_point, 5.0) + # Set view of planting area for visual debugging + general.set_current_view_position(512.0, 500.0, 38.0) + general.set_current_view_rotation(-20.0, 0.0, 0.0) - # Disable the Flat Surface Box Shape component, and temporarily ignore initial instance counts due to LYN-2245 - editor.EditorComponentAPIBus(bus.Broadcast, 'DisableComponents', [flat_entity.components[0]]) - """ - # 4) Verify initial instance counts pre-filter - num_expected = 121 - spawner_success = self.wait_for_condition( - lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, num_expected), 5.0) - self.test_success = self.test_success and spawner_success - """ + # 2) Create a new entity with required vegetation area components and a Position Modifier + spawner_center_point = math.Vector3(512.0, 512.0, 32.0) + asset_path = os.path.join("Slices", "PinkFlower.dynamicslice") + spawner_entity = dynveg.create_vegetation_area("Instance Spawner", spawner_center_point, 16.0, 16.0, 16.0, + asset_path) - # 5) Create a child entity of the spawner entity with a Constant Gradient component and pin to spawner - components_to_add = ["Constant Gradient"] - gradient_entity = hydra.Entity("Gradient Entity") - gradient_entity.create_entity(spawner_center_point, components_to_add, parent_id=spawner_entity.id) + # Add a Vegetation Position Modifier and set offset values to 0 + spawner_entity.add_component("Vegetation Position Modifier") + for path in position_modifier_paths: + spawner_entity.get_set_test(3, path, 0) - # Pin the Constant Gradient to the X axis of the spawner's Position Modifier component - spawner_entity.get_set_test(3, 'Configuration|Position X|Gradient|Gradient Entity Id', gradient_entity.id) + # 3) Create a spherical planting surface + hill_entity = dynveg.create_mesh_surface_entity_with_slopes("Planting Surface", spawner_center_point, 5.0) - # 6) Set the Position Modifier offset to 2.5 on the x-axis - spawner_entity.get_set_test(3, position_modifier_paths[0], 2.5) - spawner_entity.get_set_test(3, position_modifier_paths[1], 2.5) + # 4) Verify initial instance counts pre-filter + num_expected = 29 + spawner_success = helper.wait_for_condition( + lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, num_expected), 5.0) + Report.result(Tests.initial_instance_count, spawner_success) - # 7) Validate instance count at the top of the sphere mesh and inside the sphere mesh while Auto Snap to Surface - # is enabled - top_point = math.Vector3(512.0, 512.0, 37.0) - inside_point = math.Vector3(512.0, 512.0, 35.0) - radius = 0.5 - num_expected = 1 - self.log(f"Checking for instances in a {radius * 2}m area at {top_point.ToString()}") - top_success = self.wait_for_condition(lambda: dynveg.validate_instance_count(top_point, radius, num_expected), - 5.0) - self.test_success = top_success and self.test_success - num_expected = 0 - self.log(f"Checking for instances in a {radius * 2}m area at {inside_point.ToString()}") - inside_success = self.wait_for_condition(lambda: dynveg.validate_instance_count(inside_point, radius, - num_expected), 5.0) - self.test_success = inside_success and self.test_success + # 5) Create a child entity of the spawner entity with a Constant Gradient component and pin to spawner + components_to_add = ["Constant Gradient"] + gradient_entity = hydra.Entity("Gradient Entity") + gradient_entity.create_entity(spawner_center_point, components_to_add, parent_id=spawner_entity.id) - # 8) Toggle off Auto Snap to Surface. Instances should now plant inside the sphere and no longer on top - spawner_entity.get_set_test(3, "Configuration|Auto Snap To Surface", False) - num_expected = 0 - self.log(f"Checking for instances in a {radius * 2}m area at {top_point.ToString()}") - top_success = self.wait_for_condition(lambda: dynveg.validate_instance_count(top_point, radius, num_expected), - 5.0) - self.test_success = top_success and self.test_success - num_expected = 1 - self.log(f"Checking for instances in a {radius * 2}m area at {inside_point.ToString()}") - inside_success = self.wait_for_condition(lambda: dynveg.validate_instance_count(inside_point, radius, - num_expected), 5.0) - self.test_success = inside_success and self.test_success + # Pin the Constant Gradient to the X axis of the spawner's Position Modifier component + spawner_entity.get_set_test(3, 'Configuration|Position X|Gradient|Gradient Entity Id', gradient_entity.id) + + # 6) Set the Position Modifier offset to 2.5 on the x-axis + spawner_entity.get_set_test(3, position_modifier_paths[0], 2.5) + spawner_entity.get_set_test(3, position_modifier_paths[1], 2.5) + + # 7) Validate instance count at the top of the sphere mesh and inside the sphere mesh while Auto Snap to Surface + # is enabled + top_point = math.Vector3(512.0, 512.0, 37.0) + inside_point = math.Vector3(512.0, 512.0, 35.0) + radius = 0.5 + num_expected = 1 + Report.info(f"Checking for instances in a {radius * 2}m area at {top_point.ToString()}") + top_success = helper.wait_for_condition(lambda: dynveg.validate_instance_count(top_point, radius, num_expected), + 5.0) + num_expected = 0 + Report.info(f"Checking for instances in a {radius * 2}m area at {inside_point.ToString()}") + inside_success = helper.wait_for_condition(lambda: dynveg.validate_instance_count(inside_point, radius, + num_expected), 5.0) + Report.result(Tests.autosnap_enabled_instance_count, top_success and inside_success) + + # 8) Toggle off Auto Snap to Surface. Instances should now plant inside the sphere and no longer on top + spawner_entity.get_set_test(3, "Configuration|Auto Snap To Surface", False) + num_expected = 0 + Report.info(f"Checking for instances in a {radius * 2}m area at {top_point.ToString()}") + top_success = helper.wait_for_condition(lambda: dynveg.validate_instance_count(top_point, radius, num_expected), + 5.0) + num_expected = 1 + Report.info(f"Checking for instances in a {radius * 2}m area at {inside_point.ToString()}") + inside_success = helper.wait_for_condition(lambda: dynveg.validate_instance_count(inside_point, radius, + num_expected), 5.0) + Report.result(Tests.autosnap_disabled_instance_count, top_success and inside_success) -test = TestPositionModifierAutoSnapToSurface() -test.run() +if __name__ == "__main__": + + from editor_python_test_tools.utils import Report + Report.start_test(PositionModifier_AutoSnapToSurfaceWorks) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/PositionModifier_ComponentAndOverrides_InstancesPlantAtSpecifiedOffsets.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/PositionModifier_ComponentAndOverrides_InstancesPlantAtSpecifiedOffsets.py index 9597f79339..c4fa91f886 100755 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/PositionModifier_ComponentAndOverrides_InstancesPlantAtSpecifiedOffsets.py +++ b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/PositionModifier_ComponentAndOverrides_InstancesPlantAtSpecifiedOffsets.py @@ -5,159 +5,164 @@ For complete copyright and license terms please see the LICENSE at the root of t SPDX-License-Identifier: Apache-2.0 OR MIT """ -import os -import random -import sys -sys.path.append(os.path.dirname(os.path.abspath(__file__))) -import azlmbr.editor as editor -import azlmbr.legacy.general as general -import azlmbr.bus as bus -import azlmbr.math as math -import azlmbr.paths - -sys.path.append(os.path.join(azlmbr.paths.devroot, 'AutomatedTesting', 'Gem', 'PythonTests')) -import editor_python_test_tools.hydra_editor_utils as hydra -from editor_python_test_tools.editor_test_helper import EditorTestHelper -from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg +class Tests: + initial_instance_count = ( + "Initial instance count is as expected", + "Found an unexpected number of initial instances" + ) + position_offset = ( + "Found instances at all expected locations with Position Modifier offsets configured", + "Failed to find all expected instances at all locations with Position Modifier offsets configured" + ) + position_offset_overrides = ( + "Found instances at all expected locations with Position Modifier offset overrides configured", + "Failed to find all expected instances at all locations with Position Modifier offset overrides configured" + ) -class TestPositionModifierComponentAndOverrides(EditorTestHelper): - def __init__(self): - EditorTestHelper.__init__(self, log_prefix="PositionModifierComponentAndOverrides_InstanceOffset", args=["level"]) +def PositionModifier_ComponentAndOverrides_InstancesPlantAtSpecifiedOffsets(): + """ + Summary: Range Min/Max in the Vegetation Position Modifier component and component overrides can be set for all + axes, and functions as expected when fed a gradient signal. - def run_test(self): - """ - Summary: Range Min/Max in the Vegetation Position Modifier component and component overrides can be set for all - axes, and functions as expected when fed a gradient signal. + Expected Behavior: Instances are offset by the specified amount. - Expected Behavior: Instances are offset by the specified amount. + Test Steps: + 1) Open an existing level + 2) Spawner area is setup with all necessary components + 3) Surface for planting is created + 4) Initial instance count validation pre-filter is performed + 5) An entity with a Constant Gradient of 1 is added as a child to the spawner entity, and pinned to the Position + Modifier Gradient Entity Id fields + 6) Sector size is adjusted on a Vegetation System Settings component to allow for offset instances to not fall + outside of the queried sector + 7) Random offsets are set for each axis of the Position Modifier component, and instance counts are validated + 8) Overrides are enabled on the Position Modifier component + 9) Random offsets are set for each axis of the descriptor's Position Modifier overrides, and instance counts + are validated - Test Steps: - 1) New test level is created - 2) Spawner area is setup with all necessary components - 3) Surface for planting is created - 4) Initial instance count validation pre-filter is performed - 5) An entity with a Constant Gradient of 1 is added as a child to the spawner entity, and pinned to the Position - Modifier Gradient Entity Id fields - 6) Sector size is adjusted on a Vegetation System Settings component to allow for offset instances to not fall - outside of the queried sector - 7) Random offsets are set for each axis of the Position Modifier component, and instance counts are validated - 8) Overrides are enabled on the Position Modifier component - 9) Random offsets are set for each axis of the descriptor's Position Modifier overrides, and instance counts - are validated + Note: + - This test file must be called from the Open 3D Engine Editor command terminal + - Any passed and failed tests are written to the Editor.log file. + Parsing the file or running a log_monitor are required to observe the test results. - Note: - - This test file must be called from the Open 3D Engine Editor command terminal - - Any passed and failed tests are written to the Editor.log file. - Parsing the file or running a log_monitor are required to observe the test results. + :return: None + """ - :return: None - """ - position_modifier_paths = ['Configuration|Position X|Range Min', 'Configuration|Position X|Range Max', - 'Configuration|Position Y|Range Min', 'Configuration|Position Y|Range Max', - 'Configuration|Position Z|Range Min', 'Configuration|Position Z|Range Max'] + import os + import random - override_position_modifier_paths = ['Configuration|Embedded Assets|[0]|Position Modifier|Min X', - 'Configuration|Embedded Assets|[0]|Position Modifier|Max X', - 'Configuration|Embedded Assets|[0]|Position Modifier|Min Y', - 'Configuration|Embedded Assets|[0]|Position Modifier|Max Y', - 'Configuration|Embedded Assets|[0]|Position Modifier|Min Z', - 'Configuration|Embedded Assets|[0]|Position Modifier|Max Z'] + import azlmbr.editor as editor + import azlmbr.legacy.general as general + import azlmbr.bus as bus + import azlmbr.math as math - def generate_random_offset_list(): - offset_list = [] - while len(offset_list) < 10: - offset = round(random.uniform(-8.0, 8.0), 2) - if not -1.0 <= offset <= 1.0: - offset_list.append(offset) - print("List of values to test against = " + str(offset_list)) - return offset_list + import editor_python_test_tools.hydra_editor_utils as hydra + from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg + from editor_python_test_tools.utils import Report + from editor_python_test_tools.utils import TestHelper as helper - def set_offset_and_verify_instance_counts(offset_to_test, center, is_override=False): - print(f"Starting test with an offset of {offset_to_test}") - # Set min/max values to the offset value - if not is_override: - for path in position_modifier_paths: - spawner_entity.get_set_test(3, path, offset_to_test) - else: - spawner_entity.get_set_test(2, "Configuration|Embedded Assets|[0]|Position Modifier|Override Enabled", - True) - for path in override_position_modifier_paths: - spawner_entity.get_set_test(2, path, offset_to_test) - center_point = math.Vector3(center.x + offset_to_test, center.y + offset_to_test, center.z + offset_to_test) - radius = 0.5 - print(f"Querying for instances in a {radius * 2}m area around {center_point.ToString()}") - offset_success = self.wait_for_condition(lambda: dynveg.validate_instance_count(center_point, radius, 1), 5.0) - offset_success2 = self.wait_for_condition(lambda: dynveg.validate_instance_count(center, radius, 0), 5.0) - self.test_success = offset_success and offset_success2 and self.test_success + position_modifier_paths = ['Configuration|Position X|Range Min', 'Configuration|Position X|Range Max', + 'Configuration|Position Y|Range Min', 'Configuration|Position Y|Range Max', + 'Configuration|Position Z|Range Min', 'Configuration|Position Z|Range Max'] - # 1) Create a new, temporary level - self.test_success = self.create_level( - self.args["level"], - heightmap_resolution=1024, - heightmap_meters_per_pixel=1, - terrain_texture_resolution=4096, - use_terrain=False, - ) + override_position_modifier_paths = ['Configuration|Embedded Assets|[0]|Position Modifier|Min X', + 'Configuration|Embedded Assets|[0]|Position Modifier|Max X', + 'Configuration|Embedded Assets|[0]|Position Modifier|Min Y', + 'Configuration|Embedded Assets|[0]|Position Modifier|Max Y', + 'Configuration|Embedded Assets|[0]|Position Modifier|Min Z', + 'Configuration|Embedded Assets|[0]|Position Modifier|Max Z'] - # Set view of planting area for visual debugging - general.set_current_view_position(16.0, -5.0, 32.0) + def generate_random_offset_list(): + offset_list = [] + while len(offset_list) < 10: + offset = round(random.uniform(-8.0, 8.0), 2) + if not -1.0 <= offset <= 1.0: + offset_list.append(offset) + Report.info("List of values to test against = " + str(offset_list)) + return offset_list - # 2) Create a new entity with required vegetation area components - spawner_center_point = math.Vector3(16.0, 16.0, 32.0) - asset_path = os.path.join("Slices", "PinkFlower.dynamicslice") - spawner_entity = dynveg.create_vegetation_area("Instance Spawner", spawner_center_point, 1.0, 1.0, 1.0, asset_path) + def set_offset_and_verify_instance_counts(offset_to_test, center, is_override=False): + Report.info(f"Starting test with an offset of {offset_to_test}") + # Set min/max values to the offset value + if not is_override: + for path in position_modifier_paths: + spawner_entity.get_set_test(3, path, offset_to_test) + else: + spawner_entity.get_set_test(2, "Configuration|Embedded Assets|[0]|Position Modifier|Override Enabled", + True) + for path in override_position_modifier_paths: + spawner_entity.get_set_test(2, path, offset_to_test) + center_point = math.Vector3(center.x + offset_to_test, center.y + offset_to_test, center.z + offset_to_test) + radius = 0.5 + Report.info(f"Querying for instances in a {radius * 2}m area around {center_point.ToString()}") + offset_success = helper.wait_for_condition(lambda: dynveg.validate_instance_count(center_point, radius, 1), 5.0) + offset_success2 = helper.wait_for_condition(lambda: dynveg.validate_instance_count(center, radius, 0), 5.0) + return offset_success and offset_success2 - # Add a Vegetation Position Modifier and set offset values to 0 - spawner_entity.add_component("Vegetation Position Modifier") - for path in position_modifier_paths: - spawner_entity.get_set_test(3, path, 0) + # 1) Open an existing simple level + helper.init_idle() + helper.open_level("Physics", "Base") - # 3) Add flat surface to plant on - dynveg.create_surface_entity("Planting Surface", spawner_center_point, 32.0, 32.0, 0.0) + # Set view of planting area for visual debugging + general.set_current_view_position(16.0, -5.0, 32.0) - # 4) Verify initial instance counts pre-filter - num_expected = 1 # Single instance planted - spawner_success = self.wait_for_condition( - lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, num_expected), 5.0) - self.test_success = self.test_success and spawner_success + # 2) Create a new entity with required vegetation area components + spawner_center_point = math.Vector3(16.0, 16.0, 32.0) + asset_path = os.path.join("Slices", "PinkFlower.dynamicslice") + spawner_entity = dynveg.create_vegetation_area("Instance Spawner", spawner_center_point, 1.0, 1.0, 1.0, asset_path) - # 5) Create a child entity of the spawner entity with a Constant Gradient component - components_to_add = ["Constant Gradient"] - gradient_entity = hydra.Entity("Gradient Entity") - gradient_entity.create_entity(spawner_center_point, components_to_add, parent_id=spawner_entity.id) + # Add a Vegetation Position Modifier and set offset values to 0 + spawner_entity.add_component("Vegetation Position Modifier") + for path in position_modifier_paths: + spawner_entity.get_set_test(3, path, 0) - # Pin the Constant Gradient to each axis of the Position Modifier - position_modifier_gradient_paths = ['Configuration|Position X|Gradient|Gradient Entity Id', - 'Configuration|Position Y|Gradient|Gradient Entity Id', - 'Configuration|Position Z|Gradient|Gradient Entity Id'] - for path in position_modifier_gradient_paths: - spawner_entity.get_set_test(3, path, gradient_entity.id) + # 3) Add flat surface to plant on + dynveg.create_surface_entity("Planting Surface", spawner_center_point, 32.0, 32.0, 0.0) - # 6) Add a Vegetation System Settings Level component and change sector size to 32 sq meters so instances can - # offset to a greater range and still be validated - veg_system_settings_component = hydra.add_level_component("Vegetation System Settings") - editor.EditorComponentAPIBus(bus.Broadcast, "SetComponentProperty", veg_system_settings_component, - "Configuration|Area System Settings|Sector Size In Meters", 32) - sector_size = hydra.get_component_property_value(veg_system_settings_component, - "Configuration|Area System Settings|Sector Size In Meters") - self.test_success = (sector_size == 32) and self.test_success + # 4) Verify initial instance counts pre-filter + num_expected = 1 # Single instance planted + spawner_success = helper.wait_for_condition( + lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, num_expected), 5.0) + Report.result(Tests.initial_instance_count, spawner_success) - # 7) Set offsets on all axes and verify instance counts - offsets_to_test = generate_random_offset_list() - for offset in offsets_to_test: - if self.test_success: - set_offset_and_verify_instance_counts(offset, spawner_center_point) + # 5) Create a child entity of the spawner entity with a Constant Gradient component + components_to_add = ["Constant Gradient"] + gradient_entity = hydra.Entity("Gradient Entity") + gradient_entity.create_entity(spawner_center_point, components_to_add, parent_id=spawner_entity.id) - # 8) Toggle on allow overrides on the Position Modifier Component - spawner_entity.get_set_test(3, "Configuration|Allow Per-Item Overrides", True) + # Pin the Constant Gradient to each axis of the Position Modifier + position_modifier_gradient_paths = ['Configuration|Position X|Gradient|Gradient Entity Id', + 'Configuration|Position Y|Gradient|Gradient Entity Id', + 'Configuration|Position Z|Gradient|Gradient Entity Id'] + for path in position_modifier_gradient_paths: + spawner_entity.get_set_test(3, path, gradient_entity.id) - # 9) Set offsets on all axes on descriptor overrides and verify instance counts - for offset in offsets_to_test: - if self.test_success: - set_offset_and_verify_instance_counts(offset, spawner_center_point, is_override=True) + # 6) Add a Vegetation System Settings Level component and change sector size to 32 sq meters so instances can + # offset to a greater range and still be validated + veg_system_settings_component = hydra.add_level_component("Vegetation System Settings") + editor.EditorComponentAPIBus(bus.Broadcast, "SetComponentProperty", veg_system_settings_component, + "Configuration|Area System Settings|Sector Size In Meters", 32) + + # 7) Set offsets on all axes and verify instance counts + offsets_to_test = generate_random_offset_list() + success = True + for offset in offsets_to_test: + success = success and set_offset_and_verify_instance_counts(offset, spawner_center_point) + Report.result(Tests.position_offset, success) + + # 8) Toggle on allow overrides on the Position Modifier Component + spawner_entity.get_set_test(3, "Configuration|Allow Per-Item Overrides", True) + + # 9) Set offsets on all axes on descriptor overrides and verify instance counts + success = True + for offset in offsets_to_test: + success = success and set_offset_and_verify_instance_counts(offset, spawner_center_point, is_override=True) + Report.result(Tests.position_offset_overrides, success) -test = TestPositionModifierComponentAndOverrides() -test.run() +if __name__ == "__main__": + + from editor_python_test_tools.utils import Report + Report.start_test(PositionModifier_ComponentAndOverrides_InstancesPlantAtSpecifiedOffsets) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/RotationModifierOverrides_InstancesRotateWithinRange.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/RotationModifierOverrides_InstancesRotateWithinRange.py index b428459032..4d8019bb33 100755 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/RotationModifierOverrides_InstancesRotateWithinRange.py +++ b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/RotationModifierOverrides_InstancesRotateWithinRange.py @@ -5,139 +5,138 @@ For complete copyright and license terms please see the LICENSE at the root of t SPDX-License-Identifier: Apache-2.0 OR MIT """ -""" -C4814460: A level with simple vegetation is created. A child entity with required components is then created, -and pinned to the gradient entity id in Z direction for vegetation entity. The changes in vegetation area are observed. -""" -import os -import sys - -sys.path.append(os.path.dirname(os.path.abspath(__file__))) -import azlmbr.legacy.general as general -import azlmbr.math as math -import azlmbr.bus as bus -import azlmbr.areasystem as areasystem - -sys.path.append(os.path.join(azlmbr.paths.devroot, 'AutomatedTesting', 'Gem', 'PythonTests')) -import editor_python_test_tools.hydra_editor_utils as hydra -from editor_python_test_tools.editor_test_helper import EditorTestHelper -from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg +class Tests: + gradient_entity_created = ( + "Successfully created new Gradient entity", + "Failed to create Gradient entity" + ) + non_override_rotation_check = ( + "Instances are rotated at expected values after initial setup", + "Found unexpectedly rotated instances after initial setup" + ) + override_rotation_check = ( + "Instances are rotated at expected values after configuring overrides", + "Found unexpectedly rotated instances after configuring overrides" + ) -class TestRotationModifierOverrides_InstancesRotateWithinRange(EditorTestHelper): - def __init__(self): - EditorTestHelper.__init__(self, log_prefix="RotationModifierOverrides_InstancesRotateWithinRange", args=["level"]) +def RotationModifierOverrides_InstancesRotateWithinRange(): + """ + Summary: + A level with simple vegetation is created. A child entity with required components is then created, + and pinned to the gradient entity id in Z direction for vegetation entity. The changes in vegetation + area are observed. - def run_test(self): - """ - Summary: - A level with simple vegetation is created. A child entity with required components is then created, - and pinned to the gradient entity id in Z direction for vegetation entity. The changes in vegetation - area are observed. + Expected Behavior: + Vegetation instances all rotate randomly between 0-360 degrees on the Z-axis. - Expected Behavior: - Vegetation instances all rotate randomly between 0-360 degrees on the Z-axis. + Test Steps: + 1) Open a new level + 2) Create vegetation entity and add components + 3) Set properties for vegetation entity + 4) Create new child entity + 5) Pin the child entity to vegetation entity as gradient entity id + 6) Verify rotation without per-item overrides + 7) Verify rotation with per-item overrides - Test Steps: - 1) Create level - 2) Create vegetation entity and add components - 3) Set properties for vegetation entity - 4) Create new child entity - 5) Pin the child entity to vegetation entity as gradient entity id - 6) Verify rotation without per-item overrides - 7) Verify rotation with per-item overrides + Note: + - Any passed and failed tests are written to the Editor.log file. + Parsing the file or running a log_monitor are required to observe the test results. - Note: - - Any passed and failed tests are written to the Editor.log file. - Parsing the file or running a log_monitor are required to observe the test results. + :return: None + """ - :return: None - """ + import os - def get_expected_rotation(min, max, gradient_value): - return min + ((max - min) * gradient_value) + import azlmbr.legacy.general as general + import azlmbr.math as math + import azlmbr.bus as bus + import azlmbr.areasystem as areasystem - def validate_rotation(center, radius, num_expected, rot_degrees_vector): - # Verify that every instance in the given area has the expected rotation. - box = math.Aabb_CreateCenterRadius(center, radius) - instances = areasystem.AreaSystemRequestBus(bus.Broadcast, 'GetInstancesInAabb', box) - num_found = len(instances) - num_validated = 0 - result = (num_found == num_expected) - print(f'instance count validation: {result} (found={num_found}, expected={num_expected})') - expected_rotation = math.Quaternion() - expected_rotation.SetFromEulerDegrees(rot_degrees_vector) - for instance in instances: - is_close = instance.rotation.IsClose(expected_rotation) - result = result and is_close - if is_close: - num_validated = num_validated + 1 - #else: - # print(f'instance rotation validation: {is_close} (rotation={instance.rotation} expected={expected_rotation})') - print(f'instance rotation validation: {result} (num_validated={num_validated})') - return result + import editor_python_test_tools.hydra_editor_utils as hydra + from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg + from editor_python_test_tools.utils import Report + from editor_python_test_tools.utils import TestHelper as helper - # 1) Create level - self.test_success = self.create_level( - self.args["level"], - heightmap_resolution=1024, - heightmap_meters_per_pixel=1, - terrain_texture_resolution=4096, - use_terrain=False, - ) - general.set_current_view_position(512.0, 480.0, 38.0) + def get_expected_rotation(min, max, gradient_value): + return min + ((max - min) * gradient_value) - # 2) Create vegetation entity and add components - entity_position = math.Vector3(512.0, 512.0, 32.0) - asset_path = os.path.join("Slices", "PurpleFlower.dynamicslice") - spawner_entity = dynveg.create_vegetation_area("Spawner Entity", entity_position, 16.0, 16.0, 16.0, asset_path) - spawner_entity.add_component("Vegetation Rotation Modifier") - # Our default vegetation settings places 20 instances per 16 meters, so we expect 20 * 20 total instances. - num_expected = 20 * 20 - # This is technically twice as big as we need, but we want to make sure our query radius is large enough to discover every - # instance we've created. - area_radius = 16.0 + def validate_rotation(center, radius, num_expected, rot_degrees_vector): + # Verify that every instance in the given area has the expected rotation. + box = math.Aabb_CreateCenterRadius(center, radius) + instances = areasystem.AreaSystemRequestBus(bus.Broadcast, 'GetInstancesInAabb', box) + num_found = len(instances) + num_validated = 0 + result = (num_found == num_expected) + Report.info(f'instance count validation: {result} (found={num_found}, expected={num_expected})') + expected_rotation = math.Quaternion() + expected_rotation.SetFromEulerDegrees(rot_degrees_vector) + for instance in instances: + is_close = instance.rotation.IsClose(expected_rotation) + result = result and is_close + if is_close: + num_validated = num_validated + 1 + Report.info(f'instance rotation validation: {result} (num_validated={num_validated})') + return result - # Create surface to spawn on - dynveg.create_surface_entity("Surface Entity", entity_position, 16.0, 16.0, 1.0) + # 1) Open an existing simple level + helper.init_idle() + helper.open_level("Physics", "Base") + general.set_current_view_position(512.0, 480.0, 38.0) - # 3) Set properties for the rotation override on the descriptor, but don't set "allow overrides" on the rotation modifier yet. - spawner_entity.get_set_test(2, "Configuration|Embedded Assets|[0]|Rotation Modifier|Override Enabled", True) - spawner_entity.get_set_test(2, "Configuration|Embedded Assets|[0]|Rotation Modifier|Min Z", -70.0) - spawner_entity.get_set_test(2, "Configuration|Embedded Assets|[0]|Rotation Modifier|Max Z", 30.0) + # 2) Create vegetation entity and add components + entity_position = math.Vector3(512.0, 512.0, 32.0) + asset_path = os.path.join("Slices", "PurpleFlower.dynamicslice") + spawner_entity = dynveg.create_vegetation_area("Spawner Entity", entity_position, 16.0, 16.0, 16.0, asset_path) + spawner_entity.add_component("Vegetation Rotation Modifier") + # Our default vegetation settings places 20 instances per 16 meters, so we expect 20 * 20 total instances. + num_expected = 20 * 20 + # This is technically twice as big as we need, but we want to make sure our query radius is large enough to discover + # every instance we've created. + area_radius = 16.0 - # 4) Create new child entity with a constant gradient - constant_gradient_value = 0.25 - gradient_entity = hydra.Entity("Gradient Entity") - gradient_entity.create_entity( - entity_position, - ["Constant Gradient"], - parent_id=spawner_entity.id - ) - if gradient_entity.id.IsValid(): - self.log(f"'{gradient_entity.name}' created") - gradient_entity.get_set_test(0, "Configuration|Value", constant_gradient_value) + # Create surface to spawn on + dynveg.create_surface_entity("Surface Entity", entity_position, 16.0, 16.0, 1.0) - # 5) Pin the child entity to vegetation entity as gradient entity id - spawner_entity.get_set_test(3, "Configuration|Rotation Z|Gradient|Gradient Entity Id", gradient_entity.id) + # 3) Set properties for the rotation override on the descriptor, but don't set "allow overrides" on the rotation + # modifier yet + spawner_entity.get_set_test(2, "Configuration|Embedded Assets|[0]|Rotation Modifier|Override Enabled", True) + spawner_entity.get_set_test(2, "Configuration|Embedded Assets|[0]|Rotation Modifier|Min Z", -70.0) + spawner_entity.get_set_test(2, "Configuration|Embedded Assets|[0]|Rotation Modifier|Max Z", 30.0) - # 6) Verify that without per-item overrides, the rotation matches the one calculated from the default rotation range. - general.idle_wait(1.0) - rotation_degrees = get_expected_rotation(-180.0, 180.0, constant_gradient_value) - rotation_success = self.wait_for_condition( - lambda: validate_rotation(entity_position, area_radius, num_expected, math.Vector3(0.0, 0.0, rotation_degrees)), - 5.0) - self.test_success = self.test_success and rotation_success + # 4) Create new child entity with a constant gradient + constant_gradient_value = 0.25 + gradient_entity = hydra.Entity("Gradient Entity") + gradient_entity.create_entity( + entity_position, + ["Constant Gradient"], + parent_id=spawner_entity.id + ) + Report.critical_result(Tests.gradient_entity_created, gradient_entity.id.IsValid()) + gradient_entity.get_set_test(0, "Configuration|Value", constant_gradient_value) - # 7) Verify that with per-item overrides enabled, the rotation matches the one calculated from the override range. - spawner_entity.get_set_test(3, "Configuration|Allow Per-Item Overrides", True) - rotation_degrees = get_expected_rotation(-70.0, 30.0, constant_gradient_value) - rotation_success = self.wait_for_condition( - lambda: validate_rotation(entity_position, area_radius, num_expected, math.Vector3(0.0, 0.0, rotation_degrees)), - 5.0) - self.test_success = self.test_success and rotation_success + # 5) Pin the child entity to vegetation entity as gradient entity id + spawner_entity.get_set_test(3, "Configuration|Rotation Z|Gradient|Gradient Entity Id", gradient_entity.id) + + # 6) Verify that without per-item overrides, the rotation matches the one calculated from the default rotation range + general.idle_wait(1.0) + rotation_degrees = get_expected_rotation(-180.0, 180.0, constant_gradient_value) + rotation_success = helper.wait_for_condition( + lambda: validate_rotation(entity_position, area_radius, num_expected, math.Vector3(0.0, 0.0, rotation_degrees)), + 5.0) + Report.result(Tests.non_override_rotation_check, rotation_success) + + # 7) Verify that with per-item overrides enabled, the rotation matches the one calculated from the override range. + spawner_entity.get_set_test(3, "Configuration|Allow Per-Item Overrides", True) + rotation_degrees = get_expected_rotation(-70.0, 30.0, constant_gradient_value) + rotation_success = helper.wait_for_condition( + lambda: validate_rotation(entity_position, area_radius, num_expected, math.Vector3(0.0, 0.0, rotation_degrees)), + 5.0) + Report.result(Tests.override_rotation_check, rotation_success) -test = TestRotationModifierOverrides_InstancesRotateWithinRange() -test.run() +if __name__ == "__main__": + + from editor_python_test_tools.utils import Report + Report.start_test(RotationModifierOverrides_InstancesRotateWithinRange) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/RotationModifier_InstancesRotateWithinRange.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/RotationModifier_InstancesRotateWithinRange.py index 6b6f66467b..c261415958 100755 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/RotationModifier_InstancesRotateWithinRange.py +++ b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/RotationModifier_InstancesRotateWithinRange.py @@ -5,207 +5,226 @@ For complete copyright and license terms please see the LICENSE at the root of t SPDX-License-Identifier: Apache-2.0 OR MIT """ -import os -import sys -sys.path.append(os.path.dirname(os.path.abspath(__file__))) -import azlmbr.math as math -import azlmbr.legacy.general as general -import azlmbr.bus as bus -import azlmbr.areasystem as areasystem - -sys.path.append(os.path.join(azlmbr.paths.devroot, 'AutomatedTesting', 'Gem', 'PythonTests')) -import editor_python_test_tools.hydra_editor_utils as hydra -from editor_python_test_tools.editor_test_helper import EditorTestHelper -from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg +class Tests: + gradient_entity_created = ( + "Successfully created new Gradient entity", + "Failed to create Gradient entity" + ) + rotation_baseline = ( + "Instances are not rotated after initial setup", + "Unexpectedly found rotated instances after initial setup" + ) + x_axis_rotation_180 = ( + "Instances rotated 180 degrees on X-axis as expected", + "Found unexpected instance rotation on X-axis" + ) + x_axis_rotation_90 = ( + "Instances rotated 90 degrees on X-axis as expected", + "Found unexpected instance rotation on X-axis" + ) + y_axis_rotation_180 = ( + "Instances rotated 180 degrees on Y-axis as expected", + "Found unexpected instance rotation on Y-axis" + ) + y_axis_rotation_90 = ( + "Instances rotated 90 degrees on Y-axis as expected", + "Found unexpected instance rotation on Y-axis" + ) + z_axis_rotation_180 = ( + "Instances rotated 180 degrees on Z-axis as expected", + "Found unexpected instance rotation on Z-axis" + ) + z_axis_rotation_90 = ( + "Instances rotated 90 degrees on Z-axis as expected", + "Found unexpected instance rotation on Z-axis" + ) -class TestRotationModifier_InstancesRotateWithinRange(EditorTestHelper): - def __init__(self): - EditorTestHelper.__init__(self, log_prefix="RotationModifier_InstancesRotateWithinRange", args=["level"]) +def RotationModifier_InstancesRotateWithinRange(): + """ + Summary: Range Min/Max in the Vegetation Rotation Modifier component can be set for all axes, + and functions as expected when fed a gradient signal - def run_test(self): - """ - Summary: Range Min/Max in the Vegetation Rotation Modifier component can be set for all axes, - and functions as expected when fed a gradient signal - - Vegetation Entity: Set in the middle of the level it holds a child entity and the following components: - Vegetation Asset List - Box Shape (size: <10, 10, 10>) - Vegetation Layer Spawner - Vegetation Rotation Modifier - Rotation X (gradient: child, Range Min: Variable, Range Max: Variable) - Rotation Y (gradient: child, Range Min: Variable, Range Max: Variable) - Rotation Z (gradient: child, Range Min: Variable, Range Max: Variable) + Vegetation Entity: Set in the middle of the level it holds a child entity and the following components: + Vegetation Asset List + Box Shape (size: <10, 10, 10>) + Vegetation Layer Spawner + Vegetation Rotation Modifier + Rotation X (gradient: child, Range Min: Variable, Range Max: Variable) + Rotation Y (gradient: child, Range Min: Variable, Range Max: Variable) + Rotation Z (gradient: child, Range Min: Variable, Range Max: Variable) - Child Entity: Child to Vegetation Entity has the following components: - Box Shape (size: <10, 10, 10>) - Gradient Transform Modifier - Constant Gradient + Child Entity: Child to Vegetation Entity has the following components: + Box Shape (size: <10, 10, 10>) + Gradient Transform Modifier + Constant Gradient - Expected Behavior: The vegetation area adjusts rotation based on the Constant Gradient component - and the min and max values for each component. Min max of each axis is checked + Expected Behavior: The vegetation area adjusts rotation based on the Constant Gradient component + and the min and max values for each component. Min max of each axis is checked - Test Steps: - 1) Create level - 2) Set up vegetation entities - 3) X-axis Check - 4) Y-axis Check - 5) Z-axis Check + Test Steps: + 1) Open a new level + 2) Set up vegetation entities + 3) X-axis Check + 4) Y-axis Check + 5) Z-axis Check - Note: - - Any passed and failed tests are written to the Editor.log file. - Parsing the file or running a log_monitor are required to observe the test results. + Note: + - Any passed and failed tests are written to the Editor.log file. + Parsing the file or running a log_monitor are required to observe the test results. - :return: None - """ + :return: None + """ - # Test Constants - LEVEL_CENTER = math.Vector3(512.0, 512.0, 32.0) - constant_gradient_value = 0.15 + import os - # Helper Functions - def change_range_max(axis, value): - spawner_entity.get_set_test(3, f"Configuration|Rotation {axis}|Range Max", value) + import azlmbr.math as math + import azlmbr.legacy.general as general + import azlmbr.bus as bus + import azlmbr.areasystem as areasystem - def change_range_min(axis, value): - spawner_entity.get_set_test(3, f"Configuration|Rotation {axis}|Range Min", value) + import editor_python_test_tools.hydra_editor_utils as hydra + from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg + from editor_python_test_tools.utils import Report + from editor_python_test_tools.utils import TestHelper as helper - def get_expected_rotation(min, max, gradient_value): - return min + ((max - min) * gradient_value) + # Test Constants + LEVEL_CENTER = math.Vector3(512.0, 512.0, 32.0) + constant_gradient_value = 0.15 - def validate_rotation(center, radius, num_expected, rot_degrees_vector): - # Verify that every instance in the given area has the expected rotation. - box = math.Aabb_CreateCenterRadius(center, radius) - instances = areasystem.AreaSystemRequestBus(bus.Broadcast, 'GetInstancesInAabb', box) - num_found = len(instances) - result = (num_found == num_expected) - print(f'instance count validation: {result} (found={num_found}, expected={num_expected})') - expected_rotation = math.Quaternion() - expected_rotation.SetFromEulerDegrees(rot_degrees_vector) - for instance in instances: - result = result and instance.rotation.IsClose(expected_rotation) - print(f'instance rotation validation: {result} (rotation={instance.rotation} expected={expected_rotation})') - return result + # Helper Functions + def change_range_max(axis, value): + spawner_entity.get_set_test(3, f"Configuration|Rotation {axis}|Range Max", value) + def change_range_min(axis, value): + spawner_entity.get_set_test(3, f"Configuration|Rotation {axis}|Range Min", value) - # Main Script - # 1) Create Level - self.test_success = self.create_level( - self.args["level"], - heightmap_resolution=1024, - heightmap_meters_per_pixel=1, - terrain_texture_resolution=4096, - use_terrain=False, + def get_expected_rotation(min, max, gradient_value): + return min + ((max - min) * gradient_value) + + def validate_rotation(center, radius, num_expected, rot_degrees_vector): + # Verify that every instance in the given area has the expected rotation. + box = math.Aabb_CreateCenterRadius(center, radius) + instances = areasystem.AreaSystemRequestBus(bus.Broadcast, 'GetInstancesInAabb', box) + num_found = len(instances) + result = (num_found == num_expected) + Report.info(f'instance count validation: {result} (found={num_found}, expected={num_expected})') + expected_rotation = math.Quaternion() + expected_rotation.SetFromEulerDegrees(rot_degrees_vector) + for instance in instances: + result = result and instance.rotation.IsClose(expected_rotation) + Report.info(f'instance rotation validation: {result} (rotation={instance.rotation} expected={expected_rotation})') + return result + + # Main Script + # 1) Open an existing simple level + helper.init_idle() + helper.open_level("Physics", "Base") + general.set_current_view_position(512.0, 480.0, 38.0) + + # 2) Set up vegetation entities + asset_path = os.path.join("Slices", "PurpleFlower.dynamicslice") + spawner_entity = dynveg.create_vegetation_area("Spawner Entity", LEVEL_CENTER, 2.0, 2.0, 2.0, asset_path) + + additional_components = [ + "Vegetation Rotation Modifier" + ] + for component in additional_components: + spawner_entity.add_component(component) + + # Create surface to spawn vegetation on + dynveg.create_surface_entity("Surface Entity", LEVEL_CENTER, 10.0, 10.0, 1.0) + + # Create Gradient Entity + gradient_entity = hydra.Entity("Gradient Entity") + gradient_entity.create_entity( + LEVEL_CENTER, + ["Constant Gradient"], + parent_id=spawner_entity.id + ) + Report.critical_result(Tests.gradient_entity_created, gradient_entity.id.IsValid()) + gradient_entity.get_set_test(0, "Configuration|Value", constant_gradient_value) + + # Vegetation Rotation Modifier + for axis in ["X", "Y", "Z"]: + spawner_entity.get_set_test( + 3, f"Configuration|Rotation {axis}|Gradient|Gradient Entity Id", gradient_entity.id ) - general.set_current_view_position(512.0, 480.0, 38.0) - # 2) Set up vegetation entities - asset_path = os.path.join("Slices", "PurpleFlower.dynamicslice") - spawner_entity = dynveg.create_vegetation_area("Spawner Entity", LEVEL_CENTER, 2.0, 2.0, 2.0, asset_path) + # Set up constants used across all the rotation checks - additional_components = [ - "Vegetation Rotation Modifier" - ] - for component in additional_components: - spawner_entity.add_component(component) + # Choose an area large enough to contain all of the instances we spawned. + area_center = LEVEL_CENTER + area_radius = 20.0 + # We're spawning a 2x2 area, which will have 3 rows of 3 instances due to default vegetation system spacing, so + # we should have a total of 9 instances. + num_expected = 9 - # Create surface to spawn vegetation on - dynveg.create_surface_entity("Surface Entity", LEVEL_CENTER, 10.0, 10.0, 1.0) + # 3) X-axis check + # baseline, verify that we initially have no rotation + change_range_min("Z", 0.0) + change_range_max("Z", 0.0) + rotation_success = helper.wait_for_condition( + lambda: validate_rotation(area_center, area_radius, num_expected, math.Vector3(0.0, 0.0, 0.0)), 5.0) + Report.result(Tests.rotation_baseline, rotation_success) - # Create Gradient Entity - gradient_entity = hydra.Entity("Gradient Entity") - gradient_entity.create_entity( - LEVEL_CENTER, - ["Constant Gradient"], - parent_id=spawner_entity.id - ) - if gradient_entity.id.IsValid(): - self.log(f"'{gradient_entity.name}' created") - gradient_entity.get_set_test(0, "Configuration|Value", constant_gradient_value) + # Adjust x-axis range min / max to (-180, 0). + # Because we have a constant gradient of 0.25, our actual rotation should be (min + (max - min) * gradient), + # or (-180 + (0 - -180) * 0.25) + change_range_min("X", -180.0) + rotation_degrees = get_expected_rotation(-180.0, 0.0, constant_gradient_value) + rotation_success = helper.wait_for_condition( + lambda: validate_rotation(area_center, area_radius, num_expected, math.Vector3(rotation_degrees, 0.0, 0.0)), + 5.0) + Report.result(Tests.x_axis_rotation_180, rotation_success) - # Vegetation Rotation Modifier - for axis in ["X", "Y", "Z"]: - spawner_entity.get_set_test( - 3, f"Configuration|Rotation {axis}|Gradient|Gradient Entity Id", gradient_entity.id - ) + # Set the min / max to (0, 90), with an expected result of (0 + (90 - 0) * 0.25) + change_range_min("X", 0.0) + change_range_max("X", 90.0) + rotation_degrees = get_expected_rotation(0.0, 90.0, constant_gradient_value) + rotation_success = helper.wait_for_condition( + lambda: validate_rotation(area_center, area_radius, num_expected, math.Vector3(rotation_degrees, 0.0, 0.0)), + 5.0) + Report.result(Tests.x_axis_rotation_90, rotation_success) + change_range_max("X", 0.0) - # Set up constants used across all the rotation checks + # 4) Y-axis check + change_range_min("Y", -180.0) + rotation_degrees = get_expected_rotation(-180.0, 0.0, constant_gradient_value) + rotation_success = helper.wait_for_condition( + lambda: validate_rotation(area_center, area_radius, num_expected, math.Vector3(0.0, rotation_degrees, 0.0)), + 5.0) + Report.result(Tests.y_axis_rotation_180, rotation_success) - # Choose an area large enough to contain all of the instances we spawned. - area_center = LEVEL_CENTER - area_radius = 20.0 - # We're spawning a 2x2 area, which will have 3 rows of 3 instances due to default vegetation system spacing, so - # we should have a total of 9 instances. - num_expected = 9 - - # 3) X-axis check - general.idle_wait(3.0) # Allow mesh to load + change_range_min("Y", 0.0) + change_range_max("Y", 90.0) + rotation_degrees = get_expected_rotation(0.0, 90.0, constant_gradient_value) + rotation_success = helper.wait_for_condition( + lambda: validate_rotation(area_center, area_radius, num_expected, math.Vector3(0.0, rotation_degrees, 0.0)), + 5.0) + Report.result(Tests.y_axis_rotation_90, rotation_success) + change_range_max("Y", 0.0) - # baseline, verify that we initially have no rotation - change_range_min("Z", 0.0) - change_range_max("Z", 0.0) - rotation_success = self.wait_for_condition( - lambda: validate_rotation(area_center, area_radius, num_expected, math.Vector3(0.0, 0.0, 0.0)), - 5.0) - self.test_success = self.test_success and rotation_success + # 5) Z-axis check + change_range_min("Z", -180.0) + rotation_degrees = get_expected_rotation(-180.0, 0.0, constant_gradient_value) + rotation_success = helper.wait_for_condition( + lambda: validate_rotation(area_center, area_radius, num_expected, math.Vector3(0.0, 0.0, rotation_degrees)), + 5.0) + Report.result(Tests.z_axis_rotation_180, rotation_success) - # Adjust x-axis range min / max to (-180, 0). - # Because we have a constant gradient of 0.25, our actual rotation should be (min + (max - min) * gradient), - # or (-180 + (0 - -180) * 0.25) - change_range_min("X", -180.0) - rotation_degrees = get_expected_rotation(-180.0, 0.0, constant_gradient_value) - rotation_success = self.wait_for_condition( - lambda: validate_rotation(area_center, area_radius, num_expected, math.Vector3(rotation_degrees, 0.0, 0.0)), - 5.0) - self.test_success = self.test_success and rotation_success - - # Set the min / max to (0, 90), with an expected result of (0 + (90 - 0) * 0.25) - change_range_min("X", 0.0) - change_range_max("X", 90.0) - rotation_degrees = get_expected_rotation(0.0, 90.0, constant_gradient_value) - rotation_success = self.wait_for_condition( - lambda: validate_rotation(area_center, area_radius, num_expected, math.Vector3(rotation_degrees, 0.0, 0.0)), - 5.0) - self.test_success = self.test_success and rotation_success - - change_range_max("X", 0.0) - - # 4) Y-axis check - change_range_min("Y", -180.0) - rotation_degrees = get_expected_rotation(-180.0, 0.0, constant_gradient_value) - rotation_success = self.wait_for_condition( - lambda: validate_rotation(area_center, area_radius, num_expected, math.Vector3(0.0, rotation_degrees, 0.0)), - 5.0) - self.test_success = self.test_success and rotation_success - - change_range_min("Y", 0.0) - change_range_max("Y", 90.0) - rotation_degrees = get_expected_rotation(0.0, 90.0, constant_gradient_value) - rotation_success = self.wait_for_condition( - lambda: validate_rotation(area_center, area_radius, num_expected, math.Vector3(0.0, rotation_degrees, 0.0)), - 5.0) - self.test_success = self.test_success and rotation_success - - change_range_max("Y", 0.0) - - # 5) Z-axis check - change_range_min("Z", -180.0) - rotation_degrees = get_expected_rotation(-180.0, 0.0, constant_gradient_value) - rotation_success = self.wait_for_condition( - lambda: validate_rotation(area_center, area_radius, num_expected, math.Vector3(0.0, 0.0, rotation_degrees)), - 5.0) - self.test_success = self.test_success and rotation_success - - change_range_min("Z", 0.0) - change_range_max("Z", 90.0) - rotation_degrees = get_expected_rotation(0.0, 90.0, constant_gradient_value) - rotation_success = self.wait_for_condition( - lambda: validate_rotation(area_center, area_radius, num_expected, math.Vector3(0.0, 0.0, rotation_degrees)), - 5.0) - self.test_success = self.test_success and rotation_success + change_range_min("Z", 0.0) + change_range_max("Z", 90.0) + rotation_degrees = get_expected_rotation(0.0, 90.0, constant_gradient_value) + rotation_success = helper.wait_for_condition( + lambda: validate_rotation(area_center, area_radius, num_expected, math.Vector3(0.0, 0.0, rotation_degrees)), + 5.0) + Report.result(Tests.z_axis_rotation_90, rotation_success) -test = TestRotationModifier_InstancesRotateWithinRange() -test.run() +if __name__ == "__main__": + + from editor_python_test_tools.utils import Report + Report.start_test(RotationModifier_InstancesRotateWithinRange) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/ScaleModifierOverrides_InstancesProperlyScale.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/ScaleModifierOverrides_InstancesProperlyScale.py index d065c821bf..9f2359ebe2 100755 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/ScaleModifierOverrides_InstancesProperlyScale.py +++ b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/ScaleModifierOverrides_InstancesProperlyScale.py @@ -5,153 +5,155 @@ For complete copyright and license terms please see the LICENSE at the root of t SPDX-License-Identifier: Apache-2.0 OR MIT """ -import os -import sys -sys.path.append(os.path.dirname(os.path.abspath(__file__))) -import azlmbr.areasystem as areasystem -import azlmbr.bus as bus -import azlmbr.legacy.general as general -import azlmbr.math as math - -sys.path.append(os.path.join(azlmbr.paths.devroot, 'AutomatedTesting', 'Gem', 'PythonTests')) -import editor_python_test_tools.hydra_editor_utils as hydra -from editor_python_test_tools.editor_test_helper import EditorTestHelper -from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg - -# Constants -CLOSE_ENOUGH_THRESHOLD = 0.01 +class Tests: + gradient_entity_created = ( + "Successfully created Gradient entity", + "Failed to create Gradient entity" + ) + scale_values_set = ( + "Scale Min and Scale Max are set to 0.1 and 1.0 in Vegetation Asset List", + "Scale Min and Scale Max are not set to 0.1 and 1.0 in Vegetation Asset List" + ) + instance_count = ( + "Found the expected number of instances", + "Found an unexpected number of instances" + ) + instances_properly_scaled = ( + "All instances scaled within appropriate range", + "Found instances scaled outside of the appropriate range" + ) -class TestScaleModifierOverrides_InstancesProperlyScale(EditorTestHelper): - def __init__(self): - EditorTestHelper.__init__(self, log_prefix="ScaleModifierOverrides_InstancesProperlyScale", args=["level"]) +def ScaleModifierOverrides_InstancesProperlyScale(): + """ + Summary: + A level is created, then as simple vegetation area is created. Vegetation Scale Modifier component is + added to the vegetation area. A new child entity is created with Random Noise Gradient Generator, + Gradient Transform Modifier, and Box Shape. Child entity is set as gradient entity id in Vegetation + Scale Modifier, and scale of instances is validated to fall within expected range. - def run_test(self): - """ - Summary: - A level is created, then as simple vegetation area is created. Vegetation Scale Modifier component is - added to the vegetation area. A new child entity is created with Random Noise Gradient Generator, - Gradient Transform Modifier, and Box Shape. Child entity is set as gradient entity id in Vegetation - Scale Modifier, and scale of instances is validated to fall within expected range. + Expected Behavior: + Vegetation instances have random scale between Range Min and Range Max applied. - Expected Behavior: - Vegetation instances have random scale between Range Min and Range Max applied. + Test Steps: + 1) Open an existing level + 2) Create a new entity with components "Vegetation Layer Spawner", "Vegetation Asset List", "Box Shape" + 3) Set a valid mesh asset on the Vegetation Asset List + 4) Add Vegetation Scale Modifier component to the vegetation and set the values + 5) Toggle on Scale Modifier Override and verify Scale Min and Scale Max are set 0.1 and 1.0 + 6) Create a new child entity and add components + 7) Add child entity as gradient entity id in Vegetation Scale Modifier + 8) Validate scale of instances with a few different min/max override values - Test Steps: - 1) Create level - 2) Create a new entity with components "Vegetation Layer Spawner", "Vegetation Asset List", "Box Shape" - 3) Set a valid mesh asset on the Vegetation Asset List - 4) Add Vegetation Scale Modifier component to the vegetation and set the values - 5) Toggle on Scale Modifier Override and verify Scale Min and Scale Max are set 0.1 and 1.0 - 6) Create a new child entity and add components - 7) Add child entity as gradient entity id in Vegetation Scale Modifier - 8) Validate scale of instances with a few different min/max override values + Note: + - Any passed and failed tests are written to the Editor.log file. + Parsing the file or running a log_monitor are required to observe the test results. - Note: - - Any passed and failed tests are written to the Editor.log file. - Parsing the file or running a log_monitor are required to observe the test results. + :return: None + """ - :return: None - """ + import os - def set_and_validate_scale(entity, min_scale, max_scale): - # Set Range Min/Max - entity.get_set_test(2, "Configuration|Embedded Assets|[0]|Scale Modifier|Min", min_scale) - entity.get_set_test(2, "Configuration|Embedded Assets|[0]|Scale Modifier|Max", max_scale) + import azlmbr.areasystem as areasystem + import azlmbr.bus as bus + import azlmbr.legacy.general as general + import azlmbr.math as math - # Clear all areas to force a refresh - general.run_console('veg_debugClearAllAreas') + import editor_python_test_tools.hydra_editor_utils as hydra + from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg + from editor_python_test_tools.utils import Report + from editor_python_test_tools.utils import TestHelper as helper - # Wait for instances to spawn - num_expected = 20 * 20 - self.test_success = self.test_success and self.wait_for_condition( - lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, num_expected), 5.0) + # Constants + CLOSE_ENOUGH_THRESHOLD = 0.01 - # Validate scale values of instances - box = azlmbr.shape.ShapeComponentRequestsBus(bus.Event, 'GetEncompassingAabb', entity.id) - instances = areasystem.AreaSystemRequestBus(bus.Broadcast, 'GetInstancesInAabb', box) - if len(instances) == num_expected: - for instance in instances: - if min_scale <= instance.scale <= max_scale: - self.log("All instances scaled within appropriate range") - return True - self.log(f"Instance at {instance.position} scale is {instance.scale}. Expected between " - f"{min_scale}/{max_scale}") - return False - self.log(f"Failed to find all instances! Found {len(instances)}, expected {num_expected}.") - return False + def set_and_validate_scale(entity, min_scale, max_scale): + # Set Range Min/Max + entity.get_set_test(2, "Configuration|Embedded Assets|[0]|Scale Modifier|Min", min_scale) + entity.get_set_test(2, "Configuration|Embedded Assets|[0]|Scale Modifier|Max", max_scale) - # 1) Create level and set an appropriate view of spawner area - self.test_success = self.create_level( - self.args["level"], - heightmap_resolution=1024, - heightmap_meters_per_pixel=1, - terrain_texture_resolution=4096, - use_terrain=False, + # Refresh the planted instances + general.run_console("veg_DebugClearAllAreas") + + # Check the initial instance count + num_expected = 20 * 20 + success = helper.wait_for_condition( + lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, num_expected), 5.0) + Report.critical_result(Tests.instance_count, success) + + # Validate scale values of instances + box = azlmbr.shape.ShapeComponentRequestsBus(bus.Event, 'GetEncompassingAabb', entity.id) + instances = areasystem.AreaSystemRequestBus(bus.Broadcast, 'GetInstancesInAabb', box) + for instance in instances: + if min_scale <= instance.scale <= max_scale: + return True + return False + + # 1) Open an existing simple level + helper.init_idle() + helper.open_level("Physics", "Base") + + general.set_current_view_position(500.49, 498.69, 46.66) + general.set_current_view_rotation(-42.05, 0.00, -36.33) + + # 2) Create a new entity with components "Vegetation Layer Spawner", "Vegetation Asset List", "Box Shape" + entity_position = math.Vector3(512.0, 512.0, 32.0) + asset_path = os.path.join("Slices", "PurpleFlower.dynamicslice") + spawner_entity = dynveg.create_vegetation_area("Spawner Entity", entity_position, 16.0, 16.0, 10.0, asset_path) + + # Create a surface to plant on and add a Vegetation Debugger Level component to allow refreshes + dynveg.create_surface_entity("Surface Entity", entity_position, 20.0, 20.0, 1.0) + hydra.add_level_component("Vegetation Debugger") + + # 4) Add Vegetation Scale Modifier component to the vegetation and set the values + spawner_entity.add_component("Vegetation Scale Modifier") + spawner_entity.get_set_test(3, "Configuration|Allow Per-Item Overrides", True) + + # 5) Toggle on Scale Modifier Override and verify Scale Min and Scale Max are set 0.1 and 1.0 + spawner_entity.get_set_test(2, "Configuration|Embedded Assets|[0]|Scale Modifier|Override Enabled", True) + scale_min = float( + format( + ( + hydra.get_component_property_value( + spawner_entity.components[2], "Configuration|Embedded Assets|[0]|Scale Modifier|Min" + ) + ), + ".1f", ) - - general.set_current_view_position(500.49, 498.69, 46.66) - general.set_current_view_rotation(-42.05, 0.00, -36.33) - - # 2) Create a new entity with components "Vegetation Layer Spawner", "Vegetation Asset List", "Box Shape" - entity_position = math.Vector3(512.0, 512.0, 32.0) - asset_path = os.path.join("Slices", "PurpleFlower.dynamicslice") - spawner_entity = dynveg.create_vegetation_area("Spawner Entity", entity_position, 16.0, 16.0, 10.0, asset_path) - - # Create a surface to plant on and add a Vegetation Debugger Level component to allow refreshes - dynveg.create_surface_entity("Surface Entity", entity_position, 20.0, 20.0, 1.0) - hydra.add_level_component("Vegetation Debugger") - - # 4) Add Vegetation Scale Modifier component to the vegetation and set the values - spawner_entity.add_component("Vegetation Scale Modifier") - spawner_entity.get_set_test(3, "Configuration|Allow Per-Item Overrides", True) - - # 5) Toggle on Scale Modifier Override and verify Scale Min and Scale Max are set 0.1 and 1.0 - spawner_entity.get_set_test(2, "Configuration|Embedded Assets|[0]|Scale Modifier|Override Enabled", True) - scale_min = float( - format( - ( - hydra.get_component_property_value( - spawner_entity.components[2], "Configuration|Embedded Assets|[0]|Scale Modifier|Min" - ) - ), - ".1f", - ) + ) + scale_max = float( + format( + ( + hydra.get_component_property_value( + spawner_entity.components[2], "Configuration|Embedded Assets|[0]|Scale Modifier|Max" + ) + ), + ".1f", ) - scale_max = float( - format( - ( - hydra.get_component_property_value( - spawner_entity.components[2], "Configuration|Embedded Assets|[0]|Scale Modifier|Max" - ) - ), - ".1f", - ) - ) - if ((scale_max - 1.0) < CLOSE_ENOUGH_THRESHOLD) and ((scale_min - 0.1) < CLOSE_ENOUGH_THRESHOLD): - self.log("Scale Min and Scale Max are set to 0.1 and 1.0 in Vegetation Asset List") - else: - self.log("Scale Min and Scale Max are not set to 0.1 and 1.0 in Vegetation Asset List") + ) + Report.result(Tests.scale_values_set, ((scale_max - 1.0) < CLOSE_ENOUGH_THRESHOLD) and + ((scale_min - 0.1) < CLOSE_ENOUGH_THRESHOLD)) - # 6) Create a new child entity and add components - gradient_entity = hydra.Entity("Gradient Entity") - gradient_entity.create_entity( - entity_position, - ["Random Noise Gradient", "Gradient Transform Modifier", "Box Shape"], - parent_id=spawner_entity.id - ) - if gradient_entity.id.IsValid(): - self.log(f"'{gradient_entity.name}' created") + # 6) Create a new child entity and add components + gradient_entity = hydra.Entity("Gradient Entity") + gradient_entity.create_entity( + entity_position, + ["Random Noise Gradient", "Gradient Transform Modifier", "Box Shape"], + parent_id=spawner_entity.id + ) + Report.result(Tests.gradient_entity_created, gradient_entity.id.IsValid()) - # 7) Add child entity as gradient entity id in Vegetation Scale Modifier - spawner_entity.get_set_test(3, "Configuration|Gradient|Gradient Entity Id", gradient_entity.id) + # 7) Add child entity as gradient entity id in Vegetation Scale Modifier + spawner_entity.get_set_test(3, "Configuration|Gradient|Gradient Entity Id", gradient_entity.id) - # 8) Validate instances are scaled properly via a few different Range Min/Max settings on the override - self.test_success = set_and_validate_scale(spawner_entity, 0.1, 1.0) and self.test_success - self.test_success = set_and_validate_scale(spawner_entity, 2.0, 2.5) and self.test_success - self.test_success = set_and_validate_scale(spawner_entity, 1.0, 5.0) and self.test_success + # 8) Validate instances are scaled properly via a few different Range Min/Max settings on the override + Report.result(Tests.instances_properly_scaled, set_and_validate_scale(spawner_entity, 0.1, 1.0)) + Report.result(Tests.instances_properly_scaled, set_and_validate_scale(spawner_entity, 2.0, 2.5)) + Report.result(Tests.instances_properly_scaled, set_and_validate_scale(spawner_entity, 1.0, 5.0)) -test = TestScaleModifierOverrides_InstancesProperlyScale() -test.run() +if __name__ == "__main__": + + from editor_python_test_tools.utils import Report + Report.start_test(ScaleModifierOverrides_InstancesProperlyScale) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/ScaleModifier_InstancesProperlyScale.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/ScaleModifier_InstancesProperlyScale.py index e579495e42..b2fadaa703 100755 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/ScaleModifier_InstancesProperlyScale.py +++ b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/ScaleModifier_InstancesProperlyScale.py @@ -5,123 +5,123 @@ For complete copyright and license terms please see the LICENSE at the root of t SPDX-License-Identifier: Apache-2.0 OR MIT """ -import os -import sys -sys.path.append(os.path.dirname(os.path.abspath(__file__))) -import azlmbr.areasystem as areasystem -import azlmbr.bus as bus -import azlmbr.legacy.general as general -import azlmbr.math as math - -sys.path.append(os.path.join(azlmbr.paths.devroot, 'AutomatedTesting', 'Gem', 'PythonTests')) -import editor_python_test_tools.hydra_editor_utils as hydra -from editor_python_test_tools.editor_test_helper import EditorTestHelper -from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg +class Tests: + gradient_entity_created = ( + "Successfully created Gradient entity", + "Failed to create Gradient entity" + ) + instance_count = ( + "Found the expected number of instances", + "Found an unexpected number of instances" + ) + instances_properly_scaled = ( + "All instances scaled within appropriate range", + "Found instances scaled outside of the appropriate range" + ) -class TestScaleModifier_InstancesProperlyScale(EditorTestHelper): - def __init__(self): - EditorTestHelper.__init__(self, log_prefix="ScaleModifier_InstancesProperlyScale", args=["level"]) +def ScaleModifier_InstancesProperlyScale(): + """ + Summary: + A New level is created. A New entity is created with components Vegetation Layer Spawner, Vegetation Asset List, + Box Shape and Vegetation Scale Modifier. A New child entity is created with components Random Noise Gradient, + Gradient Transform Modifier, and Box Shape. Pin the Random Noise entity to the Gradient Entity Id field for + the Gradient group. Range Min and Range Max are set to few values and values are validated. Range Min and Range + Max are set to few other values and values are validated. - def run_test(self): - """ - Summary: - A New level is created. A New entity is created with components Vegetation Layer Spawner, Vegetation Asset List, - Box Shape and Vegetation Scale Modifier. A New child entity is created with components Random Noise Gradient, - Gradient Transform Modifier, and Box Shape. Pin the Random Noise entity to the Gradient Entity Id field for - the Gradient group. Range Min and Range Max are set to few values and values are validated. Range Min and Range - Max are set to few other values and values are validated. + Expected Behavior: + Vegetation instances are scaled within Range Min/Range Max. - Expected Behavior: - Vegetation instances are scaled within Range Min/Range Max. + Test Steps: + 1) Open an existing level + 2) Create a new entity with components Vegetation Layer Spawner, Vegetation Asset List, Box Shape and + Vegetation Scale Modifier + 3) Create child entity with components Random Noise Gradient, Gradient Transform Modifier and Box Shape + 4) Pin the Random Noise entity to the Gradient Entity Id field for the Gradient group. + 5) Range Min/Max is set to few different values on the Vegetation Scale Modifier component and + scale of instances is validated - Test Steps: - 1) Create level - 2) Create a new entity with components Vegetation Layer Spawner, Vegetation Asset List, Box Shape and - Vegetation Scale Modifier - 3) Create child entity with components Random Noise Gradient, Gradient Transform Modifier and Box Shape - 4) Pin the Random Noise entity to the Gradient Entity Id field for the Gradient group. - 5) Range Min/Max is set to few different values on the Vegetation Scale Modifier component and - scale of instances is validated + Note: + - Any passed and failed tests are written to the Editor.log file. + Parsing the file or running a log_monitor are required to observe the test results. - Note: - - Any passed and failed tests are written to the Editor.log file. - Parsing the file or running a log_monitor are required to observe the test results. + :return: None + """ - :return: None - """ + import os - def set_and_validate_scale(entity, min_scale, max_scale): - # Set Range Min/Max - entity.get_set_test(3, "Configuration|Range Min", min_scale) - entity.get_set_test(3, "Configuration|Range Max", max_scale) + import azlmbr.areasystem as areasystem + import azlmbr.bus as bus + import azlmbr.legacy.general as general + import azlmbr.math as math - # Clear all areas to force a refresh - general.run_console('veg_debugClearAllAreas') + import editor_python_test_tools.hydra_editor_utils as hydra + from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg + from editor_python_test_tools.utils import Report + from editor_python_test_tools.utils import TestHelper as helper - # Wait for instances to spawn - num_expected = 20 * 20 - self.test_success = self.test_success and self.wait_for_condition( - lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, num_expected), 5.0) + def set_and_validate_scale(entity, min_scale, max_scale): + # Set Range Min/Max + entity.get_set_test(3, "Configuration|Range Min", min_scale) + entity.get_set_test(3, "Configuration|Range Max", max_scale) - # Validate scale values of instances - box = azlmbr.shape.ShapeComponentRequestsBus(bus.Event, 'GetEncompassingAabb', entity.id) - instances = areasystem.AreaSystemRequestBus(bus.Broadcast, 'GetInstancesInAabb', box) - if len(instances) == num_expected: - for instance in instances: - if min_scale <= instance.scale <= max_scale: - self.log("All instances scaled within appropriate range") - return True - self.log(f"Instance at {instance.position} scale is {instance.scale}. Expected between " - f"{min_scale}/{max_scale}") - return False - self.log(f"Failed to find all instances! Found {len(instances)}, expected {num_expected}.") - return False + # Refresh the planted instances + general.run_console("veg_DebugClearAllAreas") - # 1) Create level and set an appropriate view of spawner area - self.test_success = self.create_level( - self.args["level"], - heightmap_resolution=1024, - heightmap_meters_per_pixel=1, - terrain_texture_resolution=4096, - use_terrain=False, - ) + # Check the initial instance count + num_expected = 20 * 20 + success = helper.wait_for_condition( + lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, num_expected), 5.0) + Report.critical_result(Tests.instance_count, success) - general.set_current_view_position(500.49, 498.69, 46.66) - general.set_current_view_rotation(-42.05, 0.00, -36.33) + # Validate scale values of instances + box = azlmbr.shape.ShapeComponentRequestsBus(bus.Event, 'GetEncompassingAabb', entity.id) + instances = areasystem.AreaSystemRequestBus(bus.Broadcast, 'GetInstancesInAabb', box) + for instance in instances: + if min_scale <= instance.scale <= max_scale: + return True + return False - # 2) Create a new entity with components Vegetation Layer Spawner, Vegetation Asset List, Box Shape and - # Vegetation Scale Modifier - entity_position = math.Vector3(512.0, 512.0, 32.0) - asset_path = os.path.join("Slices", "PurpleFlower.dynamicslice") - spawner_entity = dynveg.create_vegetation_area("Spawner Entity", entity_position, 16.0, 16.0, 16.0, - asset_path) - spawner_entity.add_component("Vegetation Scale Modifier") + # 1) Open an existing simple level + helper.init_idle() + helper.open_level("Physics", "Base") - # Create a surface to plant on and add a Vegetation Debugger Level component to allow refreshes - dynveg.create_surface_entity("Surface Entity", entity_position, 20.0, 20.0, 1.0) - hydra.add_level_component("Vegetation Debugger") + general.set_current_view_position(500.49, 498.69, 46.66) + general.set_current_view_rotation(-42.05, 0.00, -36.33) - # 3) Create child entity with components Random Noise Gradient, Gradient Transform Modifier and Box Shape - gradient_entity = hydra.Entity("Gradient Entity") - gradient_entity.create_entity( - entity_position, - ["Random Noise Gradient", "Gradient Transform Modifier", "Box Shape"], - parent_id=spawner_entity.id - ) - if gradient_entity.id.IsValid(): - self.log(f"'{gradient_entity.name}' created") + # 2) Create a new entity with components Vegetation Layer Spawner, Vegetation Asset List, Box Shape and + # Vegetation Scale Modifier + entity_position = math.Vector3(512.0, 512.0, 32.0) + asset_path = os.path.join("Slices", "PurpleFlower.dynamicslice") + spawner_entity = dynveg.create_vegetation_area("Spawner Entity", entity_position, 16.0, 16.0, 16.0, + asset_path) + spawner_entity.add_component("Vegetation Scale Modifier") - # 4) Pin the Random Noise entity to the Gradient Entity Id field for the Gradient group. - spawner_entity.get_set_test(3, "Configuration|Gradient|Gradient Entity Id", gradient_entity.id) + # Create a surface to plant on and add a Vegetation Debugger Level component to allow refreshes + dynveg.create_surface_entity("Surface Entity", entity_position, 20.0, 20.0, 1.0) + hydra.add_level_component("Vegetation Debugger") - # 5) Set Range Min/Max on the Vegetation Scale Modifier component to diff values, and verify instance scale is - # within bounds - self.test_success = set_and_validate_scale(spawner_entity, 2.0, 4.0) and self.test_success - self.test_success = set_and_validate_scale(spawner_entity, 12.0, 40.0) and self.test_success - self.test_success = set_and_validate_scale(spawner_entity, 0.5, 2.5) and self.test_success + # 3) Create child entity with components Random Noise Gradient, Gradient Transform Modifier and Box Shape + gradient_entity = hydra.Entity("Gradient Entity") + gradient_entity.create_entity( + entity_position, + ["Random Noise Gradient", "Gradient Transform Modifier", "Box Shape"], + parent_id=spawner_entity.id + ) + Report.critical_result(Tests.gradient_entity_created, gradient_entity.id.IsValid()) + + # 4) Pin the Random Noise entity to the Gradient Entity Id field for the Gradient group. + spawner_entity.get_set_test(3, "Configuration|Gradient|Gradient Entity Id", gradient_entity.id) + + # 5) Set Range Min/Max on the Vegetation Scale Modifier component to diff values, and verify instance scale is + # within bounds + Report.result(Tests.instances_properly_scaled, set_and_validate_scale(spawner_entity, 2.0, 4.0)) + Report.result(Tests.instances_properly_scaled, set_and_validate_scale(spawner_entity, 12.0, 40.0)) + Report.result(Tests.instances_properly_scaled, set_and_validate_scale(spawner_entity, 0.5, 2.5)) -test = TestScaleModifier_InstancesProperlyScale() -test.run() +if __name__ == "__main__": + + from editor_python_test_tools.utils import Report + Report.start_test(ScaleModifier_InstancesProperlyScale) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/ShapeIntersectionFilter_FilterStageToggle.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/ShapeIntersectionFilter_FilterStageToggle.py new file mode 100644 index 0000000000..1b9c6aa5ea --- /dev/null +++ b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/ShapeIntersectionFilter_FilterStageToggle.py @@ -0,0 +1,113 @@ +""" +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 +""" + + +class Tests: + instance_count_in_box_shape = ( + "Only found instances in the configured Box Shape intersection area", + "Found instances outside of the configured Box Shape intersection area" + ) + instance_count_in_cylinder_shape = ( + "Only found instances in the configured Cylinder Shape intersection area", + "Found instances outside of the configured Cylinder Shape intersection area" + ) + preprocess_instance_count = ( + "Found the expected number of instances with preprocessing filter stage", + "Found an unexpected number of instances with preprocessing filter stage" + ) + postprocess_instance_count = ( + "Found the expected number of instances with postprocessing filter stage", + "Found an unexpected number of instances with postprocessing filter stage" + ) + + +def ShapeIntersectionFilter_FilterStageToggle(): + """ + Summary: + Filter Stage toggle affects final vegetation position + + Expected Result: + Vegetation instances plant differently depending on the Filter Stage setting. With PreProcess, some vegetation + instances can appear on slopes outside the filtered values. With PostProcess, vegetation instances only appear on + the correct slope values. + + :return: None + """ + + import os + + import azlmbr.math as math + import azlmbr.legacy.general as general + + import editor_python_test_tools.hydra_editor_utils as hydra + from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg + from editor_python_test_tools.utils import Report + from editor_python_test_tools.utils import TestHelper as helper + + # Open an existing simple level + helper.init_idle() + helper.open_level("Physics", "Base") + + general.set_current_view_position(512.0, 480.0, 38.0) + + # Create basic vegetation entity + position = math.Vector3(512.0, 512.0, 32.0) + asset_path = os.path.join("Slices", "PinkFlower.dynamicslice") + vegetation = dynveg.create_vegetation_area("vegetation", position, 16.0, 16.0, 16.0, asset_path) + + # Create Surface for instances to plant on + dynveg.create_surface_entity("Surface_Entity_Parent", position, 16.0, 16.0, 1.0) + + # Add a Vegetation Shape Intersection Filter to the vegetation area entity + vegetation.add_component("Vegetation Shape Intersection Filter") + + # Create a new entity as a child of the vegetation area entity with Box Shape + box = hydra.Entity("box") + box.create_entity(position, ["Box Shape"]) + box.get_set_test(0, "Box Shape|Box Configuration|Dimensions", math.Vector3(8.0, 8.0, 1.0)) + + # Create a new entity as a child of the vegetation area entity with Cylinder Shape. + cylinder = hydra.Entity("cylinder") + cylinder.create_entity(position, ["Cylinder Shape"]) + cylinder.get_set_test(0, "Cylinder Shape|Cylinder Configuration|Radius", 5.0) + cylinder.get_set_test(0, "Cylinder Shape|Cylinder Configuration|Height", 5.0) + box.set_test_parent_entity(vegetation) + cylinder.set_test_parent_entity(vegetation) + + # On the Shape Intersection Filter component, click the crosshair button, and add child entities one by one + vegetation.get_set_test(3, "Configuration|Shape Entity Id", box.id) + result = helper.wait_for_condition(lambda: dynveg.validate_instance_count(position, 8.0, 100), 2.0) + Report.result(Tests.instance_count_in_box_shape, result) + vegetation.get_set_test(3, "Configuration|Shape Entity Id", cylinder.id) + result = helper.wait_for_condition(lambda: dynveg.validate_instance_count(position, 5.0, 100), 2.0) + Report.result(Tests.instance_count_in_cylinder_shape, result) + + # Create a new entity as a child of the area entity with Random Noise Gradient, Gradient Transform Modifier, + # and Box Shape component + random_noise = hydra.Entity("random_noise") + random_noise.create_entity(position, ["Random Noise Gradient", "Gradient Transform Modifier", "Box Shape"]) + random_noise.set_test_parent_entity(vegetation) + + # Add a Vegetation Position Modifier to the vegetation area entity + vegetation.add_component("Vegetation Position Modifier") + + # Pin the Random Noise entity to the Gradient Entity Id field of the Position Modifier's Gradient X + vegetation.get_set_test(4, "Configuration|Position X|Gradient|Gradient Entity Id", random_noise.id) + + # Toggle between PreProcess and PostProcess + vegetation.get_set_test(3, "Configuration|Filter Stage", 1) + result = helper.wait_for_condition(lambda: dynveg.validate_instance_count(position, 5.0, 117), 2.0) + Report.result(Tests.preprocess_instance_count, result) + vegetation.get_set_test(3, "Configuration|Filter Stage", 2) + result = helper.wait_for_condition(lambda: dynveg.validate_instance_count(position, 5.0, 122), 2.0) + Report.result(Tests.postprocess_instance_count, result) + + +if __name__ == "__main__": + + from editor_python_test_tools.utils import Report + Report.start_test(ShapeIntersectionFilter_FilterStageToggle) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/ShapeIntersectionFilter_InstancesPlantInAssignedShape.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/ShapeIntersectionFilter_InstancesPlantInAssignedShape.py index 83458fe153..e872b23054 100755 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/ShapeIntersectionFilter_InstancesPlantInAssignedShape.py +++ b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/ShapeIntersectionFilter_InstancesPlantInAssignedShape.py @@ -5,124 +5,126 @@ For complete copyright and license terms please see the LICENSE at the root of t SPDX-License-Identifier: Apache-2.0 OR MIT """ -""" -C4874094: Shape reference can be replaced/removed -""" -import os -import sys - -sys.path.append(os.path.dirname(os.path.abspath(__file__))) -import azlmbr.editor as editor -import azlmbr.legacy.general as general -import azlmbr.bus as bus -import azlmbr.math as math -import azlmbr.paths - -sys.path.append(os.path.join(azlmbr.paths.devroot, 'AutomatedTesting', 'Gem', 'PythonTests')) -import editor_python_test_tools.hydra_editor_utils as hydra -from editor_python_test_tools.editor_test_helper import EditorTestHelper -from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg +class Tests: + instance_count_in_box_shape = ( + "Only found instances in the configured Box Shape intersection area", + "Found instances outside of the configured Box Shape intersection area" + ) + instance_count_in_cylinder_shape = ( + "Only found instances in the configured Cylinder Shape intersection area", + "Found instances outside of the configured Cylinder Shape intersection area" + ) + unfiltered_instance_count = ( + "Found instances in the entire Spawner area with no filter set", + "Failed to find all expected instances in the Spawner area with no filter set" + ) -class TestShapeIntersectionFilter(EditorTestHelper): - def __init__(self): - EditorTestHelper.__init__(self, log_prefix="ShapeIntersectionFilter_InstancePlanting", args=["level"]) +def ShapeIntersectionFilter_InstancesPlantInAssignedShape(): + """ + Summary: + A spawner area is created with a Vegetation Shape Intersection Filter. 2 different shape entities are created, + pinned to the Shape Intersection Filter, and instance counts are verified. - def run_test(self): - """ - Summary: - A spawner area is created with a Vegetation Shape Intersection Filter. 2 different shape entities are created, - pinned to the Shape Intersection Filter, and instance counts are verified. + Expected Behavior: + The Shape Entity Id reference can be successfully set/updated. Instances spawn only in the specified shape area. - Expected Behavior: - The Shape Entity Id reference can be successfully set/updated. Instances spawn only in the specified shape area. + Test Steps: + 1) Open an existing level, and set view for visual debugging + 2) Create an instance spawner and planting surface + 3) Create child entity with Box Shape + 4) Create child entity with Cylinder Shape + 5) Assign the Intersection Filter to the Box Shape and validate instance counts + 6) Assign the Intersection Filter to the Cylinder Shape and validate instance counts + 7) Remove the shape reference on the Intersection Filter and validate instance counts - Test Steps: - 1) Create a new level, and set view for visual debugging - 2) Create an instance spawner and planting surface - 3) Create child entity with Box Shape - 4) Create child entity with Cylinder Shape - 5) Assign the Intersection Filter to the Box Shape and validate instance counts - 6) Assign the Intersection Filter to the Cylinder Shape and validate instance counts - 7) Remove the shape reference on the Intersection Filter and validate instance counts + Note: + - This test file must be called from the Open 3D Engine Editor command terminal + - Any passed and failed tests are written to the Editor.log file. + Parsing the file or running a log_monitor are required to observe the test results. - Note: - - This test file must be called from the Open 3D Engine Editor command terminal - - Any passed and failed tests are written to the Editor.log file. - Parsing the file or running a log_monitor are required to observe the test results. + :return: None + """ - :return: None - """ - # 1) Create a new, temporary level - self.test_success = self.create_level( - self.args["level"], - heightmap_resolution=1024, - heightmap_meters_per_pixel=1, - terrain_texture_resolution=4096, - use_terrain=False, - ) + import os - # Set view of planting area for visual debugging - general.set_current_view_position(512.0, 500.0, 38.0) - general.set_current_view_rotation(-20.0, 0.0, 0.0) + import azlmbr.editor as editor + import azlmbr.legacy.general as general + import azlmbr.bus as bus + import azlmbr.math as math - # 2) Create a new entity with required vegetation area components and Vegetation Shape Intersection Filter - center_point = math.Vector3(512.0, 512.0, 32.0) - asset_path = os.path.join("Slices", "PinkFlower.dynamicslice") - spawner_entity = dynveg.create_vegetation_area("Instance Spawner", center_point, 16.0, 16.0, 1.0, - asset_path) - spawner_entity.add_component("Vegetation Shape Intersection Filter") + import editor_python_test_tools.hydra_editor_utils as hydra + from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg + from editor_python_test_tools.utils import Report + from editor_python_test_tools.utils import TestHelper as helper - # Create a planting surface - dynveg.create_surface_entity("Planting Surface", center_point, 32.0, 32.0, 1.0) + # 1) Open an existing simple level + helper.init_idle() + helper.open_level("Physics", "Base") - # 3) Create a child entity with Box Shape - components_to_add = ["Box Shape"] - box_id = editor.ToolsApplicationRequestBus(bus.Broadcast, "CreateNewEntity", spawner_entity.id) - box = hydra.Entity("Box", box_id) - box.components = [] - for component in components_to_add: - box.components.append(hydra.add_component(component, box_id)) - new_box_dimension = math.Vector3(5.0, 5.0, 5.0) - hydra.get_set_test(box, 0, "Box Shape|Box Configuration|Dimensions", new_box_dimension) + # Set view of planting area for visual debugging + general.set_current_view_position(512.0, 500.0, 38.0) + general.set_current_view_rotation(-20.0, 0.0, 0.0) - # 4) Create a child entity with Cylinder Shape - components_to_add = ["Cylinder Shape"] - cylinder_id = editor.ToolsApplicationRequestBus(bus.Broadcast, "CreateNewEntity", spawner_entity.id) - cylinder = hydra.Entity("Cylinder", cylinder_id) - cylinder.components = [] - for component in components_to_add: - cylinder.components.append(hydra.add_component(component, cylinder_id)) - hydra.get_set_test(cylinder, 0, "Cylinder Shape|Cylinder Configuration|Radius", 5.0) + # 2) Create a new entity with required vegetation area components and Vegetation Shape Intersection Filter + center_point = math.Vector3(512.0, 512.0, 32.0) + asset_path = os.path.join("Slices", "PinkFlower.dynamicslice") + spawner_entity = dynveg.create_vegetation_area("Instance Spawner", center_point, 16.0, 16.0, 1.0, + asset_path) + spawner_entity.add_component("Vegetation Shape Intersection Filter") - # 5) Set the Intersection Filter's Shape Entity Id to the Box Shape entity - spawner_entity.get_set_test(3, "Configuration|Shape Entity Id", box_id) + # Create a planting surface + dynveg.create_surface_entity("Planting Surface", center_point, 32.0, 32.0, 1.0) - # Validate instance counts. Instances should only plant in the Box Shape area - num_expected = 49 - success = self.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, - num_expected), 5.0) - self.test_success = success and self.test_success - - # 6) Set the Intersection Filter's Shape Entity Id to the Cylinder Shape entity - spawner_entity.get_set_test(3, "Configuration|Shape Entity Id", cylinder_id) + # 3) Create a child entity with Box Shape + components_to_add = ["Box Shape"] + box_id = editor.ToolsApplicationRequestBus(bus.Broadcast, "CreateNewEntity", spawner_entity.id) + box = hydra.Entity("Box", box_id) + box.components = [] + for component in components_to_add: + box.components.append(hydra.add_component(component, box_id)) + new_box_dimension = math.Vector3(5.0, 5.0, 5.0) + hydra.get_set_test(box, 0, "Box Shape|Box Configuration|Dimensions", new_box_dimension) - # Validate instance counts. Instances should only plant in the Cylinder Shape area - num_expected = 121 - success = self.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, - num_expected), 5.0) - self.test_success = success and self.test_success + # 4) Create a child entity with Cylinder Shape + components_to_add = ["Cylinder Shape"] + cylinder_id = editor.ToolsApplicationRequestBus(bus.Broadcast, "CreateNewEntity", spawner_entity.id) + cylinder = hydra.Entity("Cylinder", cylinder_id) + cylinder.components = [] + for component in components_to_add: + cylinder.components.append(hydra.add_component(component, cylinder_id)) + hydra.get_set_test(cylinder, 0, "Cylinder Shape|Cylinder Configuration|Radius", 5.0) - # 7) Clear the Intersection Filter's Shape Entity Id reference - spawner_entity.get_set_test(3, "Configuration|Shape Entity Id", None) + # 5) Set the Intersection Filter's Shape Entity Id to the Box Shape entity + spawner_entity.get_set_test(3, "Configuration|Shape Entity Id", box_id) - # Validate instance counts. Instances should now fill the entire spawner_entity's area - num_expected = 20 * 20 - success = self.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, - num_expected), 5.0) - self.test_success = success and self.test_success + # Validate instance counts. Instances should only plant in the Box Shape area + num_expected = 49 + success = helper.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, + num_expected), 5.0) + Report.result(Tests.instance_count_in_box_shape, success) + + # 6) Set the Intersection Filter's Shape Entity Id to the Cylinder Shape entity + spawner_entity.get_set_test(3, "Configuration|Shape Entity Id", cylinder_id) + + # Validate instance counts. Instances should only plant in the Cylinder Shape area + num_expected = 121 + success = helper.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, + num_expected), 5.0) + Report.result(Tests.instance_count_in_cylinder_shape, success) + + # 7) Clear the Intersection Filter's Shape Entity Id reference + spawner_entity.get_set_test(3, "Configuration|Shape Entity Id", None) + + # Validate instance counts. Instances should now fill the entire spawner_entity's area + num_expected = 20 * 20 + success = helper.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, + num_expected), 5.0) + Report.result(Tests.unfiltered_instance_count, success) -test = TestShapeIntersectionFilter() -test.run() +if __name__ == "__main__": + + from editor_python_test_tools.utils import Report + Report.start_test(ShapeIntersectionFilter_InstancesPlantInAssignedShape) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/SlopeAlignmentModifierOverrides_InstanceSurfaceAlignment.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/SlopeAlignmentModifierOverrides_InstanceSurfaceAlignment.py index abd3dc35d5..5855972f0c 100755 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/SlopeAlignmentModifierOverrides_InstanceSurfaceAlignment.py +++ b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/SlopeAlignmentModifierOverrides_InstanceSurfaceAlignment.py @@ -5,121 +5,132 @@ For complete copyright and license terms please see the LICENSE at the root of t SPDX-License-Identifier: Apache-2.0 OR MIT """ -import os -from math import radians -import sys -import azlmbr.areasystem as areasystem -import azlmbr.asset as asset -import azlmbr.bus as bus -import azlmbr.components as components -import azlmbr.legacy.general as general -import azlmbr.math as math -import azlmbr.paths - -sys.path.append(os.path.join(azlmbr.paths.devroot, 'AutomatedTesting', 'Gem', 'PythonTests')) -import editor_python_test_tools.hydra_editor_utils as hydra -from editor_python_test_tools.editor_test_helper import EditorTestHelper -from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg +class Tests: + surface_entity_created = ( + "Successfully created Surface entity", + "Failed to create Surface entity" + ) + instance_count = ( + "Found the expected number of instances", + "Unexpected number of instances found" + ) + instances_aligned_0 = ( + "All instances are pointed straight up", + "Found instances not aligned to surface pointing straight up" + ) + instances_aligned_1 = ( + "All instances are planted perpendicularly to the surface", + "Found instances not aligned to surface perpendicularly" + ) -class TestSlopeAlignmentModifierOverrides(EditorTestHelper): - def __init__(self): - EditorTestHelper.__init__(self, log_prefix="SlopeAlignmentModifierOverrides", args=["level"]) +def SlopeAlignmentModifierOverrides_InstanceSurfaceAlignment(): + """ + Summary: + Verifies instances properly align to surfaces based on configuration of descriptor overrides of the + Vegetation Slope Alignment Modifier component. - def run_test(self): - """ - Summary: - C4814459 Verifies instances properly align to surfaces based on configuration of descriptor overrides of the - Vegetation Slope Alignment Modifier component. + :return: None + """ - :return: None - """ + import os + from math import radians - def verify_proper_alignment(instance, rot_degrees_vec): - expected_rotation = math.Quaternion() - expected_rotation.SetFromEulerDegrees(rot_degrees_vec) - if instance.alignment.IsClose(expected_rotation): - return True - self.log(f"Expected rotation of {expected_rotation}, Found {instance.alignment}") - return False + import azlmbr.areasystem as areasystem + import azlmbr.asset as asset + import azlmbr.bus as bus + import azlmbr.components as components + import azlmbr.legacy.general as general + import azlmbr.math as math - # Create empty level - self.test_success = self.create_level( - self.args["level"], - heightmap_resolution=1024, - heightmap_meters_per_pixel=1, - terrain_texture_resolution=4096, - use_terrain=False, - ) + import editor_python_test_tools.hydra_editor_utils as hydra + from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg + from editor_python_test_tools.utils import Report + from editor_python_test_tools.utils import TestHelper as helper - general.set_current_view_position(512.0, 480.0, 38.0) + def verify_proper_alignment(instance, rot_degrees_vec): + expected_rotation = math.Quaternion() + expected_rotation.SetFromEulerDegrees(rot_degrees_vec) + if instance.alignment.IsClose(expected_rotation): + return True + Report.info(f"Expected rotation of {expected_rotation}, Found {instance.alignment}") + return False - # Create a spawner entity setup with all needed components - center_point = math.Vector3(512.0, 512.0, 32.0) - asset_path = os.path.join("Slices", "PinkFlower.dynamicslice") - spawner_entity = dynveg.create_vegetation_area("Instance Spawner", center_point, 16.0, 16.0, 32.0, asset_path) + # Open an existing simple level + helper.init_idle() + helper.open_level("Physics", "Base") - # Create a sloped mesh surface for the instances to plant on - center_point = math.Vector3(502.0, 512.0, 24.0) - mesh_asset_path = os.path.join("objects", "_primitives", "_box_1x1.azmodel") - mesh_asset = asset.AssetCatalogRequestBus(bus.Broadcast, "GetAssetIdByPath", mesh_asset_path, math.Uuid(), - False) - rotation = math.Vector3(0.0, radians(45.0), 0.0) - surface_entity = hydra.Entity("Surface Entity") - surface_entity.create_entity( - center_point, - ["Mesh", "Mesh Surface Tag Emitter"] - ) - if surface_entity.id.IsValid(): - print(f"'{surface_entity.name}' created") - hydra.get_set_test(surface_entity, 0, "Controller|Configuration|Mesh Asset", mesh_asset) - components.TransformBus(bus.Event, "SetLocalRotation", surface_entity.id, rotation) - components.TransformBus(bus.Event, "SetLocalUniformScale", surface_entity.id, 30.0) + general.set_current_view_position(512.0, 480.0, 38.0) - # Add a Vegetation Debugger component to allow refreshing instances - hydra.add_level_component("Vegetation Debugger") + # Create a spawner entity setup with all needed components + center_point = math.Vector3(512.0, 512.0, 32.0) + asset_path = os.path.join("Slices", "PinkFlower.dynamicslice") + spawner_entity = dynveg.create_vegetation_area("Instance Spawner", center_point, 16.0, 16.0, 32.0, asset_path) - # Add Vegetation Slope Alignment Modifier to the spawner entity and toggle on Allow Per-Item Overrides - spawner_entity.add_component("Vegetation Slope Alignment Modifier") - spawner_entity.get_set_test(3, "Configuration|Allow Per-Item Overrides", True) + # Create a sloped mesh surface for the instances to plant on + center_point = math.Vector3(502.0, 512.0, 24.0) + mesh_asset_path = os.path.join("objects", "_primitives", "_box_1x1.azmodel") + mesh_asset = asset.AssetCatalogRequestBus(bus.Broadcast, "GetAssetIdByPath", mesh_asset_path, math.Uuid(), + False) + rotation = math.Vector3(0.0, radians(45.0), 0.0) + surface_entity = hydra.Entity("Surface Entity") + surface_entity.create_entity( + center_point, + ["Mesh", "Mesh Surface Tag Emitter"] + ) + Report.critical_result(Tests.surface_entity_created, surface_entity.id.IsValid()) + hydra.get_set_test(surface_entity, 0, "Controller|Configuration|Mesh Asset", mesh_asset) + components.TransformBus(bus.Event, "SetLocalRotation", surface_entity.id, rotation) + components.TransformBus(bus.Event, "SetLocalUniformScale", surface_entity.id, 30.0) - # Toggle on Surface Slope Alignment Override Enabled on the Vegetation Asset List component - spawner_entity.get_set_test(2, "Configuration|Embedded Assets|[0]|Surface Slope Alignment|Override Enabled", - True) + # Add a Vegetation Debugger component to allow refreshing instances + hydra.add_level_component("Vegetation Debugger") - # Set Surface Slope Alignment Override Min and Max to 0 and validate instance alignment - spawner_entity.get_set_test(2, "Configuration|Embedded Assets|[0]|Surface Slope Alignment|Max", 0.0) - spawner_entity.get_set_test(2, "Configuration|Embedded Assets|[0]|Surface Slope Alignment|Max", 0.0) + # Add Vegetation Slope Alignment Modifier to the spawner entity and toggle on Allow Per-Item Overrides + spawner_entity.add_component("Vegetation Slope Alignment Modifier") + spawner_entity.get_set_test(3, "Configuration|Allow Per-Item Overrides", True) - # Verify instances are have planted and are aligned to slope as expected - num_expected = 20 * 20 - self.test_success = self.test_success and self.wait_for_condition( - lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, num_expected), 5.0) + # Toggle on Surface Slope Alignment Override Enabled on the Vegetation Asset List component + spawner_entity.get_set_test(2, "Configuration|Embedded Assets|[0]|Surface Slope Alignment|Override Enabled", + True) - box = azlmbr.shape.ShapeComponentRequestsBus(bus.Event, 'GetEncompassingAabb', spawner_entity.id) - instances = areasystem.AreaSystemRequestBus(bus.Broadcast, 'GetInstancesInAabb', box) + # Set Surface Slope Alignment Override Min and Max to 0 and validate instance alignment + spawner_entity.get_set_test(2, "Configuration|Embedded Assets|[0]|Surface Slope Alignment|Max", 0.0) + spawner_entity.get_set_test(2, "Configuration|Embedded Assets|[0]|Surface Slope Alignment|Max", 0.0) - if self.test_success and num_expected == len(instances): - for instance in instances: - self.test_success = verify_proper_alignment(instance, - math.Vector3(0.0, 0.0, 0.0)) and self.test_success + # Verify instances are have planted and are aligned to slope as expected + num_expected = 20 * 20 + instances_planted = helper.wait_for_condition( + lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, num_expected), 5.0) + Report.critical_result(Tests.instance_count, instances_planted) - # Set Surface Slope Alignment Min and Max to 1 and validate instance alignment - spawner_entity.get_set_test(2, "Configuration|Embedded Assets|[0]|Surface Slope Alignment|Min", 1.0) - spawner_entity.get_set_test(2, "Configuration|Embedded Assets|[0]|Surface Slope Alignment|Max", 1.0) - general.run_console('veg_debugClearAllAreas') - self.test_success = self.test_success and self.wait_for_condition( - lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, num_expected), 5.0) + box = azlmbr.shape.ShapeComponentRequestsBus(bus.Event, 'GetEncompassingAabb', spawner_entity.id) + instances = areasystem.AreaSystemRequestBus(bus.Broadcast, 'GetInstancesInAabb', box) - box = azlmbr.shape.ShapeComponentRequestsBus(bus.Event, 'GetEncompassingAabb', spawner_entity.id) - instances = areasystem.AreaSystemRequestBus(bus.Broadcast, 'GetInstancesInAabb', box) + success = True + for instance in instances: + success = verify_proper_alignment(instance, math.Vector3(0.0, 0.0, 0.0)) + Report.result(Tests.instances_aligned_0, success) - if self.test_success and num_expected == len(instances): - for instance in instances: - self.test_success = verify_proper_alignment(instance, math.Vector3(0.0, 45.0, 0.0)) and \ - self.test_success + # Set Surface Slope Alignment Min and Max to 1 and validate instance alignment + spawner_entity.get_set_test(2, "Configuration|Embedded Assets|[0]|Surface Slope Alignment|Min", 1.0) + spawner_entity.get_set_test(2, "Configuration|Embedded Assets|[0]|Surface Slope Alignment|Max", 1.0) + general.run_console('veg_debugClearAllAreas') + instances_planted = helper.wait_for_condition( + lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, num_expected), 5.0) + Report.critical_result(Tests.instance_count, instances_planted) + + box = azlmbr.shape.ShapeComponentRequestsBus(bus.Event, 'GetEncompassingAabb', spawner_entity.id) + instances = areasystem.AreaSystemRequestBus(bus.Broadcast, 'GetInstancesInAabb', box) + + success = True + for instance in instances: + success = verify_proper_alignment(instance, math.Vector3(0.0, 45.0, 0.0)) + Report.result(Tests.instances_aligned_1, success) -test = TestSlopeAlignmentModifierOverrides() -test.run() +if __name__ == "__main__": + + from editor_python_test_tools.utils import Report + Report.start_test(SlopeAlignmentModifierOverrides_InstanceSurfaceAlignment) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/SlopeAlignmentModifier_InstanceSurfaceAlignment.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/SlopeAlignmentModifier_InstanceSurfaceAlignment.py index 6e9e36957f..11427b9b0e 100755 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/SlopeAlignmentModifier_InstanceSurfaceAlignment.py +++ b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/SlopeAlignmentModifier_InstanceSurfaceAlignment.py @@ -5,127 +5,139 @@ For complete copyright and license terms please see the LICENSE at the root of t SPDX-License-Identifier: Apache-2.0 OR MIT """ -import os -from math import radians -import sys -import azlmbr.areasystem as areasystem -import azlmbr.asset as asset -import azlmbr.bus as bus -import azlmbr.components as components -import azlmbr.editor as editor -import azlmbr.legacy.general as general -import azlmbr.math as math -import azlmbr.paths - -sys.path.append(os.path.join(azlmbr.paths.devroot, 'AutomatedTesting', 'Gem', 'PythonTests')) -import editor_python_test_tools.hydra_editor_utils as hydra -from editor_python_test_tools.editor_test_helper import EditorTestHelper -from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg +class Tests: + surface_entity_created = ( + "Successfully created Surface entity", + "Failed to create Surface entity" + ) + instance_count = ( + "Found the expected number of instances", + "Unexpected number of instances found" + ) + instances_aligned_1 = ( + "All instances are planted perpendicularly to the surface", + "Found instances not aligned to surface perpendicularly" + ) + instances_aligned_0 = ( + "All instances are pointed straight up", + "Found instances not aligned to surface pointing straight up" + ) -class TestSlopeAlignmentModifier(EditorTestHelper): - def __init__(self): - EditorTestHelper.__init__(self, log_prefix="SlopeAlignmentModifier", args=["level"]) +def SlopeAlignmentModifier_InstanceSurfaceAlignment(): + """ + Summary: + Verifies instances properly align to surfaces based on configuration of the Vegetation Slope Alignment + Modifier. - def run_test(self): - """ - Summary: - C4896941 Verifies instances properly align to surfaces based on configuration of the Vegetation Slope Alignment - Modifier. + :return: None + """ - :return: None - """ + import os + from math import radians - def verify_proper_alignment(instance, rot_degrees_vec): - expected_rotation = math.Quaternion() - expected_rotation.SetFromEulerDegrees(rot_degrees_vec) - if instance.alignment.IsClose(expected_rotation): - return True - self.log(f"Expected rotation of {expected_rotation}, Found {instance.alignment}") - return False + import azlmbr.areasystem as areasystem + import azlmbr.asset as asset + import azlmbr.bus as bus + import azlmbr.components as components + import azlmbr.editor as editor + import azlmbr.legacy.general as general + import azlmbr.math as math - # Create empty level - self.test_success = self.create_level( - self.args["level"], - heightmap_resolution=1024, - heightmap_meters_per_pixel=1, - terrain_texture_resolution=4096, - use_terrain=False, - ) + import editor_python_test_tools.hydra_editor_utils as hydra + from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg + from editor_python_test_tools.utils import Report + from editor_python_test_tools.utils import TestHelper as helper - general.set_current_view_position(512.0, 480.0, 38.0) + def verify_proper_alignment(instance, rot_degrees_vec): + expected_rotation = math.Quaternion() + expected_rotation.SetFromEulerDegrees(rot_degrees_vec) + if instance.alignment.IsClose(expected_rotation): + return True + Report.info(f"Expected rotation of {expected_rotation}, Found {instance.alignment}") + return False - # Create a spawner entity setup with all needed components - center_point = math.Vector3(512.0, 512.0, 32.0) - asset_path = os.path.join("Slices", "PinkFlower.dynamicslice") - spawner_entity = dynveg.create_vegetation_area("Instance Spawner", center_point, 16.0, 16.0, 32.0, asset_path) + # Open an existing simple level + helper.init_idle() + helper.open_level("Physics", "Base") - # Create a sloped mesh surface for the instances to plant on - center_point = math.Vector3(502.0, 512.0, 24.0) - mesh_asset_path = os.path.join("objects", "_primitives", "_box_1x1.azmodel") - mesh_asset = asset.AssetCatalogRequestBus(bus.Broadcast, "GetAssetIdByPath", mesh_asset_path, math.Uuid(), - False) - rotation = math.Vector3(0.0, radians(45.0), 0.0) - surface_entity = hydra.Entity("Surface Entity") - surface_entity.create_entity( - center_point, - ["Mesh", "Mesh Surface Tag Emitter"] - ) - if surface_entity.id.IsValid(): - print(f"'{surface_entity.name}' created") - hydra.get_set_test(surface_entity, 0, "Controller|Configuration|Mesh Asset", mesh_asset) - components.TransformBus(bus.Event, "SetLocalRotation", surface_entity.id, rotation) - components.TransformBus(bus.Event, "SetLocalUniformScale", surface_entity.id, 30.0) + general.set_current_view_position(512.0, 480.0, 38.0) - # Add a Vegetation Debugger component to allow refreshing instances - hydra.add_level_component("Vegetation Debugger") + # Create a spawner entity setup with all needed components + center_point = math.Vector3(512.0, 512.0, 32.0) + asset_path = os.path.join("Slices", "PinkFlower.dynamicslice") + spawner_entity = dynveg.create_vegetation_area("Instance Spawner", center_point, 16.0, 16.0, 32.0, asset_path) - # Add Vegetation Slope Alignment Modifier to the spawner entity - spawner_entity.add_component("Vegetation Slope Alignment Modifier") + # Create a sloped mesh surface for the instances to plant on + center_point = math.Vector3(502.0, 512.0, 24.0) + mesh_asset_path = os.path.join("objects", "_primitives", "_box_1x1.azmodel") + mesh_asset = asset.AssetCatalogRequestBus(bus.Broadcast, "GetAssetIdByPath", mesh_asset_path, math.Uuid(), + False) + rotation = math.Vector3(0.0, radians(45.0), 0.0) + surface_entity = hydra.Entity("Surface Entity") + surface_entity.create_entity( + center_point, + ["Mesh", "Mesh Surface Tag Emitter"] + ) + Report.critical_result(Tests.surface_entity_created, surface_entity.id.IsValid()) + hydra.get_set_test(surface_entity, 0, "Controller|Configuration|Mesh Asset", mesh_asset) + components.TransformBus(bus.Event, "SetLocalRotation", surface_entity.id, rotation) + components.TransformBus(bus.Event, "SetLocalUniformScale", surface_entity.id, 30.0) - # Set Alignment Coefficient Min/Max to 1 on the Slope Alignment Modifier - spawner_entity.get_set_test(3, "Configuration|Alignment Coefficient Min", 1.0) - spawner_entity.get_set_test(3, "Configuration|Alignment Coefficient Max", 1.0) + # Add a Vegetation Debugger component to allow refreshing instances + hydra.add_level_component("Vegetation Debugger") - # Create new child entity with a Constant Gradient - child_vegetation_id = editor.ToolsApplicationRequestBus(bus.Broadcast, "CreateNewEntity", spawner_entity.id) - child_vegetation = hydra.Entity("Child Vegetation Entity", child_vegetation_id) - components_to_add = ["Constant Gradient"] - child_vegetation.components = [] - for component in components_to_add: - child_vegetation.components.append(hydra.add_component(component, child_vegetation_id)) + # Add Vegetation Slope Alignment Modifier to the spawner entity + spawner_entity.add_component("Vegetation Slope Alignment Modifier") - # Reference the Constant Gradient on the Slope Alignment Modifier component - spawner_entity.get_set_test(3, "Configuration|Gradient|Gradient Entity Id", child_vegetation_id) + # Set Alignment Coefficient Min/Max to 1 on the Slope Alignment Modifier + spawner_entity.get_set_test(3, "Configuration|Alignment Coefficient Min", 1.0) + spawner_entity.get_set_test(3, "Configuration|Alignment Coefficient Max", 1.0) - # Verify instances are have planted and are aligned to slope as expected - num_expected = 20 * 20 - self.test_success = self.test_success and self.wait_for_condition( - lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, num_expected), 5.0) + # Create new child entity with a Constant Gradient + child_vegetation_id = editor.ToolsApplicationRequestBus(bus.Broadcast, "CreateNewEntity", spawner_entity.id) + child_vegetation = hydra.Entity("Child Vegetation Entity", child_vegetation_id) + components_to_add = ["Constant Gradient"] + child_vegetation.components = [] + for component in components_to_add: + child_vegetation.components.append(hydra.add_component(component, child_vegetation_id)) - box = azlmbr.shape.ShapeComponentRequestsBus(bus.Event, 'GetEncompassingAabb', spawner_entity.id) - instances = areasystem.AreaSystemRequestBus(bus.Broadcast, 'GetInstancesInAabb', box) + # Reference the Constant Gradient on the Slope Alignment Modifier component + spawner_entity.get_set_test(3, "Configuration|Gradient|Gradient Entity Id", child_vegetation_id) - if self.test_success and num_expected == len(instances): - for instance in instances: - self.test_success = verify_proper_alignment(instance, math.Vector3(0.0, 45.0, 0.0)) and \ - self.test_success + # Verify instances are have planted and are aligned to slope as expected + num_expected = 20 * 20 + instances_planted = helper.wait_for_condition( + lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, num_expected), 5.0) + Report.critical_result(Tests.instance_count, instances_planted) - # Change Min/Max to 0.0 and verify proper alignment - spawner_entity.get_set_test(3, "Configuration|Alignment Coefficient Min", 0.0) - spawner_entity.get_set_test(3, "Configuration|Alignment Coefficient Max", 0.0) - general.run_console('veg_debugClearAllAreas') - self.test_success = self.test_success and self.wait_for_condition( - lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, num_expected), 5.0) + box = azlmbr.shape.ShapeComponentRequestsBus(bus.Event, 'GetEncompassingAabb', spawner_entity.id) + instances = areasystem.AreaSystemRequestBus(bus.Broadcast, 'GetInstancesInAabb', box) - box = azlmbr.shape.ShapeComponentRequestsBus(bus.Event, 'GetEncompassingAabb', spawner_entity.id) - instances = areasystem.AreaSystemRequestBus(bus.Broadcast, 'GetInstancesInAabb', box) + success = True + for instance in instances: + success = verify_proper_alignment(instance, math.Vector3(0.0, 45.0, 0.0)) + Report.result(Tests.instances_aligned_1, success) - if self.test_success and num_expected == len(instances): - for instance in instances: - self.test_success = verify_proper_alignment(instance, math.Vector3(0.0, 0.0, 0.0)) and self.test_success + # Change Min/Max to 0.0 and verify proper alignment + spawner_entity.get_set_test(3, "Configuration|Alignment Coefficient Min", 0.0) + spawner_entity.get_set_test(3, "Configuration|Alignment Coefficient Max", 0.0) + general.run_console('veg_debugClearAllAreas') + instances_planted = helper.wait_for_condition( + lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, num_expected), 5.0) + Report.critical_result(Tests.instance_count, instances_planted) + + box = azlmbr.shape.ShapeComponentRequestsBus(bus.Event, 'GetEncompassingAabb', spawner_entity.id) + instances = areasystem.AreaSystemRequestBus(bus.Broadcast, 'GetInstancesInAabb', box) + + success = True + for instance in instances: + success = verify_proper_alignment(instance, math.Vector3(0.0, 0.0, 0.0)) + Report.result(Tests.instances_aligned_0, success) -test = TestSlopeAlignmentModifier() -test.run() +if __name__ == "__main__": + + from editor_python_test_tools.utils import Report + Report.start_test(SlopeAlignmentModifier_InstanceSurfaceAlignment) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/SlopeFilter_ComponentAndOverrides_InstancesPlantOnValidSlope.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/SlopeFilter_ComponentAndOverrides_InstancesPlantOnValidSlope.py index d66716cee3..ee0851d552 100755 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/SlopeFilter_ComponentAndOverrides_InstancesPlantOnValidSlope.py +++ b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/SlopeFilter_ComponentAndOverrides_InstancesPlantOnValidSlope.py @@ -5,121 +5,122 @@ For complete copyright and license terms please see the LICENSE at the root of t SPDX-License-Identifier: Apache-2.0 OR MIT """ -""" -C4874096 - Slope Min/Max properties can be set, and properly affect planted vegetation -C4814464 - Slope Filter overrides function as expected -""" -import os -import sys - -import azlmbr.bus as bus -import azlmbr.editor as editor -import azlmbr.legacy.general as general -import azlmbr.math as math -import azlmbr.paths - -sys.path.append(os.path.join(azlmbr.paths.devroot, "AutomatedTesting", "Gem", "PythonTests")) -import editor_python_test_tools.hydra_editor_utils as hydra -from editor_python_test_tools.editor_test_helper import EditorTestHelper -from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg +class Tests: + prefilter_instance_count = ( + "Found the expected number of instances before applying the Slope Filter", + "Found an unexpected number of instances before applying the Slope Filter" + ) + postfilter_instance_count = ( + "Found the expected number of instances after applying the Slope Filter", + "Found an unexpected number of instances after applying the Slope Filter" + ) + postfilter_overrides_instance_count = ( + "Found the expected number of instances after applying descriptor overrides to the Slope Filter", + "Found an unexpected number of instances after applying descriptor overrides to the Slope Filter" + ) -class TestSlopeFilterComponentAndOverrides(EditorTestHelper): - def __init__(self): - EditorTestHelper.__init__(self, log_prefix="SlopeFilter_InstancesPlantOnValidSlope", args=["level"]) +def SlopeFilter_ComponentAndOverrides_InstancesPlantOnValidSlopes(): + """ + Summary: + An existing level is opened. A spawner entity is added, along with a flat planting surface at 32 on Z, and sphere + mesh at 38 on Z to provide a sloped surface. A Slope Filter is added to the spawner entity, and Slope Min/Max + values are set. Instance counts are validated. The same test is then performed for Slope Filter overrides. - def run_test(self): - """ - Summary: - A new level is created. A spawner entity is added, along with a flat planting surface at 32 on Z, and sphere - mesh at 38 on Z to provide a sloped surface. A Slope Filter is added to the spawner entity, and Slope Min/Max - values are set. Instance counts are validated. The same test is then performed for Slope Filter overrides. + Expected Behavior: + Instances plant only on surfaces that fall between the Slope Filter Min/Max settings - Expected Behavior: - Instances plant only on surfaces that fall between the Slope Filter Min/Max settings + Test Steps: + 1) Open an existing level + 2) Create an instance spawner entity + 3) Create surfaces to plant on, one at 32 on Z and another sloped surface at 38 on Z. + 4) Initial instance counts pre-filter are verified. + 5) Slope Min/Max values are set on the Slope Filter component + 6) Instance counts are validated + 7) Setup for overrides tests + 8) Slope Min/Max values are set on the descriptor overrides + 9) Instance counts are validated - Test Steps: - 1) Create a new level - 2) Create an instance spawner entity - 3) Create surfaces to plant on, one at 32 on Z and another sloped surface at 38 on Z. - 4) Initial instance counts pre-filter are verified. - 5) Slope Min/Max values are set on the Slope Filter component - 6) Instance counts are validated - 7) Setup for overrides tests - 8) Slope Min/Max values are set on the descriptor overrides - 9) Instance counts are validated + Note: + - This test file must be called from the Open 3D Engine Editor command terminal + - Any passed and failed tests are written to the Editor.log file. + Parsing the file or running a log_monitor are required to observe the test results. - Note: - - This test file must be called from the Open 3D Engine Editor command terminal - - Any passed and failed tests are written to the Editor.log file. - Parsing the file or running a log_monitor are required to observe the test results. + :return: None + """ - :return: None - """ + import os - # 1) Create a new, temporary level - self.test_success = self.create_level( - self.args["level"], - heightmap_resolution=1024, - heightmap_meters_per_pixel=1, - terrain_texture_resolution=4096, - use_terrain=False, - ) + import azlmbr.bus as bus + import azlmbr.editor as editor + import azlmbr.legacy.general as general + import azlmbr.math as math - # Set view of planting area for visual debugging - general.set_current_view_position(512.0, 475.0, 38.0) + import editor_python_test_tools.hydra_editor_utils as hydra + from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg + from editor_python_test_tools.utils import Report + from editor_python_test_tools.utils import TestHelper as helper - # 2) Create a new entity with required vegetation area components - center_point = math.Vector3(512.0, 512.0, 32.0) - asset_path = os.path.join("Slices", "PinkFlower.dynamicslice") - spawner_entity = dynveg.create_vegetation_area("Instance Spawner", center_point, 32.0, 32.0, 32.0, asset_path) + # 1) Open an existing simple level + helper.init_idle() + helper.open_level("Physics", "Base") - # Add a Vegetation Slope Filter - spawner_entity.add_component("Vegetation Slope Filter") + # Set view of planting area for visual debugging + general.set_current_view_position(512.0, 475.0, 38.0) - # 3) Add surfaces to plant on. This will include a flat surface and a sphere mesh to provide a sloped surface - dynveg.create_surface_entity("Planting Surface", center_point, 32.0, 32.0, 1.0) - sloped_surface_center = math.Vector3(512.0, 512.0, 38.0) - dynveg.create_mesh_surface_entity_with_slopes("Sloped Planting Surface", sloped_surface_center, 10.0) + # 2) Create a new entity with required vegetation area components + center_point = math.Vector3(512.0, 512.0, 32.0) + asset_path = os.path.join("Slices", "PinkFlower.dynamicslice") + spawner_entity = dynveg.create_vegetation_area("Instance Spawner", center_point, 32.0, 32.0, 32.0, asset_path) - # Set instances to spawn on a center snap point to avoid unexpected instances around the edges of the box shape - veg_system_settings_component = hydra.add_level_component("Vegetation System Settings") - editor.EditorComponentAPIBus(bus.Broadcast, "SetComponentProperty", veg_system_settings_component, - 'Configuration|Area System Settings|Sector Point Snap Mode', 1) + # Add a Vegetation Slope Filter + spawner_entity.add_component("Vegetation Slope Filter") - # 4) Validate instance counts pre-filter - num_expected_flat_surface = 40 * 40 # 20x20 instances per 16m - num_expected_slopes_pre_filter = 120 # Unfiltered planting on the top of the sphere mesh - num_expected = num_expected_flat_surface + num_expected_slopes_pre_filter - initial_success = self.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape( - spawner_entity.id, num_expected), 5.0) - self.test_success = initial_success and self.test_success + # 3) Add surfaces to plant on. This will include a flat surface and a sphere mesh to provide a sloped surface + dynveg.create_surface_entity("Planting Surface", center_point, 32.0, 32.0, 1.0) + sloped_surface_center = math.Vector3(512.0, 512.0, 38.0) + dynveg.create_mesh_surface_entity_with_slopes("Sloped Planting Surface", sloped_surface_center, 10.0) - # 5) Change Slope Min/Max on the Vegetation Slope Filter component - spawner_entity.get_set_test(3, "Configuration|Slope Min", 20) - spawner_entity.get_set_test(3, "Configuration|Slope Max", 45) + # Set instances to spawn on a center snap point to avoid unexpected instances around the edges of the box shape + veg_system_settings_component = hydra.add_level_component("Vegetation System Settings") + editor.EditorComponentAPIBus(bus.Broadcast, "SetComponentProperty", veg_system_settings_component, + 'Configuration|Area System Settings|Sector Point Snap Mode', 1) - # 6) Validate instance counts post-filter: instances should only plant on slopes between 20-45 degrees - num_expected_slopes_post_filter = 48 - slope_min_max_success = self.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape( - spawner_entity.id, num_expected_slopes_post_filter), 5.0) - self.test_success = slope_min_max_success and self.test_success + # 4) Validate instance counts pre-filter + num_expected_flat_surface = 40 * 40 # 20x20 instances per 16m + num_expected_slopes_pre_filter = 120 # Unfiltered planting on the top of the sphere mesh + num_expected = num_expected_flat_surface + num_expected_slopes_pre_filter + initial_success = helper.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape( + spawner_entity.id, num_expected), 5.0) + Report.result(Tests.prefilter_instance_count, initial_success) - # 7) Setup for overrides on the Slope Filter component and the spawner entity's descriptor - spawner_entity.get_set_test(3, "Configuration|Allow Per-Item Overrides", True) - spawner_entity.get_set_test(2, "Configuration|Embedded Assets|[0]|Slope Filter|Override Enabled", True) + # 5) Change Slope Min/Max on the Vegetation Slope Filter component + spawner_entity.get_set_test(3, "Configuration|Slope Min", 20) + spawner_entity.get_set_test(3, "Configuration|Slope Max", 45) - # 8) Set Slope Filter Min/Max overrides on the spawner entity's descriptor - spawner_entity.get_set_test(2, "Configuration|Embedded Assets|[0]|Slope Filter|Min", 5) - spawner_entity.get_set_test(2, "Configuration|Embedded Assets|[0]|Slope Filter|Max", 20) + # 6) Validate instance counts post-filter: instances should only plant on slopes between 20-45 degrees + num_expected_slopes_post_filter = 48 + slope_min_max_success = helper.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape( + spawner_entity.id, num_expected_slopes_post_filter), 5.0) + Report.result(Tests.postfilter_instance_count, slope_min_max_success) - # 9) Validate instance counts post-filter: instances should only plant on slopes between 5-20 degrees - num_expected_slopes_post_filter_overrides = 12 - overrides_min_max_success = self.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape( - spawner_entity.id, num_expected_slopes_post_filter_overrides), 5.0) - self.test_success = overrides_min_max_success and self.test_success + # 7) Setup for overrides on the Slope Filter component and the spawner entity's descriptor + spawner_entity.get_set_test(3, "Configuration|Allow Per-Item Overrides", True) + spawner_entity.get_set_test(2, "Configuration|Embedded Assets|[0]|Slope Filter|Override Enabled", True) + + # 8) Set Slope Filter Min/Max overrides on the spawner entity's descriptor + spawner_entity.get_set_test(2, "Configuration|Embedded Assets|[0]|Slope Filter|Min", 5) + spawner_entity.get_set_test(2, "Configuration|Embedded Assets|[0]|Slope Filter|Max", 20) + + # 9) Validate instance counts post-filter: instances should only plant on slopes between 5-20 degrees + num_expected_slopes_post_filter_overrides = 12 + overrides_min_max_success = helper.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape( + spawner_entity.id, num_expected_slopes_post_filter_overrides), 5.0) + Report.result(Tests.postfilter_overrides_instance_count, overrides_min_max_success) -test = TestSlopeFilterComponentAndOverrides() -test.run() +if __name__ == "__main__": + + from editor_python_test_tools.utils import Report + Report.start_test(SlopeFilter_ComponentAndOverrides_InstancesPlantOnValidSlopes) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/SlopeFilter_FilterStageToggle.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/SlopeFilter_FilterStageToggle.py deleted file mode 100755 index 33275f2992..0000000000 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/SlopeFilter_FilterStageToggle.py +++ /dev/null @@ -1,103 +0,0 @@ -""" -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 -""" - -import os -import sys -import azlmbr.math as math -import azlmbr.bus as bus -import azlmbr.paths -import azlmbr.editor as editor -import azlmbr.entity as EntityId -import azlmbr.components as components -import azlmbr.legacy.general as general - -sys.path.append(os.path.join(azlmbr.paths.devroot, "AutomatedTesting", "Gem", "PythonTests")) -import editor_python_test_tools.hydra_editor_utils as hydra -from editor_python_test_tools.editor_test_helper import EditorTestHelper -from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg - -class TestSlopeFilterFilterStageToggle(EditorTestHelper): - def __init__(self): - EditorTestHelper.__init__(self, log_prefix="SlopeFilter_FilterStageToggle", args=["level"]) - - def run_test(self): - """ - Summary: - Filter Stage toggle affects final vegetation position - - Expected Result: - Vegetation instances plant differently depending on the Filter Stage setting. With PreProcess, some vegetation instances can - appear on slopes outside the filtered values. With PostProcess, vegetation instances only appear on the correct slope values. - - :return: None - """ - - # Create empty level - self.test_success = self.create_level( - self.args["level"], - heightmap_resolution=1024, - heightmap_meters_per_pixel=1, - terrain_texture_resolution=4096, - use_terrain=False, - ) - - general.set_current_view_position(512.0, 480.0, 38.0) - - # Create basic vegetation entity - position = math.Vector3(512.0, 512.0, 32.0) - asset_path = os.path.join("Slices", "PinkFlower.dynamicslice") - vegetation = dynveg.create_vegetation_area("vegetation", position, 16.0, 16.0, 16.0, asset_path) - - # Create Surface for instances to plant on - dynveg.create_surface_entity("Surface_Entity_Parent", position, 16.0, 16.0, 1.0) - - # Add a Vegetation Shape Intersection Filter to the vegetation area entity - vegetation.add_component("Vegetation Shape Intersection Filter") - - # Create a new entity as a child of the vegetation area entity with Box Shape - box = hydra.Entity("box") - box.create_entity(position, ["Box Shape"]) - box.get_set_test(0, "Box Shape|Box Configuration|Dimensions", math.Vector3(8.0, 8.0, 1.0)) - - # Create a new entity as a child of the vegetation area entity with Cylinder Shape. - cylinder = hydra.Entity("cylinder") - cylinder.create_entity(position, ["Cylinder Shape"]) - cylinder.get_set_test(0, "Cylinder Shape|Cylinder Configuration|Radius", 5.0) - cylinder.get_set_test(0, "Cylinder Shape|Cylinder Configuration|Height", 5.0) - box.set_test_parent_entity(vegetation) - cylinder.set_test_parent_entity(vegetation) - - # # On the Vegetation Shape Intersection Filter component, click the crosshair button, and add child entities one by one - vegetation.get_set_test(3, "Configuration|Shape Entity Id", box.id) - result = self.wait_for_condition(lambda: dynveg.validate_instance_count(position, 8.0, 100), 2.0) - self.log(f"Vegetation plant only in the areas where the Box overlaps with the vegetation area's boundaries: {result}") - vegetation.get_set_test(3, "Configuration|Shape Entity Id", cylinder.id) - result = self.wait_for_condition(lambda: dynveg.validate_instance_count(position, 5.0, 100), 2.0) - self.log(f"Vegetation plant only in the areas where the Cylinder overlaps with the vegetation area's boundaries: {result}") - - # Create a new entity as a child of the vegetation area entity with Random Noise Gradient Generator, Gradient Transform Modifier, - # and Box Shape component - random_noise = hydra.Entity("random_noise") - random_noise.create_entity(position, ["Random Noise Gradient", "Gradient Transform Modifier", "Box Shape"]) - random_noise.set_test_parent_entity(vegetation) - - # Add a Vegetation Position Modifier to the vegetation area entity - vegetation.add_component("Vegetation Position Modifier") - - # Pin the Random Noise entity to the Gradient Entity Id field of the Position Modifier's Gradient X - vegetation.get_set_test(4, "Configuration|Position X|Gradient|Gradient Entity Id", random_noise.id) - - # Toggle between PreProcess and PostProcess - vegetation.get_set_test(3, "Configuration|Filter Stage", 1) - result = self.wait_for_condition(lambda: dynveg.validate_instance_count(position, 5.0, 117), 2.0) - self.log(f"Vegetation instances count equal to expected value for PREPROCESS filter stage: {result}") - vegetation.get_set_test(3, "Configuration|Filter Stage", 2) - result = self.wait_for_condition(lambda: dynveg.validate_instance_count(position, 5.0, 122), 2.0) - self.log(f"Vegetation instances count equal to expected value for POSTPROCESS filter stage: {result}") - -test = TestSlopeFilterFilterStageToggle() -test.run() \ No newline at end of file diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/SpawnerSlices_SliceCreationAndVisibilityToggleWorks.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/SpawnerSlices_SliceCreationAndVisibilityToggleWorks.py new file mode 100644 index 0000000000..1658ffc532 --- /dev/null +++ b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/SpawnerSlices_SliceCreationAndVisibilityToggleWorks.py @@ -0,0 +1,126 @@ +""" +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 +""" + + +class Tests: + spawner_slice_created = ( + "Spawner slice created successfully", + "Failed to create Spawner slice" + ) + instance_count_unhidden = ( + "Initial instance counts are as expected", + "Found an unexpected number of initial instances" + ) + instance_count_hidden = ( + "Instance counts upon hiding the Spawner slice are as expected", + "Unexpectedly found instances with the Spawner slice hidden" + ) + blender_slice_created = ( + "Blender slice created successfully", + "Failed to create Blender slice" + ) + + +def SpawnerSlices_SliceCreationAndVisibilityToggleWorks(): + """ + Summary: + C2627900 Verifies if a slice containing the component can be created. + C2627905 A slice containing the Vegetation Layer Blender component can be created. + C2627904: Hiding a slice containing the component clears any visuals from the Viewport. + + Expected Result: + C2627900, C2627905: Slice is created, and is properly processed in the Asset Processor. + C2627904: Vegetation area visuals are hidden from the Viewport. + + :return: None + """ + + import os + + import azlmbr.math as math + import azlmbr.legacy.general as general + import azlmbr.slice as slice + import azlmbr.bus as bus + import azlmbr.editor as editor + import azlmbr.asset as asset + + import editor_python_test_tools.hydra_editor_utils as hydra + from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg + from editor_python_test_tools.utils import Report + from editor_python_test_tools.utils import TestHelper as helper + + def path_is_valid_asset(asset_path): + asset_id = asset.AssetCatalogRequestBus(bus.Broadcast, "GetAssetIdByPath", asset_path, math.Uuid(), False) + return asset_id.invoke("IsValid") + + # 1) Open an existing simple level + helper.init_idle() + helper.open_level("Physics", "Base") + general.set_current_view_position(512.0, 480.0, 38.0) + + # 2) C2627900 Verifies if a slice containing the Vegetation Layer Spawner component can be created. + # 2.1) Create basic vegetation entity + position = math.Vector3(512.0, 512.0, 32.0) + asset_path = os.path.join("Slices", "PinkFlower.dynamicslice") + veg_1 = dynveg.create_vegetation_area("vegetation_1", position, 16.0, 16.0, 16.0, asset_path) + + # 2.2) Create slice from the entity + slice_path = os.path.join("slices", "TestSlice_1.slice") + slice.SliceRequestBus(bus.Broadcast, "CreateNewSlice", veg_1.id, slice_path) + + # 2.3) Verify if the slice has been created successfully + spawner_slice_success = helper.wait_for_condition(lambda: path_is_valid_asset(slice_path), 5.0) + Report.result(Tests.spawner_slice_created, spawner_slice_success) + + # 3) C2627904: Hiding a slice containing the component clears any visuals from the Viewport + # 3.1) Create Surface for instances to plant on + dynveg.create_surface_entity("Surface_Entity", position, 16.0, 16.0, 1.0) + + # 3.2) Initially verify instance count before hiding slice + initial_count_success = helper.wait_for_condition(lambda: dynveg.validate_instance_count(position, 16.0, 400), 5.0) + Report.result(Tests.instance_count_unhidden, initial_count_success) + + # 3.3) Hide the slice and verify instance count + editor.EditorEntityAPIBus(bus.Event, "SetVisibilityState", veg_1.id, False) + hidden_instance_count = helper.wait_for_condition(lambda: dynveg.validate_instance_count(position, 16.0, 0), 5.0) + Report.result(Tests.instance_count_hidden, hidden_instance_count) + + # 3.4) Unhide the slice + editor.EditorEntityAPIBus(bus.Event, "SetVisibilityState", veg_1.id, True) + + # 4) C2627905 A slice containing the Vegetation Layer Blender component can be created. + # 4.1) Create another vegetation entity to add to blender component + veg_2 = dynveg.create_vegetation_area("vegetation_2", position, 1.0, 1.0, 1.0, "") + + # 4.2) Create entity with Vegetation Layer Blender + components_to_add = ["Box Shape", "Vegetation Layer Blender"] + blender_entity = hydra.Entity("blender_entity") + blender_entity.create_entity(position, components_to_add) + + # 4.3) Pin both the vegetation areas to the blender entity + pte = hydra.get_property_tree(blender_entity.components[1]) + path = "Configuration|Vegetation Areas" + pte.update_container_item(path, 0, veg_1.id) + pte.add_container_item(path, 1, veg_2.id) + + # 4.4) Drag the simple vegetation areas under the Vegetation Layer Blender entity to create an entity hierarchy. + veg_1.set_test_parent_entity(blender_entity) + veg_2.set_test_parent_entity(blender_entity) + + # 4.5) Create slice from blender entity + slice_path = os.path.join("slices", "TestSlice_2.slice") + slice.SliceRequestBus(bus.Broadcast, "CreateNewSlice", blender_entity.id, slice_path) + + # 4.6) Verify if the slice has been created successfully + blender_slice_success = helper.wait_for_condition(lambda: path_is_valid_asset(slice_path), 5.0) + Report.result(Tests.blender_slice_created, blender_slice_success) + + +if __name__ == "__main__": + + from editor_python_test_tools.utils import Report + Report.start_test(SpawnerSlices_SliceCreationAndVisibilityToggleWorks) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/SurfaceDataRefreshes_RemainsStable.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/SurfaceDataRefreshes_RemainsStable.py index 2f08d3963d..1ad4f305c3 100755 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/SurfaceDataRefreshes_RemainsStable.py +++ b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/SurfaceDataRefreshes_RemainsStable.py @@ -5,99 +5,97 @@ For complete copyright and license terms please see the LICENSE at the root of t SPDX-License-Identifier: Apache-2.0 OR MIT """ -import os -import sys -sys.path.append(os.path.dirname(os.path.abspath(__file__))) -import azlmbr.legacy.general as general -import azlmbr.math as math +class Tests: + editor_remains_stable = ( + "Editor did not crash following rapid surface data updates", + "Editor crashed" + ) -sys.path.append(os.path.join(azlmbr.paths.devroot, 'AutomatedTesting', 'Gem', 'PythonTests')) -from editor_python_test_tools.editor_test_helper import EditorTestHelper -from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg +def SurfaceDataRefreshes_RemainsStable(): + """ + Summary: + The Vegetation Area System can intermittently crash when updating surface data and moving the camera + around rapidly. The situation occurs across multiple frames - the surface data updates, which triggers a bunch + of sector updates getting added to the update queue. Then in a subsequent frame, there is no active vegetation + area or surface data updates, which triggers "delete all sectors". The "delete all" wasn't deleting entries from + the update queue, so any unprocessed updates would continue to get processed. If any of those updates referenced + a sector that no longer exists, because the camera changed position, then it would assert and crash. + + To repro this bug, this test loads an empty level with a large box shape emitting a surface, and then runs a tight + loop of camera movements and "surface changed" events that invalidate all surface points. Because this is a timing + issue, there's no guarantee that the test below will successfully cause the condition to occur, but it successfully + crashed every time it was tested locally prior to the bugfix. + + :return: None + """ + + import azlmbr.legacy.general as general + import azlmbr.math as math + + from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg + from editor_python_test_tools.utils import Report + from editor_python_test_tools.utils import TestHelper as helper + + # 1) Open an existing simple level + helper.init_idle() + helper.open_level("Physics", "Base") + + world_center = math.Vector3(512.0, 512.0, 32.0) + + # Add an entity with a 1024 x 1024 box centered at 512,512. + surface_entity = dynveg.create_surface_entity("Surface Data", world_center, 1024.0, 1024.0, 1.0) + + # Move the camera to the world center + general.set_current_view_position(world_center.x, world_center.y, world_center.z) + + # 2) Perform the test. Since the conditions are extremely timing related, and every machine + # running the test can have different timing conditions, we run through a set of different + # combinations to try and cause the crash under as many scenarios as possible + + loops_per_surface_changed = [3, 5, 5] + loops_per_camera_reset = [20, 20, 20] + camera_speed_per_loop = [10.0, 10.0, 15.0] + + # Setting test success to false to make sure the toggle at the end accurately conveys the loop being successful + test_success = False + + # Loop through all our attempted timing test cases to cause the crash pretty consistently. + for test_case in range(0,3): + Report.info(f'Starting test case {test_case}') + Report.info(f'Loops per surface changed: {loops_per_surface_changed[test_case]}') + Report.info(f'Loops per camera reset: {loops_per_camera_reset[test_case]}') + Report.info(f'Camera speed per loop: {camera_speed_per_loop[test_case]}') + for test_counter in range(0, 100): + + # Every N loops, invalidate the entire set of surface data. It's mostly just important for this + # not to happen *every* iteration, since we need the vegetation system to bounce between having + # dirty surface points that cause sectors to be refreshed, and having no dirty surface points or + # active surface areas to trigger a "delete all sectors" condition. + if (test_counter % loops_per_surface_changed[test_case]) == 0: + azlmbr.surface_data.SurfaceDataSystemNotificationBus(azlmbr.bus.Broadcast, + 'OnSurfaceChanged', + surface_entity.id, + azlmbr.math.Aabb(), + azlmbr.math.Aabb()) + + # Move the camera back and forth along the X axis at just the right speed to invalidate sectors that are + # queued for updating but haven't updated yet, so that when they try to update they crash. + x_pos = world_center.x + ((test_counter % loops_per_camera_reset[test_case]) * camera_speed_per_loop[test_case]) + general.set_current_view_position(x_pos, world_center.y, world_center.z) + + Report.info(f'{test_counter}: {x_pos}') + + # Give a little processing time each iteration. + general.idle_wait(0.01) + + # If we haven't crashed, then we've succeeded. + test_success = True + Report.result(Tests.editor_remains_stable, test_success) -class TestSurfaceDataRefreshes_RemainsStable(EditorTestHelper): - def __init__(self): - EditorTestHelper.__init__(self, log_prefix="SurfaceDataRefreshes_RemainsStable", args=["level"]) +if __name__ == "__main__": - def run_test(self): - """ - Summary: - The Vegetation Area System can intermittently crash when updating surface data and moving the camera - around rapidly. The situation occurs across multiple frames - the surface data updates, which triggers a bunch - of sector updates getting added to the update queue. Then in a subsequent frame, there is no active vegetation - area or surface data updates, which triggers "delete all sectors". The "delete all" wasn't deleting entries from - the update queue, so any unprocessed updates would continue to get processed. If any of those updates referenced - a sector that no longer exists, because the camera changed position, then it would assert and crash. - - To repro this bug, this test creates an empty level with a large box shape emitting a surface, and then runs a tight - loop of camera movements and "surface changed" events that invalidate all surface points. Because this is a timing - issue, there's no guarantee that the test below will successfully cause the condition to occur, but it successfully - crashed every time it was tested locally prior to the bugfix. - - :return: None - """ - # 1) Create a test level with the needed test setup - self.test_success = self.create_level( - self.get_arg('level'), - heightmap_resolution=128, - heightmap_meters_per_pixel=1, - terrain_texture_resolution=128, - use_terrain=False) - - world_center = math.Vector3(512.0, 512.0, 32.0) - - # Add an entity with a 1024 x 1024 box centered at 512,512. - surface_entity = dynveg.create_surface_entity("Surface Data", world_center, 1024.0, 1024.0, 1.0) - - # Move the camera to the world center - general.set_current_view_position(world_center.x, world_center.y, world_center.z) - - # 2) Perform the test. Since the conditions are extremely timing related, and every machine - # running the test can have different timing conditions, we run through a set of different - # combinations to try and cause the crash under as many scenarios as possible - - loops_per_surface_changed = [3, 5, 5] - loops_per_camera_reset = [20, 20, 20] - camera_speed_per_loop = [10.0, 10.0, 15.0] - - # Setting test success to false to make sure the toggle at the end accurately conveys the loop being successful - self.test_success = False - - # Loop through all our attempted timing test cases to cause the crash pretty consistently. - for test_case in range(0,3): - self.log(f'Starting test case {test_case}') - self.log(f'Loops per surface changed: {loops_per_surface_changed[test_case]}') - self.log(f'Loops per camera reset: {loops_per_camera_reset[test_case]}') - self.log(f'Camera speed per loop: {camera_speed_per_loop[test_case]}') - for test_counter in range (0,100): - - # Every N loops, invalidate the entire set of surface data. It's mostly just important for this - # not to happen *every* iteration, since we need the vegetation system to bounce between having - # dirty surface points that cause sectors to be refreshed, and having no dirty surface points or - # active surface areas to trigger a "delete all sectors" condition. - if (test_counter % loops_per_surface_changed[test_case]) == 0: - azlmbr.surface_data.SurfaceDataSystemNotificationBus(azlmbr.bus.Broadcast, - 'OnSurfaceChanged', - surface_entity.id, - azlmbr.math.Aabb(), - azlmbr.math.Aabb()) - - # Move the camera back and forth along the X axis at just the right speed to invalidate sectors that are - # queued for updating but haven't updated yet, so that when they try to update they crash. - x_pos = world_center.x + ((test_counter % loops_per_camera_reset[test_case]) * camera_speed_per_loop[test_case]) - general.set_current_view_position(x_pos, world_center.y, world_center.z) - - self.log(f'{test_counter}: {x_pos}') - - # Give a little processing time each iteration. - general.idle_wait(0.01) - - # If we haven't crashed, then we've succeeded. - self.test_success = True - - -test = TestSurfaceDataRefreshes_RemainsStable() -test.run() + from editor_python_test_tools.utils import Report + Report.start_test(SurfaceDataRefreshes_RemainsStable) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/SurfaceMaskFilterOverrides_MultipleDescriptorOverridesPlantAsExpected.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/SurfaceMaskFilterOverrides_MultipleDescriptorOverridesPlantAsExpected.py index 87aa7946b3..ba0f3e05e3 100755 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/SurfaceMaskFilterOverrides_MultipleDescriptorOverridesPlantAsExpected.py +++ b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/SurfaceMaskFilterOverrides_MultipleDescriptorOverridesPlantAsExpected.py @@ -5,153 +5,160 @@ For complete copyright and license terms please see the LICENSE at the root of t SPDX-License-Identifier: Apache-2.0 OR MIT """ -""" -C3711666: Multiple Descriptors with different Surface Mask Filter overrides plant as expected. -""" -import os -import sys - -import azlmbr.legacy.general as general -import azlmbr.math as math -import azlmbr.paths -import azlmbr.surface_data as surface_data - -sys.path.append(os.path.join(azlmbr.paths.devroot, "AutomatedTesting", "Gem", "PythonTests")) -import editor_python_test_tools.hydra_editor_utils as hydra -from editor_python_test_tools.editor_test_helper import EditorTestHelper -from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg +class Tests: + initial_surface_validation = ( + "Found all expected instances on all surfaces with initial setup", + "Found an unexpected number of instances on all surfaces with initial setup" + ) + surface_a_validation = ( + "Found the expected number of instances on Surface A", + "Found an unexpected number of instances on Surface A" + ) + surface_b_validation = ( + "Found the expected number of instances on Surface B", + "Found an unexpected number of instances on Surface B" + ) + surface_c_validation = ( + "Found the expected number of instances on Surface C", + "Found an unexpected number of instances on Surface C" + ) -class TestSurfaceMaskFilterMultipleOverrides(EditorTestHelper): - def __init__(self): - EditorTestHelper.__init__(self, log_prefix="SurfaceMaskFilter_MultipleDescriptorOverrides", args=["level"]) +def SurfaceMaskFilterOverrides_MultipleDescriptorOverridesPlantAsExpected(): + """ + Summary: + A new level is created. An instance spawner with 3 descriptors is created. 3 planting surfaces of different + sizes are created and different surface tags are applied to each. Descriptor surface mask filter overrides are + set and instance counts are validated. - def run_test(self): - """ - Summary: - A new level is created. An instance spawner with 3 descriptors is created. 3 planting surfaces of different - sizes are created and different surface tags are applied to each. Descriptor surface mask filter overrides are - set and instance counts are validated. + Expected Behavior: + Instances plant on surfaces based on surface mask filter overrides. - Expected Behavior: - Instances plant on surfaces based on surface mask filter overrides. + Test Steps: + 1) Open an existing level + 2) An instance spawner with 3 descriptors is created, and a Surface Mask Filter is added to the entity + 3) 3 surfaces of different sizes are created, and set to emit different tags + 4) Pre-test validation of instances + 5) Test 1 setup and validation: Inclusion tag matching surface a is set on a single descriptor + 6) Test 2 setup and validation: Inclusion tag matching surface b is set on a single descriptor + 7) Test 3 setup and validation: Inclusion tag matching surface c is set on a single descriptor - Test Steps: - 1) A new level is created - 2) An instance spawner with 3 descriptors is created, and a Surface Mask Filter is added to the entity - 3) 3 surfaces of different sizes are created, and set to emit different tags - 4) Pre-test validation of instances - 5) Test 1 setup and validation: Inclusion tag matching surface a is set on a single descriptor - 6) Test 2 setup and validation: Inclusion tag matching surface b is set on a single descriptor - 7) Test 3 setup and validation: Inclusion tag matching surface c is set on a single descriptor + Note: + - This test file must be called from the Open 3D Engine Editor command terminal + - Any passed and failed tests are written to the Editor.log file. + Parsing the file or running a log_monitor are required to observe the test results. - Note: - - This test file must be called from the Open 3D Engine Editor command terminal - - Any passed and failed tests are written to the Editor.log file. - Parsing the file or running a log_monitor are required to observe the test results. + :return: None + """ - :return: None - """ - surface_tag_list = [surface_data.SurfaceTag("test_tag"), surface_data.SurfaceTag("test_tag2"), - surface_data.SurfaceTag("test_tag3")] + import os - # 1) Create a new, temporary level - self.test_success = self.create_level( - self.args["level"], - heightmap_resolution=1024, - heightmap_meters_per_pixel=1, - terrain_texture_resolution=4096, - use_terrain=False, - ) + import azlmbr.legacy.general as general + import azlmbr.math as math + import azlmbr.surface_data as surface_data - # Set view of planting area for visual debugging - general.set_current_view_position(512.0, 500.0, 38.0) - general.set_current_view_rotation(-20.0, 0.0, 0.0) + import editor_python_test_tools.hydra_editor_utils as hydra + from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg + from editor_python_test_tools.utils import Report + from editor_python_test_tools.utils import TestHelper as helper - # 2) Create a new instance spawner entity with multiple Dynamic Slice Instance Spawner descriptors - spawner_center_point = math.Vector3(512.0, 512.0, 32.0) - asset_path = os.path.join("Slices", "PinkFlower.dynamicslice") - spawner_entity = dynveg.create_vegetation_area("Instance Spawner", spawner_center_point, 16.0, 16.0, 16.0, - asset_path) - asset_list_component = spawner_entity.components[2] - desc_asset = hydra.get_component_property_value(asset_list_component, - "Configuration|Embedded Assets")[0] - desc_list = [desc_asset, desc_asset, desc_asset] - spawner_entity.get_set_test(2, "Configuration|Embedded Assets", desc_list) - - # Add a Surface Mask Filter component to the spawner entity and toggle on Allow Overrides - spawner_entity.add_component("Vegetation Surface Mask Filter") - spawner_entity.get_set_test(3, "Configuration|Allow Per-Item Overrides", True) + surface_tag_list = [surface_data.SurfaceTag("test_tag"), surface_data.SurfaceTag("test_tag2"), + surface_data.SurfaceTag("test_tag3")] - # 3) Create 3 surfaces for planting, spaced out vertically, and set expected instance counts for each surface - surface_entity_a = dynveg.create_surface_entity("Surface Entity A", math.Vector3(512.0, 512.0, 32.0), - 16.0, 16.0, 1.0) - num_expected_surface_a = 20 * 20 # 20x20 instances on a 16x16 meter surface - surface_entity_b = dynveg.create_surface_entity("Surface Entity B", math.Vector3(512.0, 512.0, 35.0), - 12.0, 12.0, 1.0) - num_expected_surface_b = 15 * 15 # 15x15 instances on a 12x12 meter surface - surface_entity_c = dynveg.create_surface_entity("Surface Entity C", math.Vector3(512.0, 512.0, 38.0), - 8.0, 8.0, 1.0) - num_expected_surface_c = 10 * 10 # 10x10 instances on a 8x8 meter surface + # 1) Open an existing simple level + helper.init_idle() + helper.open_level("Physics", "Base") - # Set each surface to emit a different tag - surface_entity_a.get_set_test(1, "Configuration|Generated Tags", [surface_tag_list[0]]) - surface_entity_b.get_set_test(1, "Configuration|Generated Tags", [surface_tag_list[1]]) - surface_entity_c.get_set_test(1, "Configuration|Generated Tags", [surface_tag_list[2]]) + # Set view of planting area for visual debugging + general.set_current_view_position(512.0, 500.0, 38.0) + general.set_current_view_rotation(-20.0, 0.0, 0.0) - # 4) Initial Validation: Validate instance count in the spawner area. Instances should plant on all surfaces - num_expected = num_expected_surface_a + num_expected_surface_b + num_expected_surface_c - initial_success = self.wait_for_condition( - lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, num_expected), 5.0) - self.test_success = initial_success and self.test_success + # 2) Create a new instance spawner entity with multiple Dynamic Slice Instance Spawner descriptors + spawner_center_point = math.Vector3(512.0, 512.0, 32.0) + asset_path = os.path.join("Slices", "PinkFlower.dynamicslice") + spawner_entity = dynveg.create_vegetation_area("Instance Spawner", spawner_center_point, 16.0, 16.0, 16.0, + asset_path) + asset_list_component = spawner_entity.components[2] + desc_asset = hydra.get_component_property_value(asset_list_component, + "Configuration|Embedded Assets")[0] + desc_list = [desc_asset, desc_asset, desc_asset] + spawner_entity.get_set_test(2, "Configuration|Embedded Assets", desc_list) - # 5) - # Test #1 Setup: Set test_tag to inclusion list for descriptor 1. Set other descriptors to exclude all surfaces + # Add a Surface Mask Filter component to the spawner entity and toggle on Allow Overrides + spawner_entity.add_component("Vegetation Surface Mask Filter") + spawner_entity.get_set_test(3, "Configuration|Allow Per-Item Overrides", True) - # Toggle on Display Per-Item Overrides and Surface Mask Filter Override for each descriptor - for index in range(3): - spawner_entity.get_set_test(2, f"Configuration|Embedded Assets|[{index}]|Display Per-Item Overrides", True) - spawner_entity.get_set_test(2, - f"Configuration|Embedded Assets|[{index}]|Surface Mask Filter|Override Mode", 1) + # 3) Create 3 surfaces for planting, spaced out vertically, and set expected instance counts for each surface + surface_entity_a = dynveg.create_surface_entity("Surface Entity A", math.Vector3(512.0, 512.0, 32.0), + 16.0, 16.0, 1.0) + num_expected_surface_a = 20 * 20 # 20x20 instances on a 16x16 meter surface + surface_entity_b = dynveg.create_surface_entity("Surface Entity B", math.Vector3(512.0, 512.0, 35.0), + 12.0, 12.0, 1.0) + num_expected_surface_b = 15 * 15 # 15x15 instances on a 12x12 meter surface + surface_entity_c = dynveg.create_surface_entity("Surface Entity C", math.Vector3(512.0, 512.0, 38.0), + 8.0, 8.0, 1.0) + num_expected_surface_c = 10 * 10 # 10x10 instances on a 8x8 meter surface + # Set each surface to emit a different tag + surface_entity_a.get_set_test(1, "Configuration|Generated Tags", [surface_tag_list[0]]) + surface_entity_b.get_set_test(1, "Configuration|Generated Tags", [surface_tag_list[1]]) + surface_entity_c.get_set_test(1, "Configuration|Generated Tags", [surface_tag_list[2]]) + + # 4) Initial Validation: Validate instance count in the spawner area. Instances should plant on all surfaces + num_expected = num_expected_surface_a + num_expected_surface_b + num_expected_surface_c + initial_success = helper.wait_for_condition( + lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, num_expected), 5.0) + Report.result(Tests.initial_surface_validation, initial_success) + + # 5) + # Test #1 Setup: Set test_tag to inclusion list for descriptor 1. Set other descriptors to exclude all surfaces + + # Toggle on Display Per-Item Overrides and Surface Mask Filter Override for each descriptor + for index in range(3): + spawner_entity.get_set_test(2, f"Configuration|Embedded Assets|[{index}]|Display Per-Item Overrides", True) spawner_entity.get_set_test(2, - "Configuration|Embedded Assets|[0]|Surface Mask Filter|Inclusion Tags", - [surface_tag_list[0]]) - spawner_entity.get_set_test(2, - "Configuration|Embedded Assets|[1]|Surface Mask Filter|Exclusion Tags", - surface_tag_list) - spawner_entity.get_set_test(2, - "Configuration|Embedded Assets|[2]|Surface Mask Filter|Exclusion Tags", - surface_tag_list) + f"Configuration|Embedded Assets|[{index}]|Surface Mask Filter|Override Mode", 1) - # Test #1 Validation: Validate instance count. Should only plant on a single surface for 400 instances - test_1_success = self.wait_for_condition( - lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, num_expected_surface_a), 5.0) - self.test_success = test_1_success and self.test_success + spawner_entity.get_set_test(2, + "Configuration|Embedded Assets|[0]|Surface Mask Filter|Inclusion Tags", + [surface_tag_list[0]]) + spawner_entity.get_set_test(2, + "Configuration|Embedded Assets|[1]|Surface Mask Filter|Exclusion Tags", + surface_tag_list) + spawner_entity.get_set_test(2, + "Configuration|Embedded Assets|[2]|Surface Mask Filter|Exclusion Tags", + surface_tag_list) - # 6) - # Test #2 Setup: Set test_tag2 to inclusion for descriptor 1. - spawner_entity.get_set_test(2, - "Configuration|Embedded Assets|[0]|Surface Mask Filter|Inclusion Tags", - [surface_tag_list[1]]) + # Test #1 Validation: Validate instance count. Should only plant on a single surface for 400 instances + test_1_success = helper.wait_for_condition( + lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, num_expected_surface_a), 5.0) + Report.result(Tests.surface_a_validation, test_1_success) - # Test #2 Validation: Validate instance count. Should only plant on a single surface for 225 instances - test_2_success = self.wait_for_condition( - lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, num_expected_surface_b), 5.0) - self.test_success = test_2_success and self.test_success + # 6) + # Test #2 Setup: Set test_tag2 to inclusion for descriptor 1. + spawner_entity.get_set_test(2, + "Configuration|Embedded Assets|[0]|Surface Mask Filter|Inclusion Tags", + [surface_tag_list[1]]) - # 7) - # Test #3 Setup: Set test_tag3 to inclusion for descriptor 1. - spawner_entity.get_set_test(2, - "Configuration|Embedded Assets|[0]|Surface Mask Filter|Inclusion Tags", - [surface_tag_list[2]]) + # Test #2 Validation: Validate instance count. Should only plant on a single surface for 225 instances + test_2_success = helper.wait_for_condition( + lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, num_expected_surface_b), 5.0) + Report.result(Tests.surface_b_validation, test_2_success) - # Test #3 Validation: Validate instance count. Should only plant on a single surface for 100 instances - test_3_success = self.wait_for_condition( - lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, num_expected_surface_c), 5.0) - self.test_success = test_3_success and self.test_success + # 7) + # Test #3 Setup: Set test_tag3 to inclusion for descriptor 1. + spawner_entity.get_set_test(2, + "Configuration|Embedded Assets|[0]|Surface Mask Filter|Inclusion Tags", + [surface_tag_list[2]]) + + # Test #3 Validation: Validate instance count. Should only plant on a single surface for 100 instances + test_3_success = helper.wait_for_condition( + lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, num_expected_surface_c), 5.0) + Report.result(Tests.surface_c_validation, test_3_success) -test = TestSurfaceMaskFilterMultipleOverrides() -test.run() +if __name__ == "__main__": + + from editor_python_test_tools.utils import Report + Report.start_test(SurfaceMaskFilterOverrides_MultipleDescriptorOverridesPlantAsExpected) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/SurfaceMaskFilter_BasicSurfaceTagCreation.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/SurfaceMaskFilter_BasicSurfaceTagCreation.py index 1baec7694d..fee62c04d1 100755 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/SurfaceMaskFilter_BasicSurfaceTagCreation.py +++ b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/SurfaceMaskFilter_BasicSurfaceTagCreation.py @@ -5,66 +5,61 @@ For complete copyright and license terms please see the LICENSE at the root of t SPDX-License-Identifier: Apache-2.0 OR MIT """ -import os -import sys -import azlmbr.surface_data as surface_data - -sys.path.append(os.path.join(azlmbr.paths.devroot, 'AutomatedTesting', 'Gem', 'PythonTests')) -from editor_python_test_tools.editor_test_helper import EditorTestHelper +class Tests: + tags_same_value_equal = ( + "Two Surface Tags of the same value evaluated as equal", + "Two Surface Tags of the same value unexpectedly evaluated as unequal" + ) + tags_different_value_unequal = ( + "Two Surface Tags of different values evaluated as unequal", + "Two Surface Tags of different values unexpectedly evaluated as equal" + ) -class TestSurfaceMaskFilter_BasicSurfaceTagCreation(EditorTestHelper): - def __init__(self): - EditorTestHelper.__init__(self, log_prefix="TestSurfaceMaskFilter_BasicSurfaceTagCreation", args=["level"]) - - def run_test(self): - """ - Summary: - Verifies basic surface tag value equality +def SurfaceMaskFilter_BasicSurfaceTagCreation(): + """ + Summary: + Verifies basic surface tag value equality - Expected Behavior: - Surface tags of the same name are equal, and different names aren't. + Expected Behavior: + Surface tags of the same name are equal, and different names aren't. - Test Steps: - 1) Open level - 2) Create 2 new surface tags of identical names and verify they resolve as equal. - 3) Create another new tag of a different name and verify they resolve as different. + Test Steps: + 1) Open level + 2) Create 2 new surface tags of identical names and verify they resolve as equal. + 3) Create another new tag of a different name and verify they resolve as different. - Note: - - This test file must be called from the Open 3D Engine Editor command terminal - - Any passed and failed tests are written to the Editor.log file. - Parsing the file or running a log_monitor are required to observe the test results. + Note: + - This test file must be called from the Open 3D Engine Editor command terminal + - Any passed and failed tests are written to the Editor.log file. + Parsing the file or running a log_monitor are required to observe the test results. - :return: None - """ - self.log("SurfaceTag test started") - - # Create a level - self.test_success = self.create_level( - self.args["level"], - heightmap_resolution=1024, - heightmap_meters_per_pixel=1, - terrain_texture_resolution=4096, - use_terrain=False, - ) - - tag1 = surface_data.SurfaceTag() - tag2 = surface_data.SurfaceTag() - - # Test 1: Verify that two tags with the same value are equal - tag1.SetTag('equal_test') - tag2.SetTag('equal_test') - self.log("SurfaceTag equal tag comparison is {} expected True".format(tag1.Equal(tag2))) - self.test_success = self.test_success and tag1.Equal(tag2) - - # Test 2: Verify that two tags with different values are not equal - tag2.SetTag('not_equal_test') - self.log("SurfaceTag not equal tag comparison is {} expected False".format(tag1.Equal(tag2))) - self.test_success = self.test_success and not tag1.Equal(tag2) - - self.log("SurfaceTag test finished") + :return: None + """ + + import azlmbr.surface_data as surface_data + from editor_python_test_tools.utils import Report + from editor_python_test_tools.utils import TestHelper as helper + + # Open an existing simple level + helper.init_idle() + helper.open_level("Physics", "Base") + + tag1 = surface_data.SurfaceTag() + tag2 = surface_data.SurfaceTag() + + # Test 1: Verify that two tags with the same value are equal + tag1.SetTag('equal_test') + tag2.SetTag('equal_test') + Report.result(Tests.tags_same_value_equal, tag1.Equal(tag2)) + + # Test 2: Verify that two tags with different values are not equal + tag2.SetTag('not_equal_test') + Report.result(Tests.tags_different_value_unequal, not tag1.Equal(tag2)) -test = TestSurfaceMaskFilter_BasicSurfaceTagCreation() -test.run() +if __name__ == "__main__": + + from editor_python_test_tools.utils import Report + Report.start_test(SurfaceMaskFilter_BasicSurfaceTagCreation) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/SurfaceMaskFilter_ExclusionList.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/SurfaceMaskFilter_ExclusionList.py index 1ce7962e6a..6438124698 100755 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/SurfaceMaskFilter_ExclusionList.py +++ b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/SurfaceMaskFilter_ExclusionList.py @@ -4,147 +4,141 @@ For complete copyright and license terms please see the LICENSE at the root of t SPDX-License-Identifier: Apache-2.0 OR MIT """ -""" -C2561342: Exclusive Surface Masks tags function -""" -import os -import sys - -sys.path.append(os.path.dirname(os.path.abspath(__file__))) -import azlmbr.areasystem as areasystem -import azlmbr.bus as bus -import azlmbr.editor as editor -import azlmbr.legacy.general as general -import azlmbr.math as math -import azlmbr.shape as shape -import azlmbr.surface_data as surface_data -sys.path.append(os.path.join(azlmbr.paths.devroot, 'AutomatedTesting', 'Gem', 'PythonTests')) -import editor_python_test_tools.hydra_editor_utils as hydra -from editor_python_test_tools.editor_test_helper import EditorTestHelper -from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg +class Tests: + default_exclusion_weight = ( + "Found the expected number of instances with Exclusion Weight set to defaults", + "Found an unexpected number of instances with Exclusion Weight set to defaults" + ) + exclusion_weight_below_one = ( + "Found the expected number of instances with Exclusion Weight set below 1", + "Found an unexpected number of instances with Exclusion Weight set below 1" + ) -class TestExclusiveSurfaceMasksTag(EditorTestHelper): - def __init__(self): - EditorTestHelper.__init__(self, log_prefix="SurfaceMaskFilter_ExclusionList", args=["level"]) +def SurfaceMaskFilter_ExclusionList(): + """ + Summary: + New level is created and set up with surface shapes with varying surface tags. A simple vegetation area has been + created and Vegetation Surface Mask Filter component is added to entity with terrain hole exclusion tag. - def run_test(self): - """ - Summary: - New level is created and set up with surface shapes with varying surface tags. A simple vegetation area has been - created and Vegetation Surface Mask Filter component is added to entity with terrain hole exclusion tag. + Expected Behavior: + With default Exclusion settings, vegetation does not plant over the terrain holes. + With Exclusion Weight Max below 1.0, vegetation plants over the terrain holes. - Expected Behavior: - With default Exclusion settings, vegetation does not plant over the terrain holes. - With Exclusion Weight Max below 1.0, vegetation plants over the terrain holes. + Test Steps: + 1) Open an existing level + 2) Create entity with components "Vegetation Layer Spawner", "Vegetation Asset List", "Box Shape" + 3) Add a Vegetation Surface Mask Filter component to the entity. + 4) Create 2 surface entities to represent terrain and terrain hole surfaces + 5) Add an Exclusion List tag to the component, and set it to terrainHole. + 6) Check spawn count with default Exclusion Weights + 7) Check spawn count with Exclusion Weight Max set below 1.0 - Test Steps: - 1) Create a new level. - 2) Create entity with components "Vegetation Layer Spawner", "Vegetation Asset List", "Box Shape" - 3) Add a Vegetation Surface Mask Filter component to the entity. - 4) Create 2 surface entities to represent terrain and terrain hole surfaces - 5) Add an Exclusion List tag to the component, and set it to terrainHole. - 6) Check spawn count with default Exclusion Weights - 7) Check spawn count with Exclusion Weight Max set below 1.0 - - Note: - - Any passed and failed tests are written to the Editor.log file. - Parsing the file or running a log_monitor are required to observe the test results. + Note: + - Any passed and failed tests are written to the Editor.log file. + Parsing the file or running a log_monitor are required to observe the test results. - :return: None - """ + :return: None + """ - def update_surface_tag_exclusion_list(Entity, component_index, surface_tag): - tag_list = [surface_data.SurfaceTag()] + import os - # assign list with one surface tag to exclusion list - hydra.get_set_test(Entity, component_index, "Configuration|Exclusion|Surface Tags", tag_list) + import azlmbr.bus as bus + import azlmbr.editor as editor + import azlmbr.legacy.general as general + import azlmbr.math as math + import azlmbr.surface_data as surface_data - # set that one surface tag element to required surface tag - component = Entity.components[component_index] - path = "Configuration|Exclusion|Surface Tags|[0]|Surface Tag" - editor.EditorComponentAPIBus(bus.Broadcast, "SetComponentProperty", component, path, surface_tag) - new_value = hydra.get_component_property_value(component, path) + import editor_python_test_tools.hydra_editor_utils as hydra + from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg + from editor_python_test_tools.utils import Report + from editor_python_test_tools.utils import TestHelper as helper - if new_value == surface_tag: - self.log(f"Exclusive surface mask filter of {surface_tag} is added successfully") - else: - self.log(f"Failed to add an Exclusive surface mask filter of {surface_tag}") + def update_surface_tag_exclusion_list(Entity, component_index, surface_tag): + tag_list = [surface_data.SurfaceTag()] - def update_generated_surface_tag(Entity, component_index, surface_tag): - tag_list = [surface_data.SurfaceTag()] + # assign list with one surface tag to exclusion list + hydra.get_set_test(Entity, component_index, "Configuration|Exclusion|Surface Tags", tag_list) - # assign list with one surface tag to Generated Tags list - hydra.get_set_test(Entity, component_index, "Configuration|Generated Tags", tag_list) + # set that one surface tag element to required surface tag + component = Entity.components[component_index] + path = "Configuration|Exclusion|Surface Tags|[0]|Surface Tag" + editor.EditorComponentAPIBus(bus.Broadcast, "SetComponentProperty", component, path, surface_tag) + new_value = hydra.get_component_property_value(component, path) - # set that one surface tag element to required surface tag - component = Entity.components[component_index] - path = "Configuration|Generated Tags|[0]|Surface Tag" - editor.EditorComponentAPIBus(bus.Broadcast, "SetComponentProperty", component, path, surface_tag) - new_value = hydra.get_component_property_value(component, path) + if new_value == surface_tag: + Report.info(f"Exclusive surface mask filter of {surface_tag} is added successfully") + else: + Report.info(f"Failed to add an Exclusive surface mask filter of {surface_tag}") - if new_value == surface_tag: - self.log(f"Generated surface tag of {surface_tag} is added successfully") - else: - self.log(f"Failed to add Generated surface tag of {surface_tag}") + def update_generated_surface_tag(Entity, component_index, surface_tag): + tag_list = [surface_data.SurfaceTag()] - # 1) Create a new level - self.test_success = self.create_level( - self.args["level"], - heightmap_resolution=1024, - heightmap_meters_per_pixel=1, - terrain_texture_resolution=4096, - use_terrain=False, - ) + # assign list with one surface tag to Generated Tags list + hydra.get_set_test(Entity, component_index, "Configuration|Generated Tags", tag_list) - general.set_current_view_position(512.0, 480.0, 38.0) + # set that one surface tag element to required surface tag + component = Entity.components[component_index] + path = "Configuration|Generated Tags|[0]|Surface Tag" + editor.EditorComponentAPIBus(bus.Broadcast, "SetComponentProperty", component, path, surface_tag) + new_value = hydra.get_component_property_value(component, path) - # 2) Create entity with components "Vegetation Layer Spawner", "Vegetation Asset List", "Box Shape" - entity_position = math.Vector3(512.0, 512.0, 32.0) - asset_path = os.path.join("Slices", "PurpleFlower.dynamicslice") - spawner_entity = dynveg.create_vegetation_area("Instance Spawner", - entity_position, - 10.0, 10.0, 10.0, - asset_path) + if new_value == surface_tag: + Report.info(f"Generated surface tag of {surface_tag} is added successfully") + else: + Report.info(f"Failed to add Generated surface tag of {surface_tag}") - # 3) Add a Vegetation Surface Mask Filter component to the entity. - spawner_entity.add_component("Vegetation Surface Mask Filter") + # 1) Open an existing simple level + helper.init_idle() + helper.open_level("Physics", "Base") - # 4) Create 2 surface entities to represent terrain and terrain hole surfaces - surface_tags: dict = {"terrainHole": 1327698037, "terrain": 3363197873} - entity_position = math.Vector3(510.0, 512.0, 32.0) - surface_entity_1 = dynveg.create_surface_entity("Surface Entity 1", - entity_position, - 10.0, 10.0, 1.0) - update_generated_surface_tag(surface_entity_1, 1, surface_tags["terrainHole"]) + general.set_current_view_position(512.0, 480.0, 38.0) - entity_position = math.Vector3(520.0, 512.0, 32.0) - surface_entity_2 = dynveg.create_surface_entity("Surface Entity 2", - entity_position, - 10.0, 10.0, 1.0) - update_generated_surface_tag(surface_entity_2, 1, surface_tags["terrain"]) + # 2) Create entity with components "Vegetation Layer Spawner", "Vegetation Asset List", "Box Shape" + entity_position = math.Vector3(512.0, 512.0, 32.0) + asset_path = os.path.join("Slices", "PurpleFlower.dynamicslice") + spawner_entity = dynveg.create_vegetation_area("Instance Spawner", + entity_position, + 10.0, 10.0, 10.0, + asset_path) - # 5) Add an Exclusion List tag to the component, and set it to "terrainHole". - update_surface_tag_exclusion_list(spawner_entity, 3, surface_tags["terrainHole"]) + # 3) Add a Vegetation Surface Mask Filter component to the entity. + spawner_entity.add_component("Vegetation Surface Mask Filter") - # 6) Check spawn count with default Exclusion Weights - general.idle_wait(2.0) # Allow a few seconds for instances to spawn - num_expected_instances = 39 - box = shape.ShapeComponentRequestsBus(bus.Event, 'GetEncompassingAabb', spawner_entity.id) - num_found = areasystem.AreaSystemRequestBus(bus.Broadcast, 'GetInstanceCountInAabb', box) - self.log(f"Expected {num_expected_instances} instances - Found {num_found} instances") - self.test_success = self.test_success and num_found == num_expected_instances + # 4) Create 2 surface entities to represent terrain and terrain hole surfaces + surface_tags: dict = {"terrainHole": 1327698037, "terrain": 3363197873} + entity_position = math.Vector3(510.0, 512.0, 32.0) + surface_entity_1 = dynveg.create_surface_entity("Surface Entity 1", + entity_position, + 10.0, 10.0, 1.0) + update_generated_surface_tag(surface_entity_1, 1, surface_tags["terrainHole"]) - # 7) Check spawn count with Exclusion Weight Max set below 1.0 - hydra.get_set_test(spawner_entity, 3, "Configuration|Exclusion|Weight Max", 0.9) - general.idle_wait(2.0) # Allow a few seconds for instances to spawn - num_expected_instances = 169 - num_found = areasystem.AreaSystemRequestBus(bus.Broadcast, 'GetInstanceCountInAabb', box) - self.log(f"Expected {num_expected_instances} instances - Found {num_found} instances") - self.test_success = self.test_success and num_found == num_expected_instances + entity_position = math.Vector3(520.0, 512.0, 32.0) + surface_entity_2 = dynveg.create_surface_entity("Surface Entity 2", + entity_position, + 10.0, 10.0, 1.0) + update_generated_surface_tag(surface_entity_2, 1, surface_tags["terrain"]) + + # 5) Add an Exclusion List tag to the component, and set it to "terrainHole". + update_surface_tag_exclusion_list(spawner_entity, 3, surface_tags["terrainHole"]) + + # 6) Check spawn count with default Exclusion Weights + num_expected_instances = 39 + success = helper.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, + num_expected_instances), 2.0) + Report.result(Tests.default_exclusion_weight, success) + + # 7) Check spawn count with Exclusion Weight Max set below 1.0 + hydra.get_set_test(spawner_entity, 3, "Configuration|Exclusion|Weight Max", 0.9) + num_expected_instances = 169 + success = helper.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, + num_expected_instances), 2.0) + Report.result(Tests.exclusion_weight_below_one, success) -test = TestExclusiveSurfaceMasksTag() -test.run() +if __name__ == "__main__": + + from editor_python_test_tools.utils import Report + Report.start_test(SurfaceMaskFilter_ExclusionList) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/SurfaceMaskFilter_InclusionList.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/SurfaceMaskFilter_InclusionList.py index e394bc0cde..bbd1235abc 100755 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/SurfaceMaskFilter_InclusionList.py +++ b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/SurfaceMaskFilter_InclusionList.py @@ -4,148 +4,142 @@ For complete copyright and license terms please see the LICENSE at the root of t SPDX-License-Identifier: Apache-2.0 OR MIT """ -""" -C2561341: Inclusive Surface Masks tags function -""" -import os -import sys - -sys.path.append(os.path.dirname(os.path.abspath(__file__))) -import azlmbr.areasystem as areasystem -import azlmbr.bus as bus -import azlmbr.editor as editor -import azlmbr.legacy.general as general -import azlmbr.math as math -import azlmbr.shape as shape -import azlmbr.surface_data as surface_data -sys.path.append(os.path.join(azlmbr.paths.devroot, 'AutomatedTesting', 'Gem', 'PythonTests')) -import editor_python_test_tools.hydra_editor_utils as hydra -from editor_python_test_tools.editor_test_helper import EditorTestHelper -from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg +class Tests: + default_inclusion_weight = ( + "Found the expected number of instances with Inclusion Weight set to defaults", + "Found an unexpected number of instances with Inclusion Weight set to defaults" + ) + inclusion_weight_below_one = ( + "Found the expected number of instances with Inclusion Weight set below 1", + "Found an unexpected number of instances with Inclusion Weight set below 1" + ) -class TestInclusiveSurfaceMasksTag(EditorTestHelper): - def __init__(self): - EditorTestHelper.__init__(self, log_prefix="SurfaceMaskFilter_InclusionList", args=["level"]) - def run_test(self): - """ - Summary: - New level is created and set up with surface shapes with varying surface tags. A simple vegetation area has been - created and Vegetation Surface Mask Filter component is added to entity with terrain hole inclusion tag. +def SurfaceMaskFilter_InclusionList(): + """ + Summary: + New level is created and set up with surface shapes with varying surface tags. A simple vegetation area has been + created and Vegetation Surface Mask Filter component is added to entity with terrain hole inclusion tag. - Expected Behavior: - With default Inclusion Weights, vegetation draws over the terrain holes. - With Inclusion Weight Max set below 1.0, vegetation stops drawing over the terrain holes. + Expected Behavior: + With default Inclusion Weights, vegetation draws over the terrain holes. + With Inclusion Weight Max set below 1.0, vegetation stops drawing over the terrain holes. - Test Steps: - 1) Create a new level - 2) Create entity with components "Vegetation Layer Spawner", "Vegetation Asset List", "Box Shape" - 3) Add a Vegetation Surface Mask Filter component to the entity. - 4) Create 2 surface entities to represent terrain and terrain hole surfaces - 5) Add an Inclusion List tag to the component, and set it to "terrainHole". - 6) Check spawn count with default Inclusion Weights - 7) Check spawn count with Inclusion Weight Max set below 1.0 - - Note: - - Any passed and failed tests are written to the Editor.log file. - Parsing the file or running a log_monitor are required to observe the test results. + Test Steps: + 1) Open an existing level + 2) Create entity with components "Vegetation Layer Spawner", "Vegetation Asset List", "Box Shape" + 3) Add a Vegetation Surface Mask Filter component to the entity. + 4) Create 2 surface entities to represent terrain and terrain hole surfaces + 5) Add an Inclusion List tag to the component, and set it to "terrainHole". + 6) Check spawn count with default Inclusion Weights + 7) Check spawn count with Inclusion Weight Max set below 1.0 - :return: None - """ + Note: + - Any passed and failed tests are written to the Editor.log file. + Parsing the file or running a log_monitor are required to observe the test results. - def update_surface_tag_inclusion_list(Entity, component_index, surface_tag): - tag_list = [surface_data.SurfaceTag()] + :return: None + """ - # assign list with one surface tag to inclusion list - hydra.get_set_test(Entity, component_index, "Configuration|Inclusion|Surface Tags", tag_list) + import os - # set that one surface tag element to required surface tag - component = Entity.components[component_index] - path = "Configuration|Inclusion|Surface Tags|[0]|Surface Tag" - editor.EditorComponentAPIBus(bus.Broadcast, "SetComponentProperty", component, path, surface_tag) - new_value = hydra.get_component_property_value(component, path) + import azlmbr.bus as bus + import azlmbr.editor as editor + import azlmbr.legacy.general as general + import azlmbr.math as math + import azlmbr.surface_data as surface_data - if new_value == surface_tag: - print("Inclusive surface mask filter of terrainHole is added successfully") - else: - print("Failed to add an Inclusive surface mask filter of terrainHole") - general.idle_wait(2.0) + import editor_python_test_tools.hydra_editor_utils as hydra + from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg + from editor_python_test_tools.utils import Report + from editor_python_test_tools.utils import TestHelper as helper - def update_generated_surface_tag(Entity, component_index, surface_tag): - tag_list = [surface_data.SurfaceTag()] + def update_surface_tag_inclusion_list(Entity, component_index, surface_tag): + tag_list = [surface_data.SurfaceTag()] - # assign list with one surface tag to Generated Tags list - hydra.get_set_test(Entity, component_index, "Configuration|Generated Tags", tag_list) + # assign list with one surface tag to inclusion list + hydra.get_set_test(Entity, component_index, "Configuration|Inclusion|Surface Tags", tag_list) - # set that one surface tag element to required surface tag - component = Entity.components[component_index] - path = "Configuration|Generated Tags|[0]|Surface Tag" - editor.EditorComponentAPIBus(bus.Broadcast, "SetComponentProperty", component, path, surface_tag) - new_value = hydra.get_component_property_value(component, path) + # set that one surface tag element to required surface tag + component = Entity.components[component_index] + path = "Configuration|Inclusion|Surface Tags|[0]|Surface Tag" + editor.EditorComponentAPIBus(bus.Broadcast, "SetComponentProperty", component, path, surface_tag) + new_value = hydra.get_component_property_value(component, path) - if new_value == surface_tag: - self.log(f"Generated surface tag of {surface_tag} is added successfully") - else: - self.log(f"Failed to add Generated surface tag of {surface_tag}") + if new_value == surface_tag: + Report.info("Inclusive surface mask filter of terrainHole is added successfully") + else: + Report.info("Failed to add an Inclusive surface mask filter of terrainHole") - # 1) Create a new level - self.test_success = self.create_level( - self.args["level"], - heightmap_resolution=1024, - heightmap_meters_per_pixel=1, - terrain_texture_resolution=4096, - use_terrain=False, - ) + def update_generated_surface_tag(Entity, component_index, surface_tag): + tag_list = [surface_data.SurfaceTag()] - general.set_current_view_position(512.0, 480.0, 38.0) + # assign list with one surface tag to Generated Tags list + hydra.get_set_test(Entity, component_index, "Configuration|Generated Tags", tag_list) - # 2) Create entity with components "Vegetation Layer Spawner", "Vegetation Asset List", "Box Shape" - entity_position = math.Vector3(512.0, 512.0, 32.0) - asset_path = os.path.join("Slices", "PurpleFlower.dynamicslice") - spawner_entity = dynveg.create_vegetation_area("Instance Spawner", - entity_position, - 10.0, 10.0, 10.0, - asset_path) + # set that one surface tag element to required surface tag + component = Entity.components[component_index] + path = "Configuration|Generated Tags|[0]|Surface Tag" + editor.EditorComponentAPIBus(bus.Broadcast, "SetComponentProperty", component, path, surface_tag) + new_value = hydra.get_component_property_value(component, path) - # 3) Add a Vegetation Surface Mask Filter component to the entity. - spawner_entity.add_component("Vegetation Surface Mask Filter") + if new_value == surface_tag: + Report.info(f"Generated surface tag of {surface_tag} is added successfully") + else: + Report.info(f"Failed to add Generated surface tag of {surface_tag}") - # 4) Create 2 surface entities to represent terrain and terrain hole surfaces - surface_tags: dict = {"terrainHole": 1327698037, "terrain": 3363197873} - entity_position = math.Vector3(510.0, 512.0, 32.0) - surface_entity_1 = dynveg.create_surface_entity("Surface Entity 1", - entity_position, - 10.0, 10.0, 1.0) - update_generated_surface_tag(surface_entity_1, 1, surface_tags["terrainHole"]) + # 1) Open an existing simple level + helper.init_idle() + helper.open_level("Physics", "Base") - entity_position = math.Vector3(520.0, 512.0, 32.0) - surface_entity_2 = dynveg.create_surface_entity("Surface Entity 2", - entity_position, - 10.0, 10.0, 1.0) - update_generated_surface_tag(surface_entity_2, 1, surface_tags["terrain"]) + general.set_current_view_position(512.0, 480.0, 38.0) - # 5) Add an Inclusion List tag to the component, and set it to "terrainHole". - update_surface_tag_inclusion_list(spawner_entity, 3, surface_tags["terrainHole"]) + # 2) Create entity with components "Vegetation Layer Spawner", "Vegetation Asset List", "Box Shape" + entity_position = math.Vector3(512.0, 512.0, 32.0) + asset_path = os.path.join("Slices", "PurpleFlower.dynamicslice") + spawner_entity = dynveg.create_vegetation_area("Instance Spawner", + entity_position, + 10.0, 10.0, 10.0, + asset_path) - # 6) Check spawn count with default Inclusion Weights - general.idle_wait(2.0) # Allow a few seconds for instances to spawn - num_expected_instances = 130 - box = shape.ShapeComponentRequestsBus(bus.Event, 'GetEncompassingAabb', spawner_entity.id) - num_found = areasystem.AreaSystemRequestBus(bus.Broadcast, 'GetInstanceCountInAabb', box) - self.log(f"Expected {num_expected_instances} instances - Found {num_found} instances") - self.test_success = self.test_success and num_found == num_expected_instances + # 3) Add a Vegetation Surface Mask Filter component to the entity. + spawner_entity.add_component("Vegetation Surface Mask Filter") - # 7) Check spawn count with Inclusion Weight Max set below 1.0 - hydra.get_set_test(spawner_entity, 3, "Configuration|Inclusion|Weight Max", 0.9) - general.idle_wait(2.0) # Allow a few seconds for instances to update - num_expected_instances = 0 - num_found = areasystem.AreaSystemRequestBus(bus.Broadcast, 'GetInstanceCountInAabb', box) - self.log(f"Expected {num_expected_instances} instances - Found {num_found} instances") - self.test_success = self.test_success and num_found == num_expected_instances + # 4) Create 2 surface entities to represent terrain and terrain hole surfaces + surface_tags: dict = {"terrainHole": 1327698037, "terrain": 3363197873} + entity_position = math.Vector3(510.0, 512.0, 32.0) + surface_entity_1 = dynveg.create_surface_entity("Surface Entity 1", + entity_position, + 10.0, 10.0, 1.0) + update_generated_surface_tag(surface_entity_1, 1, surface_tags["terrainHole"]) + + entity_position = math.Vector3(520.0, 512.0, 32.0) + surface_entity_2 = dynveg.create_surface_entity("Surface Entity 2", + entity_position, + 10.0, 10.0, 1.0) + update_generated_surface_tag(surface_entity_2, 1, surface_tags["terrain"]) + + # 5) Add an Inclusion List tag to the component, and set it to "terrainHole". + update_surface_tag_inclusion_list(spawner_entity, 3, surface_tags["terrainHole"]) + + # 6) Check spawn count with default Inclusion Weights + num_expected_instances = 130 + success = helper.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, + num_expected_instances), 2.0) + Report.result(Tests.default_inclusion_weight, success) + + # 7) Check spawn count with Inclusion Weight Max set below 1.0 + hydra.get_set_test(spawner_entity, 3, "Configuration|Inclusion|Weight Max", 0.9) + num_expected_instances = 0 + success = helper.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, + num_expected_instances), 2.0) + Report.result(Tests.inclusion_weight_below_one, success) -test = TestInclusiveSurfaceMasksTag() -test.run() +if __name__ == "__main__": + + from editor_python_test_tools.utils import Report + Report.start_test(SurfaceMaskFilter_InclusionList) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/SystemSettings_SectorPointDensity.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/SystemSettings_SectorPointDensity.py index f690e175de..d808d3f20b 100755 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/SystemSettings_SectorPointDensity.py +++ b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/SystemSettings_SectorPointDensity.py @@ -5,89 +5,90 @@ For complete copyright and license terms please see the LICENSE at the root of t SPDX-License-Identifier: Apache-2.0 OR MIT """ -import os -import sys -import azlmbr.math as math -import azlmbr.paths -import azlmbr.editor as editor -import azlmbr.bus as bus -import azlmbr.legacy.general as general -sys.path.append(os.path.join(azlmbr.paths.devroot, "AutomatedTesting", "Gem", "PythonTests")) -import editor_python_test_tools.hydra_editor_utils as hydra -from editor_python_test_tools.editor_test_helper import EditorTestHelper -from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg +class Tests: + initial_density_instance_count = ( + "Found the expected number of instances with default sector density", + "Found an unexpected number of instances with default sector density" + ) + configured_density_instance_count = ( + "Found the expected number of instances with a sector density of 10", + "Found an unexpected number of instances with a sector density of 10" + ) -class TestSystemSettingsSectorPointDensity(EditorTestHelper): - def __init__(self): - EditorTestHelper.__init__(self, log_prefix="SystemSettings_SectorPointDensity", args=["level"]) +def SystemSettings_SectorPointDensity(): + """ + Summary: + Sector Point Density increases/reduces the number of vegetation points within a sector - def run_test(self): - """ - Summary: - Sector Point Density increases/reduces the number of vegetation points within a sector + Expected Result: + Default value for Sector Point Density is 20. + 20 vegetation meshes appear on each side of the established vegetation area with the default value. + When altered, the specified number of vegetation meshes along a side of a vegetation area matches the value set + in Sector Point Density. - Expected Result: - Default value for Sector Point Density is 20. - 20 vegetation meshes appear on each side of the established vegetation area with the default value. - When altered, the specified number of vegetation meshes along a side of a vegetation area matches the value set - in Sector Point Density. + :return: None + """ - :return: None - """ + import os - INSTANCE_COUNT_BEFORE_DENSITY_CHANGE = 400 - INSTANCE_COUNT_AFTER_DENSITY_CHANGE = 100 + import azlmbr.math as math + import azlmbr.editor as editor + import azlmbr.bus as bus + import azlmbr.legacy.general as general - # Create empty level - self.test_success = self.create_level( - self.args["level"], - heightmap_resolution=1024, - heightmap_meters_per_pixel=1, - terrain_texture_resolution=4096, - use_terrain=False, - ) + import editor_python_test_tools.hydra_editor_utils as hydra + from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg + from editor_python_test_tools.utils import Report + from editor_python_test_tools.utils import TestHelper as helper - general.set_current_view_position(512.0, 480.0, 38.0) + INSTANCE_COUNT_BEFORE_DENSITY_CHANGE = 400 + INSTANCE_COUNT_AFTER_DENSITY_CHANGE = 100 - # Create basic vegetation entity - position = math.Vector3(512.0, 512.0, 32.0) - asset_path = os.path.join("Slices", "PinkFlower.dynamicslice") - dynveg.create_vegetation_area("vegetation", position, 16.0, 16.0, 1.0, asset_path) - dynveg.create_surface_entity("Surface_Entity", position, 16.0, 16.0, 1.0) + # Open an existing simple level + helper.init_idle() + helper.open_level("Physics", "Base") - # Count the number of vegetation meshes along one side of the new vegetation area. # - result = self.wait_for_condition( - lambda: dynveg.validate_instance_count(position, 8.0, INSTANCE_COUNT_BEFORE_DENSITY_CHANGE), 2.0 - ) - self.log(f"Vegetation instances count equal to expected value before changing sector point density: {result}") + general.set_current_view_position(512.0, 480.0, 38.0) - # Add the Vegetation Debugger component to the Level Inspector - veg_system_settings_component = hydra.add_level_component("Vegetation System Settings") + # Create basic vegetation entity + position = math.Vector3(512.0, 512.0, 32.0) + asset_path = os.path.join("Slices", "PinkFlower.dynamicslice") + dynveg.create_vegetation_area("vegetation", position, 16.0, 16.0, 1.0, asset_path) + dynveg.create_surface_entity("Surface_Entity", position, 16.0, 16.0, 1.0) - # Change Sector Point Density to 10 - editor.EditorComponentAPIBus( - bus.Broadcast, - "SetComponentProperty", - veg_system_settings_component, - "Configuration|Area System Settings|Sector Point Snap Mode", - 1, - ) - editor.EditorComponentAPIBus( - bus.Broadcast, - "SetComponentProperty", - veg_system_settings_component, - "Configuration|Area System Settings|Sector Point Density", - 10, - ) + # Count the number of vegetation instances in the vegetation area + result = helper.wait_for_condition(lambda: dynveg.validate_instance_count(position, 8.0, + INSTANCE_COUNT_BEFORE_DENSITY_CHANGE), 2.0) + Report.result(Tests.initial_density_instance_count, result) - # Count the number of vegetation meshes along one side of the new vegetation area. - result = self.wait_for_condition( - lambda: dynveg.validate_instance_count(position, 8.0, INSTANCE_COUNT_AFTER_DENSITY_CHANGE), 2.0 - ) - self.log(f"Vegetation instances count equal to expected value after changing sector point density: {result}") + # Add the Vegetation Debugger component to the Level Inspector + veg_system_settings_component = hydra.add_level_component("Vegetation System Settings") + + # Change Sector Point Density to 10 + editor.EditorComponentAPIBus( + bus.Broadcast, + "SetComponentProperty", + veg_system_settings_component, + "Configuration|Area System Settings|Sector Point Snap Mode", + 1, + ) + editor.EditorComponentAPIBus( + bus.Broadcast, + "SetComponentProperty", + veg_system_settings_component, + "Configuration|Area System Settings|Sector Point Density", + 10, + ) + + # Count the number of vegetation instances in the vegetation area + result = helper.wait_for_condition(lambda: dynveg.validate_instance_count(position, 8.0, + INSTANCE_COUNT_AFTER_DENSITY_CHANGE), 2.0) + Report.result(Tests.configured_density_instance_count, result) -test = TestSystemSettingsSectorPointDensity() -test.run() +if __name__ == "__main__": + + from editor_python_test_tools.utils import Report + Report.start_test(SystemSettings_SectorPointDensity) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/SystemSettings_SectorSize.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/SystemSettings_SectorSize.py index 61aaedcf7c..fb660c964a 100755 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/SystemSettings_SectorSize.py +++ b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/SystemSettings_SectorSize.py @@ -5,88 +5,91 @@ For complete copyright and license terms please see the LICENSE at the root of t SPDX-License-Identifier: Apache-2.0 OR MIT """ -import os -import sys -import azlmbr.math as math -import azlmbr.paths -import azlmbr.editor as editor -import azlmbr.bus as bus -import azlmbr.legacy.general as general -sys.path.append(os.path.join(azlmbr.paths.devroot, "AutomatedTesting", "Gem", "PythonTests")) -import editor_python_test_tools.hydra_editor_utils as hydra -from editor_python_test_tools.editor_test_helper import EditorTestHelper -from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg +class Tests: + initial_sector_size_instance_count = ( + "Found the expected number of instances with default sector size", + "Found an unexpected number of instances with default sector size" + ) + configured_sector_size_instance_count = ( + "Found the expected number of instances with a sector size of 10", + "Found an unexpected number of instances with a sector size of 10" + ) -class TestSystemSettingsSectorSize(EditorTestHelper): - def __init__(self): - EditorTestHelper.__init__(self, log_prefix="SystemSettings_SectorSize", args=["level"]) +def SystemSettings_SectorSize(): + """ + Summary: + Sector Size In Meters increases/reduces the size of a sector - def run_test(self): - """ - Summary: - Sector Size In Meters increases/reduces the size of a sector + Expected Result: + The number of spawned vegetation meshes inside the vegetation area is identical after updating the Sector Size - Expected Result: - The number of spawned vegetation meshes inside the vegetation area is identical after updating the Sector Size + :return: None + """ - :return: None - """ + import os - VEGETATION_INSTANCE_COUNT = 400 + import azlmbr.math as math + import azlmbr.editor as editor + import azlmbr.bus as bus + import azlmbr.legacy.general as general - # Create empty level - self.test_success = self.create_level( - self.args["level"], - heightmap_resolution=1024, - heightmap_meters_per_pixel=1, - terrain_texture_resolution=4096, - use_terrain=False, - ) + import editor_python_test_tools.hydra_editor_utils as hydra + from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg + from editor_python_test_tools.utils import Report + from editor_python_test_tools.utils import TestHelper as helper - general.set_current_view_position(512.0, 480.0, 38.0) + VEGETATION_INSTANCE_COUNT = 400 - # Create basic vegetation entity - position = math.Vector3(512.0, 512.0, 32.0) - asset_path = os.path.join("Slices", "PinkFlower.dynamicslice") - vegetation = dynveg.create_vegetation_area("vegetation", position, 16.0, 16.0, 1.0, asset_path) - dynveg.create_surface_entity("Surface_Entity", position, 16.0, 16.0, 1.0) + # Open an existing simple level + helper.init_idle() + helper.open_level("Physics", "Base") - # Add the Vegetation Debugger component to the Level Inspector - veg_system_settings_component = hydra.add_level_component("Vegetation System Settings") + general.set_current_view_position(512.0, 480.0, 38.0) - # Count the number of vegetation meshes along one side of the new vegetation area. - result = self.wait_for_condition( - lambda: dynveg.validate_instance_count(position, 8.0, VEGETATION_INSTANCE_COUNT), 2.0 - ) - self.log(f"Vegetation instances count equal to expected value before changing sector size: {result}") + # Create basic vegetation entity + position = math.Vector3(512.0, 512.0, 32.0) + asset_path = os.path.join("Slices", "PinkFlower.dynamicslice") + vegetation = dynveg.create_vegetation_area("vegetation", position, 16.0, 16.0, 1.0, asset_path) + dynveg.create_surface_entity("Surface_Entity", position, 16.0, 16.0, 1.0) - # Change Sector Size in Meters to 10. - editor.EditorComponentAPIBus( - bus.Broadcast, - "SetComponentProperty", - veg_system_settings_component, - "Configuration|Area System Settings|Sector Point Snap Mode", - 1, - ) - editor.EditorComponentAPIBus( - bus.Broadcast, - "SetComponentProperty", - veg_system_settings_component, - "Configuration|Area System Settings|Sector Size In Meters", - 10, - ) + # Add the Vegetation Debugger component to the Level Inspector + veg_system_settings_component = hydra.add_level_component("Vegetation System Settings") - # Alter the Box Shape to be 10,10,1 - vegetation.get_set_test(1, "Box Shape|Box Configuration|Dimensions", math.Vector3(10.0, 10.0, 1.0)) + # Count the number of vegetation instances in the vegetation area + result = helper.wait_for_condition( + lambda: dynveg.validate_instance_count(position, 8.0, VEGETATION_INSTANCE_COUNT), 2.0 + ) + Report.result(Tests.initial_sector_size_instance_count, result) - # Count the number of vegetation meshes along one side of the new vegetation area. - result = self.wait_for_condition( - lambda: dynveg.validate_instance_count(position, 5.0, VEGETATION_INSTANCE_COUNT), 2.0 - ) - self.log(f"Vegetation instances count equal to expected value after changing sector size: {result}") + # Change Sector Size in Meters to 10. + editor.EditorComponentAPIBus( + bus.Broadcast, + "SetComponentProperty", + veg_system_settings_component, + "Configuration|Area System Settings|Sector Point Snap Mode", + 1, + ) + editor.EditorComponentAPIBus( + bus.Broadcast, + "SetComponentProperty", + veg_system_settings_component, + "Configuration|Area System Settings|Sector Size In Meters", + 10, + ) + + # Alter the Box Shape to be 10,10,1 + vegetation.get_set_test(1, "Box Shape|Box Configuration|Dimensions", math.Vector3(10.0, 10.0, 1.0)) + + # Count the number of vegetation instances in the vegetation area + result = helper.wait_for_condition( + lambda: dynveg.validate_instance_count(position, 5.0, VEGETATION_INSTANCE_COUNT), 2.0 + ) + Report.result(Tests.configured_sector_size_instance_count, result) -test = TestSystemSettingsSectorSize() -test.run() +if __name__ == "__main__": + + from editor_python_test_tools.utils import Report + Report.start_test(SystemSettings_SectorSize) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/VegetationInstances_DespawnWhenOutOfRange.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/VegetationInstances_DespawnWhenOutOfRange.py index 6f40d04854..a0657f3949 100755 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/VegetationInstances_DespawnWhenOutOfRange.py +++ b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/EditorScripts/VegetationInstances_DespawnWhenOutOfRange.py @@ -5,89 +5,85 @@ For complete copyright and license terms please see the LICENSE at the root of t SPDX-License-Identifier: Apache-2.0 OR MIT """ -""" -This script tests for regressions of "vegetation instances don't despawn correctly -when the camera moves beyond the range of all active vegetation areas". -This creates a new level and a vegetation area with 400 instances. -The expectation is that we will have 400 instances in that area when the camera is centered on it, -and 0 instances when the camera is moved sufficiently far away. -""" - -import sys, os - -import azlmbr.legacy.general as general -import azlmbr.math as math - -sys.path.append(os.path.join(azlmbr.paths.devroot, 'AutomatedTesting', 'Gem', 'PythonTests')) -from editor_python_test_tools.editor_test_helper import EditorTestHelper -from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg +class Tests: + instance_validation_close = ( + "Instance count is as expected when within range of Spawner", + "Instance count was unexpected when within range of Spawner" + ) + instance_validation_far = ( + "No instances found when out of range of Spawner", + "Instances still found when out of range of Spawner" + ) -class TestVegetationInstances_DespawnWhenOutOfRange(EditorTestHelper): - def __init__(self): - EditorTestHelper.__init__(self, log_prefix='VegetationInstances_DespawnWhenOutOfRange', args=['level']) +def VegetationInstances_DespawnWhenOutOfRange(): + """ + Summary: + Verifies that vegetation instances properly spawn/despawn based on camera range. - def run_test(self): - """ - Summary: - Verifies that vegetation instances properly spawn/despawn based on camera range. + Expected Behavior: + Vegetation instances despawn when out of camera range. - Expected Behavior: - Vegetation instances despawn when out of camera range. + Test Steps: + 1) Open a simple level + 2) Create a simple vegetation area, and set the view position near the spawner. Verify instances plant. + 3) Move the view position away from the spawner. Verify instances despawn. - Test Steps: - 1) Create a new level - 2) Create a simple vegetation area, and set the view position near the spawner. Verify instances plant. - 3) Move the view position away from the spawner. Verify instances despawn. + Note: + - This test file must be called from the Open 3D Engine Editor command terminal + - Any passed and failed tests are written to the Editor.log file. + Parsing the file or running a log_monitor are required to observe the test results. - Note: - - This test file must be called from the Open 3D Engine Editor command terminal - - Any passed and failed tests are written to the Editor.log file. - Parsing the file or running a log_monitor are required to observe the test results. + :return: None + """ - :return: None - """ + import os - # Create a new level - self.test_success = self.create_level( - self.get_arg('level'), - heightmap_resolution=128, - heightmap_meters_per_pixel=1, - terrain_texture_resolution=128, - use_terrain=False) + import azlmbr.legacy.general as general + import azlmbr.math as math - # Create vegetation layer spawner - world_center = math.Vector3(512.0, 512.0, 32.0) - asset_path = os.path.join("Slices", "PurpleFlower.dynamicslice") - spawner_entity = dynveg.create_vegetation_area("Spawner Instance", world_center, 16.0, 16.0, 16.0, asset_path) + from largeworlds.large_worlds_utils import editor_dynveg_test_helper as dynveg + from editor_python_test_tools.utils import Report + from editor_python_test_tools.utils import TestHelper as helper - # Create a surface to spawn on - dynveg.create_surface_entity("Spawner Entity", world_center, 16.0, 16.0, 1.0) + # Open an existing simple level + helper.init_idle() + helper.open_level("Physics", "Base") - # Get the root position of our veg area and use it to position our camera. - # This is useful both to ensure that vegetation is spawned where we're querying and to - # visually verify the number of instances in each box - position = azlmbr.components.TransformBus(azlmbr.bus.Event, "GetWorldTranslation", spawner_entity.id) - general.set_current_view_position(position.x, position.y, position.z + 30.0) - general.set_current_view_rotation(-90.0, 0.0, 0.0) + # Create vegetation layer spawner + world_center = math.Vector3(512.0, 512.0, 32.0) + asset_path = os.path.join("Slices", "PurpleFlower.dynamicslice") + spawner_entity = dynveg.create_vegetation_area("Spawner Instance", world_center, 16.0, 16.0, 16.0, asset_path) - # When centered over the veg area, we expect to find 400 instances. - # (16x16 area, 20 points per 16 meters) - num_expected = 400 - result = self.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, - num_expected), 2.0) - self.test_success = self.test_success and result + # Create a surface to spawn on + dynveg.create_surface_entity("Spawner Entity", world_center, 16.0, 16.0, 1.0) - # Move sufficiently far away from the veg area that it should all despawn. - general.set_current_view_position(position.x - 1000.0, position.y - 1000.0, position.z + 30.0) + # Get the root position of our veg area and use it to position our camera. + # This is useful both to ensure that vegetation is spawned where we're querying and to + # visually verify the number of instances in each box + position = azlmbr.components.TransformBus(azlmbr.bus.Event, "GetWorldTranslation", spawner_entity.id) + general.set_current_view_position(position.x, position.y, position.z + 30.0) + general.set_current_view_rotation(-90.0, 0.0, 0.0) - # We now expect to find 0 instances. If the bug exists, we will find 400 still. - num_expected = 0 - result = self.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, - num_expected), 2.0) - self.test_success = self.test_success and result + # When centered over the veg area, we expect to find 400 instances. + # (16x16 area, 20 points per 16 meters) + num_expected = 400 + result = helper.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, + num_expected), 2.0) + Report.result(Tests.instance_validation_close, result) + + # Move sufficiently far away from the veg area that it should all despawn. + general.set_current_view_position(position.x - 1000.0, position.y - 1000.0, position.z + 30.0) + + # We now expect to find 0 instances. If the bug exists, we will find 400 still. + num_expected = 0 + result = helper.wait_for_condition(lambda: dynveg.validate_instance_count_in_entity_shape(spawner_entity.id, + num_expected), 2.0) + Report.result(Tests.instance_validation_far, result) -test = TestVegetationInstances_DespawnWhenOutOfRange() -test.run() +if __name__ == "__main__": + + from editor_python_test_tools.utils import Report + Report.start_test(VegetationInstances_DespawnWhenOutOfRange) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/TestSuite_Main.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/TestSuite_Main.py new file mode 100644 index 0000000000..4c02c887ef --- /dev/null +++ b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/TestSuite_Main.py @@ -0,0 +1,27 @@ +""" +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 +""" + +import os +import pytest +import sys + +sys.path.append(os.path.dirname(os.path.abspath(__file__)) + '/../../automatedtesting_shared') +from base import TestAutomationBase + + +@pytest.mark.SUITE_main +@pytest.mark.parametrize("launcher_platform", ['windows_editor']) +@pytest.mark.parametrize("project", ["AutomatedTesting"]) +class TestAutomation(TestAutomationBase): + + def test_DynamicSliceInstanceSpawner_DynamicSliceSpawnerWorks(self, request, workspace, editor, launcher_platform): + from .EditorScripts import DynamicSliceInstanceSpawner_DynamicSliceSpawnerWorks as test_module + self._run_test(request, workspace, editor, test_module) + + def test_EmptyInstanceSpawner_EmptySpawnerWorks(self, request, workspace, editor, launcher_platform): + from .EditorScripts import EmptyInstanceSpawner_EmptySpawnerWorks as test_module + self._run_test(request, workspace, editor, test_module) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/TestSuite_Main_Optimized.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/TestSuite_Main_Optimized.py new file mode 100644 index 0000000000..ded2dda4e9 --- /dev/null +++ b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/TestSuite_Main_Optimized.py @@ -0,0 +1,172 @@ +""" +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 +""" + +import os +import pytest + +import ly_test_tools.environment.file_system as file_system +from ly_test_tools.o3de.editor_test import EditorSingleTest, EditorSharedTest, EditorParallelTest, EditorTestSuite + + +@pytest.mark.xfail(reason="Optimized tests are experimental, we will enable xfail and monitor them temporarily.") +@pytest.mark.SUITE_main +@pytest.mark.parametrize("launcher_platform", ['windows_editor']) +@pytest.mark.parametrize("project", ["AutomatedTesting"]) +class TestAutomation(EditorTestSuite): + + class test_DynamicSliceInstanceSpawner_DynamicSliceSpawnerWorks(EditorParallelTest): + from .EditorScripts import DynamicSliceInstanceSpawner_DynamicSliceSpawnerWorks as test_module + + class test_EmptyInstanceSpawner_EmptySpawnerWorks(EditorParallelTest): + from .EditorScripts import EmptyInstanceSpawner_EmptySpawnerWorks as test_module + + class test_AltitudeFilter_ComponentAndOverrides_InstancesPlantAtSpecifiedAltitude(EditorParallelTest): + from .EditorScripts import AltitudeFilter_ComponentAndOverrides_InstancesPlantAtSpecifiedAltitude as test_module + + class test_AltitudeFilter_ShapeSample_InstancesPlantAtSpecifiedAltitude(EditorParallelTest): + from .EditorScripts import AltitudeFilter_ShapeSample_InstancesPlantAtSpecifiedAltitude as test_module + + class test_AltitudeFilter_FilterStageToggle(EditorParallelTest): + from .EditorScripts import AltitudeFilter_FilterStageToggle as test_module + + class test_SpawnerSlices_SliceCreationAndVisibilityToggleWorks(EditorSingleTest): + # Custom teardown to remove slice asset created during test + def teardown(self, request, workspace, editor, editor_test_results, launcher_platform): + file_system.delete([os.path.join(workspace.paths.engine_root(), "AutomatedTesting", "slices", + "TestSlice_1.slice")], True, True) + file_system.delete([os.path.join(workspace.paths.engine_root(), "AutomatedTesting", "slices", + "TestSlice_2.slice")], True, True) + from .EditorScripts import SpawnerSlices_SliceCreationAndVisibilityToggleWorks as test_module + + class test_AssetListCombiner_CombinedDescriptorsExpressInConfiguredArea(EditorParallelTest): + from .EditorScripts import AssetListCombiner_CombinedDescriptorsExpressInConfiguredArea as test_module + + class test_AssetWeightSelector_InstancesExpressBasedOnWeight(EditorParallelTest): + from .EditorScripts import AssetWeightSelector_InstancesExpressBasedOnWeight as test_module + + @pytest.mark.skip(reason="https://github.com/o3de/o3de/issues/4155") + class test_DistanceBetweenFilter_InstancesPlantAtSpecifiedRadius(EditorParallelTest): + from .EditorScripts import DistanceBetweenFilter_InstancesPlantAtSpecifiedRadius as test_module + + @pytest.mark.skip(reason="https://github.com/o3de/o3de/issues/4155") + class test_DistanceBetweenFilterOverrides_InstancesPlantAtSpecifiedRadius(EditorParallelTest): + from .EditorScripts import DistanceBetweenFilterOverrides_InstancesPlantAtSpecifiedRadius as test_module + + class test_SurfaceDataRefreshes_RemainsStable(EditorParallelTest): + from .EditorScripts import SurfaceDataRefreshes_RemainsStable as test_module + + class test_VegetationInstances_DespawnWhenOutOfRange(EditorParallelTest): + from .EditorScripts import VegetationInstances_DespawnWhenOutOfRange as test_module + + class test_InstanceSpawnerPriority_LayerAndSubPriority_HigherValuesPlantOverLower(EditorParallelTest): + from .EditorScripts import InstanceSpawnerPriority_LayerAndSubPriority as test_module + + class test_LayerBlocker_InstancesBlockedInConfiguredArea(EditorParallelTest): + from .EditorScripts import LayerBlocker_InstancesBlockedInConfiguredArea as test_module + + class test_LayerSpawner_InheritBehaviorFlag(EditorParallelTest): + from .EditorScripts import LayerSpawner_InheritBehaviorFlag as test_module + + class test_LayerSpawner_InstancesPlantInAllSupportedShapes(EditorParallelTest): + from .EditorScripts import LayerSpawner_InstancesPlantInAllSupportedShapes as test_module + + class test_LayerSpawner_FilterStageToggle(EditorParallelTest): + from .EditorScripts import LayerSpawner_FilterStageToggle as test_module + + @pytest.mark.xfail(reason="https://github.com/o3de/o3de/issues/2038") + class test_LayerSpawner_InstancesRefreshUsingCorrectViewportCamera(EditorParallelTest): + from .EditorScripts import LayerSpawner_InstancesRefreshUsingCorrectViewportCamera as test_module + + class test_MeshBlocker_InstancesBlockedByMesh(EditorParallelTest): + from .EditorScripts import MeshBlocker_InstancesBlockedByMesh as test_module + + class test_MeshBlocker_InstancesBlockedByMeshHeightTuning(EditorParallelTest): + from .EditorScripts import MeshBlocker_InstancesBlockedByMeshHeightTuning as test_module + + class test_MeshSurfaceTagEmitter_DependentOnMeshComponent(EditorParallelTest): + from .EditorScripts import MeshSurfaceTagEmitter_DependentOnMeshComponent as test_module + + class test_MeshSurfaceTagEmitter_SurfaceTagsAddRemoveSuccessfully(EditorParallelTest): + from .EditorScripts import MeshSurfaceTagEmitter_SurfaceTagsAddRemoveSuccessfully as test_module + + class test_PhysXColliderSurfaceTagEmitter_E2E_Editor(EditorParallelTest): + from .EditorScripts import PhysXColliderSurfaceTagEmitter_E2E_Editor as test_module + + class test_PositionModifier_ComponentAndOverrides_InstancesPlantAtSpecifiedOffsets(EditorParallelTest): + from .EditorScripts import PositionModifier_ComponentAndOverrides_InstancesPlantAtSpecifiedOffsets as test_module + + class test_PositionModifier_AutoSnapToSurfaceWorks(EditorParallelTest): + from .EditorScripts import PositionModifier_AutoSnapToSurfaceWorks as test_module + + class test_RotationModifier_InstancesRotateWithinRange(EditorParallelTest): + from .EditorScripts import RotationModifier_InstancesRotateWithinRange as test_module + + class test_RotationModifierOverrides_InstancesRotateWithinRange(EditorParallelTest): + from .EditorScripts import RotationModifierOverrides_InstancesRotateWithinRange as test_module + + class test_ScaleModifier_InstancesProperlyScale(EditorParallelTest): + from .EditorScripts import ScaleModifier_InstancesProperlyScale as test_module + + class test_ScaleModifierOverrides_InstancesProperlyScale(EditorParallelTest): + from .EditorScripts import ScaleModifierOverrides_InstancesProperlyScale as test_module + + class test_ShapeIntersectionFilter_InstancesPlantInAssignedShape(EditorParallelTest): + from .EditorScripts import ShapeIntersectionFilter_InstancesPlantInAssignedShape as test_module + + class test_ShapeIntersectionFilter_FilterStageToggle(EditorParallelTest): + from .EditorScripts import ShapeIntersectionFilter_FilterStageToggle as test_module + + class test_SlopeAlignmentModifier_InstanceSurfaceAlignment(EditorParallelTest): + from .EditorScripts import SlopeAlignmentModifier_InstanceSurfaceAlignment as test_module + + class test_SlopeAlignmentModifierOverrides_InstanceSurfaceAlignment(EditorParallelTest): + from .EditorScripts import SlopeAlignmentModifierOverrides_InstanceSurfaceAlignment as test_module + + class test_SurfaceMaskFilter_BasicSurfaceTagCreation(EditorParallelTest): + from .EditorScripts import SurfaceMaskFilter_BasicSurfaceTagCreation as test_module + + class test_SurfaceMaskFilter_ExclusiveSurfaceTags_Function(EditorParallelTest): + from .EditorScripts import SurfaceMaskFilter_ExclusionList as test_module + + class test_SurfaceMaskFilter_InclusiveSurfaceTags_Function(EditorParallelTest): + from .EditorScripts import SurfaceMaskFilter_InclusionList as test_module + + class test_SurfaceMaskFilterOverrides_MultipleDescriptorOverridesPlantAsExpected(EditorParallelTest): + from .EditorScripts import SurfaceMaskFilterOverrides_MultipleDescriptorOverridesPlantAsExpected as test_module + + class test_SystemSettings_SectorPointDensity(EditorParallelTest): + from .EditorScripts import SystemSettings_SectorPointDensity as test_module + + class test_SystemSettings_SectorSize(EditorParallelTest): + from .EditorScripts import SystemSettings_SectorSize as test_module + + class test_SlopeFilter_ComponentAndOverrides_InstancesPlantOnValidSlopes(EditorParallelTest): + from .EditorScripts import SlopeFilter_ComponentAndOverrides_InstancesPlantOnValidSlope as test_module + + class test_DynamicSliceInstanceSpawner_Embedded_E2E_Editor(EditorSingleTest): + from .EditorScripts import DynamicSliceInstanceSpawner_Embedded_E2E as test_module + + # Custom teardown to remove test level created during test + def teardown(self, request, workspace, editor, editor_test_results, launcher_platform): + file_system.delete([os.path.join(workspace.paths.engine_root(), "AutomatedTesting", "Levels", "tmp_level")], + True, True) + + class test_DynamicSliceInstanceSpawner_External_E2E_Editor(EditorSingleTest): + from .EditorScripts import DynamicSliceInstanceSpawner_External_E2E as test_module + + # Custom teardown to remove test level created during test + def teardown(self, request, workspace, editor, editor_test_results, launcher_platform): + file_system.delete([os.path.join(workspace.paths.engine_root(), "AutomatedTesting", "Levels", "tmp_level")], + True, True) + + class test_LayerBlender_E2E_Editor(EditorSingleTest): + from .EditorScripts import LayerBlender_E2E_Editor as test_module + + # Custom teardown to remove test level created during test + def teardown(self, request, workspace, editor, editor_test_results, launcher_platform): + file_system.delete([os.path.join(workspace.paths.engine_root(), "AutomatedTesting", "Levels", "tmp_level")], + True, True) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/TestSuite_Periodic.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/TestSuite_Periodic.py new file mode 100644 index 0000000000..2780c0f471 --- /dev/null +++ b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/TestSuite_Periodic.py @@ -0,0 +1,283 @@ +""" +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 +""" + +import os +import pytest +import sys + +import ly_test_tools.environment.waiter as waiter +import ly_test_tools.environment.file_system as file_system +import editor_python_test_tools.hydra_test_utils as hydra +from ly_remote_console.remote_console_commands import RemoteConsole as RemoteConsole + +sys.path.append(os.path.dirname(os.path.abspath(__file__)) + '/../../automatedtesting_shared') +from base import TestAutomationBase + + +@pytest.fixture +def remove_test_slice(request, workspace, project): + file_system.delete([os.path.join(workspace.paths.engine_root(), project, "slices", "TestSlice_1.slice")], True, + True) + file_system.delete([os.path.join(workspace.paths.engine_root(), project, "slices", "TestSlice_2.slice")], True, + True) + + def teardown(): + file_system.delete([os.path.join(workspace.paths.engine_root(), project, "slices", "TestSlice_1.slice")], True, + True) + file_system.delete([os.path.join(workspace.paths.engine_root(), project, "slices", "TestSlice_2.slice")], True, + True) + request.addfinalizer(teardown) + + +@pytest.fixture +def remote_console_instance(request): + console = RemoteConsole() + + def teardown(): + if console.connected: + console.stop() + + request.addfinalizer(teardown) + return console + + +@pytest.mark.SUITE_periodic +@pytest.mark.parametrize("launcher_platform", ['windows_editor']) +@pytest.mark.parametrize("project", ["AutomatedTesting"]) +class TestAutomation(TestAutomationBase): + + def test_AltitudeFilter_ComponentAndOverrides_InstancesPlantAtSpecifiedAltitude(self, request, workspace, editor, launcher_platform): + from .EditorScripts import AltitudeFilter_ComponentAndOverrides_InstancesPlantAtSpecifiedAltitude as test_module + self._run_test(request, workspace, editor, test_module) + + def test_AltitudeFilter_ShapeSample_InstancesPlantAtSpecifiedAltitude(self, request, workspace, editor, launcher_platform): + from .EditorScripts import AltitudeFilter_ShapeSample_InstancesPlantAtSpecifiedAltitude as test_module + self._run_test(request, workspace, editor, test_module) + + def test_AltitudeFilter_FilterStageToggle(self, request, workspace, editor, launcher_platform): + from .EditorScripts import AltitudeFilter_FilterStageToggle as test_module + self._run_test(request, workspace, editor, test_module) + + def test_SpawnerSlices_SliceCreationAndVisibilityToggleWorks(self, request, workspace, editor, remove_test_slice, launcher_platform): + from .EditorScripts import SpawnerSlices_SliceCreationAndVisibilityToggleWorks as test_module + self._run_test(request, workspace, editor, test_module) + + def test_AssetListCombiner_CombinedDescriptorsExpressInConfiguredArea(self, request, workspace, editor, launcher_platform): + from .EditorScripts import AssetListCombiner_CombinedDescriptorsExpressInConfiguredArea as test_module + self._run_test(request, workspace, editor, test_module) + + def test_AssetWeightSelector_InstancesExpressBasedOnWeight(self, request, workspace, editor, launcher_platform): + from .EditorScripts import AssetWeightSelector_InstancesExpressBasedOnWeight as test_module + self._run_test(request, workspace, editor, test_module) + + @pytest.mark.xfail(reason="https://github.com/o3de/o3de/issues/4155") + def test_DistanceBetweenFilter_InstancesPlantAtSpecifiedRadius(self, request, workspace, editor, launcher_platform): + from .EditorScripts import DistanceBetweenFilter_InstancesPlantAtSpecifiedRadius as test_module + self._run_test(request, workspace, editor, test_module) + + @pytest.mark.xfail(reason="https://github.com/o3de/o3de/issues/4155") + def test_DistanceBetweenFilterOverrides_InstancesPlantAtSpecifiedRadius(self, request, workspace, editor, launcher_platform): + from .EditorScripts import DistanceBetweenFilterOverrides_InstancesPlantAtSpecifiedRadius as test_module + self._run_test(request, workspace, editor, test_module) + + def test_SurfaceDataRefreshes_RemainsStable(self, request, workspace, editor, launcher_platform): + from .EditorScripts import SurfaceDataRefreshes_RemainsStable as test_module + self._run_test(request, workspace, editor, test_module) + + def test_VegetationInstances_DespawnWhenOutOfRange(self, request, workspace, editor, launcher_platform): + from .EditorScripts import VegetationInstances_DespawnWhenOutOfRange as test_module + self._run_test(request, workspace, editor, test_module) + + def test_InstanceSpawnerPriority_LayerAndSubPriority_HigherValuesPlantOverLower(self, request, workspace, editor, launcher_platform): + from .EditorScripts import InstanceSpawnerPriority_LayerAndSubPriority as test_module + self._run_test(request, workspace, editor, test_module) + + def test_LayerBlocker_InstancesBlockedInConfiguredArea(self, request, workspace, editor, launcher_platform): + from .EditorScripts import LayerBlocker_InstancesBlockedInConfiguredArea as test_module + self._run_test(request, workspace, editor, test_module) + + def test_LayerSpawner_InheritBehaviorFlag(self, request, workspace, editor, launcher_platform): + from .EditorScripts import LayerSpawner_InheritBehaviorFlag as test_module + self._run_test(request, workspace, editor, test_module) + + def test_LayerSpawner_InstancesPlantInAllSupportedShapes(self, request, workspace, editor, launcher_platform): + from .EditorScripts import LayerSpawner_InstancesPlantInAllSupportedShapes as test_module + self._run_test(request, workspace, editor, test_module) + + def test_LayerSpawner_FilterStageToggle(self, request, workspace, editor, launcher_platform): + from .EditorScripts import LayerSpawner_FilterStageToggle as test_module + self._run_test(request, workspace, editor, test_module) + + @pytest.mark.xfail(reason="https://github.com/o3de/o3de/issues/2038") + def test_LayerSpawner_InstancesRefreshUsingCorrectViewportCamera(self, request, workspace, editor, launcher_platform): + from .EditorScripts import LayerSpawner_InstancesRefreshUsingCorrectViewportCamera as test_module + self._run_test(request, workspace, editor, test_module) + + def test_MeshBlocker_InstancesBlockedByMesh(self, request, workspace, editor, launcher_platform): + from .EditorScripts import MeshBlocker_InstancesBlockedByMesh as test_module + self._run_test(request, workspace, editor, test_module) + + def test_MeshBlocker_InstancesBlockedByMeshHeightTuning(self, request, workspace, editor, launcher_platform): + from .EditorScripts import MeshBlocker_InstancesBlockedByMeshHeightTuning as test_module + self._run_test(request, workspace, editor, test_module) + + def test_MeshSurfaceTagEmitter_DependentOnMeshComponent(self, request, workspace, editor, launcher_platform): + from .EditorScripts import MeshSurfaceTagEmitter_DependentOnMeshComponent as test_module + self._run_test(request, workspace, editor, test_module) + + def test_MeshSurfaceTagEmitter_SurfaceTagsAddRemoveSuccessfully(self, request, workspace, editor, launcher_platform): + from .EditorScripts import MeshSurfaceTagEmitter_SurfaceTagsAddRemoveSuccessfully as test_module + self._run_test(request, workspace, editor, test_module) + + def test_PhysXColliderSurfaceTagEmitter_E2E_Editor(self, request, workspace, editor, launcher_platform): + from .EditorScripts import PhysXColliderSurfaceTagEmitter_E2E_Editor as test_module + self._run_test(request, workspace, editor, test_module) + + def test_PositionModifier_ComponentAndOverrides_InstancesPlantAtSpecifiedOffsets(self, request, workspace, editor, launcher_platform): + from .EditorScripts import PositionModifier_ComponentAndOverrides_InstancesPlantAtSpecifiedOffsets as test_module + self._run_test(request, workspace, editor, test_module) + + def test_PositionModifier_AutoSnapToSurfaceWorks(self, request, workspace, editor, launcher_platform): + from .EditorScripts import PositionModifier_AutoSnapToSurfaceWorks as test_module + self._run_test(request, workspace, editor, test_module) + + def test_RotationModifier_InstancesRotateWithinRange(self, request, workspace, editor, launcher_platform): + from .EditorScripts import RotationModifier_InstancesRotateWithinRange as test_module + self._run_test(request, workspace, editor, test_module) + + def test_RotationModifierOverrides_InstancesRotateWithinRange(self, request, workspace, editor, launcher_platform): + from .EditorScripts import RotationModifierOverrides_InstancesRotateWithinRange as test_module + self._run_test(request, workspace, editor, test_module) + + def test_ScaleModifier_InstancesProperlyScale(self, request, workspace, editor, launcher_platform): + from .EditorScripts import ScaleModifier_InstancesProperlyScale as test_module + self._run_test(request, workspace, editor, test_module) + + def test_ScaleModifierOverrides_InstancesProperlyScale(self, request, workspace, editor, launcher_platform): + from .EditorScripts import ScaleModifierOverrides_InstancesProperlyScale as test_module + self._run_test(request, workspace, editor, test_module) + + def test_ShapeIntersectionFilter_InstancesPlantInAssignedShape(self, request, workspace, editor, launcher_platform): + from .EditorScripts import ShapeIntersectionFilter_InstancesPlantInAssignedShape as test_module + self._run_test(request, workspace, editor, test_module) + + def test_ShapeIntersectionFilter_FilterStageToggle(self, request, workspace, editor, launcher_platform): + from .EditorScripts import ShapeIntersectionFilter_FilterStageToggle as test_module + self._run_test(request, workspace, editor, test_module) + + def test_SlopeAlignmentModifier_InstanceSurfaceAlignment(self, request, workspace, editor, launcher_platform): + from .EditorScripts import SlopeAlignmentModifier_InstanceSurfaceAlignment as test_module + self._run_test(request, workspace, editor, test_module) + + def test_SlopeAlignmentModifierOverrides_InstanceSurfaceAlignment(self, request, workspace, editor, launcher_platform): + from .EditorScripts import SlopeAlignmentModifierOverrides_InstanceSurfaceAlignment as test_module + self._run_test(request, workspace, editor, test_module) + + def test_SurfaceMaskFilter_BasicSurfaceTagCreation(self, request, workspace, editor, launcher_platform): + from .EditorScripts import SurfaceMaskFilter_BasicSurfaceTagCreation as test_module + self._run_test(request, workspace, editor, test_module) + + def test_SurfaceMaskFilter_ExclusiveSurfaceTags_Function(self, request, workspace, editor, launcher_platform): + from .EditorScripts import SurfaceMaskFilter_ExclusionList as test_module + self._run_test(request, workspace, editor, test_module) + + def test_SurfaceMaskFilter_InclusiveSurfaceTags_Function(self, request, workspace, editor, launcher_platform): + from .EditorScripts import SurfaceMaskFilter_InclusionList as test_module + self._run_test(request, workspace, editor, test_module) + + def test_SurfaceMaskFilterOverrides_MultipleDescriptorOverridesPlantAsExpected(self, request, workspace, editor, launcher_platform): + from .EditorScripts import SurfaceMaskFilterOverrides_MultipleDescriptorOverridesPlantAsExpected as test_module + self._run_test(request, workspace, editor, test_module) + + def test_SystemSettings_SectorPointDensity(self, request, workspace, editor, launcher_platform): + from .EditorScripts import SystemSettings_SectorPointDensity as test_module + self._run_test(request, workspace, editor, test_module) + + def test_SystemSettings_SectorSize(self, request, workspace, editor, launcher_platform): + from .EditorScripts import SystemSettings_SectorSize as test_module + self._run_test(request, workspace, editor, test_module) + + def test_SlopeFilter_ComponentAndOverrides_InstancesPlantOnValidSlopes(self, request, workspace, editor, launcher_platform): + from .EditorScripts import SlopeFilter_ComponentAndOverrides_InstancesPlantOnValidSlope as test_module + self._run_test(request, workspace, editor, test_module) + + +@pytest.mark.SUITE_periodic +@pytest.mark.parametrize("project", ["AutomatedTesting"]) +@pytest.mark.parametrize("level", ["tmp_level"]) +class TestAutomationE2E(TestAutomationBase): + + # The following tests must run in order, please do not move tests out of order + + @pytest.mark.parametrize("launcher_platform", ['windows_editor']) + def test_DynamicSliceInstanceSpawner_Embedded_E2E_Editor(self, request, workspace, project, level, editor, launcher_platform): + # Ensure our test level does not already exist + file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", level)], True, True) + + from .EditorScripts import DynamicSliceInstanceSpawner_Embedded_E2E as test_module + self._run_test(request, workspace, editor, test_module) + + @pytest.mark.parametrize("launcher_platform", ['windows']) + def test_DynamicSliceInstanceSpawner_Embedded_E2E_Launcher(self, workspace, launcher, level, + remote_console_instance, project, launcher_platform): + + expected_lines = [ + "Instances found in area = 400" + ] + + hydra.launch_and_validate_results_launcher(launcher, level, remote_console_instance, expected_lines, launch_ap=False) + + # Cleanup our temp level + file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", level)], True, True) + + @pytest.mark.parametrize("launcher_platform", ['windows_editor']) + def test_DynamicSliceInstanceSpawner_External_E2E_Editor(self, request, workspace, project, level, editor, launcher_platform): + # Ensure our test level does not already exist + file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", level)], True, True) + + from .EditorScripts import DynamicSliceInstanceSpawner_External_E2E as test_module + self._run_test(request, workspace, editor, test_module) + + @pytest.mark.parametrize("launcher_platform", ['windows']) + def test_DynamicSliceInstanceSpawner_External_E2E_Launcher(self, workspace, launcher, level, + remote_console_instance, project, launcher_platform): + + expected_lines = [ + "Instances found in area = 400" + ] + + hydra.launch_and_validate_results_launcher(launcher, level, remote_console_instance, expected_lines, launch_ap=False) + + # Cleanup our temp level + file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", level)], True, True) + + @pytest.mark.parametrize("launcher_platform", ['windows_editor']) + def test_LayerBlender_E2E_Editor(self, request, workspace, project, level, editor, launcher_platform): + # Ensure our test level does not already exist + file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", level)], True, True) + + from .EditorScripts import LayerBlender_E2E_Editor as test_module + self._run_test(request, workspace, editor, test_module) + + @pytest.mark.parametrize("launcher_platform", ['windows']) + @pytest.mark.xfail(reason="https://github.com/o3de/o3de/issues/4170") + def test_LayerBlender_E2E_Launcher(self, workspace, launcher, level, + remote_console_instance, project, launcher_platform): + + launcher.args.extend(["-rhi=Null"]) + launcher.start(launch_ap=False) + assert launcher.is_alive(), "Launcher failed to start" + + # Wait for test script to quit the launcher. If wait_for returns exc, test was not successful + waiter.wait_for(lambda: not launcher.is_alive(), timeout=300) + + # Verify launcher quit successfully and did not crash + ret_code = launcher.get_returncode() + assert ret_code == 0, "Test failed. See Game.log for details" + + # Cleanup our temp level + file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", level)], True, True) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_AltitudeFilter.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_AltitudeFilter.py deleted file mode 100755 index ec8c225d3b..0000000000 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_AltitudeFilter.py +++ /dev/null @@ -1,111 +0,0 @@ -""" -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 -""" - -import os -import pytest -import logging -# Bail on the test if ly_test_tools doesn't exist. -pytest.importorskip('ly_test_tools') -import ly_test_tools.environment.file_system as file_system -import editor_python_test_tools.hydra_test_utils as hydra - -logger = logging.getLogger(__name__) -test_directory = os.path.join(os.path.dirname(__file__), 'EditorScripts') - - -@pytest.mark.parametrize("project", ["AutomatedTesting"]) -@pytest.mark.parametrize("level", ["tmp_level"]) -@pytest.mark.usefixtures("automatic_process_killer") -@pytest.mark.parametrize("launcher_platform", ['windows_editor']) -class TestAltitudeFilter(object): - - @pytest.fixture(autouse=True) - def setup_teardown(self, request, workspace, project, level): - def teardown(): - file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", level)], True, True) - request.addfinalizer(teardown) - - file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", level)], True, True) - - @pytest.mark.test_case_id('C4814463', 'C4847477') - @pytest.mark.SUITE_periodic - @pytest.mark.dynveg_filter - def test_AltitudeFilter_ComponentAndOverrides_InstancesPlantAtSpecifiedAltitude(self, request, editor, level, - launcher_platform): - - expected_lines = [ - "'Instance Spawner' created", - "'Planting Surface' created", - "'Planting Surface Elevated' created", - "instance count validation: True (found=3200, expected=3200)", - "instance count validation: True (found=1600, expected=1600)", - "instance count validation: True (found=400, expected=400)", - "AltitudeFilterComponentAndOverrides: result=SUCCESS" - ] - - hydra.launch_and_validate_results( - request, - test_directory, - editor, - "AltitudeFilter_ComponentAndOverrides_InstancesPlantAtSpecifiedAltitude.py", - expected_lines, - cfg_args=[level] - ) - - @pytest.mark.test_case_id("C4847476") - @pytest.mark.SUITE_periodic - @pytest.mark.dynveg_filter - def test_AltitudeFilter_ShapeSample_InstancesPlantAtSpecifiedAltitude(self, request, editor, level, - launcher_platform): - - expected_lines = [ - "'Instance Spawner' created", - "'Planting Surface' created", - "'Planting Surface Elevated' created", - "instance count validation: True (found=800, expected=800)", - "'Shape Sampler' created", - "instance count validation: True (found=400, expected=400)", - "AltitudeFilterShapeSample: result=SUCCESS" - ] - - hydra.launch_and_validate_results( - request, - test_directory, - editor, - "AltitudeFilter_ShapeSample_InstancesPlantAtSpecifiedAltitude.py", - expected_lines, - cfg_args=[level] - ) - - @pytest.mark.test_case_id("C4847478") - @pytest.mark.SUITE_periodic - @pytest.mark.dynveg_filter - @pytest.mark.xfail(reason="https://github.com/o3de/o3de/issues/2303") - def test_AltitudeFilter_FilterStageToggle(self, request, editor, level, workspace, launcher_platform): - cfg_args = [level] - - expected_lines = [ - "AltitudeFilter_FilterStageToggle: test started", - "AltitudeFilter_FilterStageToggle: Vegetation instances count equal to expected value for PREPROCESS filter stage: True", - "AltitudeFilter_FilterStageToggle: Vegetation instances count equal to expected value for POSTPROCESS filter stage: True", - "AltitudeFilter_FilterStageToggle: result=SUCCESS", - ] - - unexpected_lines = [ - "AltitudeFilter_FilterStageToggle: Vegetation instances count equal to expected value for PREPROCESS filter stage: False", - "AltitudeFilter_FilterStageToggle: Vegetation instances count equal to expected value for POSTPROCESS filter stage: False", - ] - - hydra.launch_and_validate_results( - request, - test_directory, - editor, - "AltitudeFilter_FilterStageToggle.py", - expected_lines=expected_lines, - unexpected_lines=unexpected_lines, - cfg_args=cfg_args - ) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_AreaComponentSlices.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_AreaComponentSlices.py deleted file mode 100755 index 7c72105957..0000000000 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_AreaComponentSlices.py +++ /dev/null @@ -1,72 +0,0 @@ -""" -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 -""" - -import os -import pytest -import logging - -# Bail on the test if ly_test_tools doesn't exist. -pytest.importorskip("ly_test_tools") -import ly_test_tools.environment.file_system as file_system -import editor_python_test_tools.hydra_test_utils as hydra - -logger = logging.getLogger(__name__) -test_directory = os.path.join(os.path.dirname(__file__), "EditorScripts") - - -@pytest.mark.parametrize("project", ["AutomatedTesting"]) -@pytest.mark.parametrize("level", ["tmp_level"]) -@pytest.mark.usefixtures("automatic_process_killer") -@pytest.mark.parametrize("launcher_platform", ['windows_editor']) -class TestAreaComponents(object): - @pytest.fixture(autouse=True) - def setup_teardown(self, request, workspace, project, level): - # Cleanup our temp level - file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", level)], True, True) - - # Cleanup the test slices - file_system.delete([os.path.join(workspace.paths.engine_root(), project, "slices", "TestSlice_1.slice")], True, True) - file_system.delete([os.path.join(workspace.paths.engine_root(), project, "slices", "TestSlice_2.slice")], True, True) - - def teardown(): - # Cleanup our temp level - file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", level)], True, True) - - # Cleanup the test slices - file_system.delete( - [os.path.join(workspace.paths.engine_root(), project, "slices", "TestSlice_1.slice")], True, True - ) - file_system.delete( - [os.path.join(workspace.paths.engine_root(), project, "slices", "TestSlice_2.slice")], True, True - ) - - request.addfinalizer(teardown) - - @pytest.mark.test_case_id("C2627900", "C2627905", "C2627904") - @pytest.mark.SUITE_periodic - @pytest.mark.dynveg_misc - def test_AreaComponents_SliceCreationVisibilityToggleWorks(self, request, editor, level, workspace, - launcher_platform): - cfg_args = [level] - - expected_lines = [ - "AreaComponentSlices_SliceCreationAndVisibilityToggle: test started", - "AreaComponentSlices_SliceCreationAndVisibilityToggle: Slice has been created successfully (entity with spawner component): True", - "AreaComponentSlices_SliceCreationAndVisibilityToggle: Vegetation plants initially when slice is shown: True", - "AreaComponentSlices_SliceCreationAndVisibilityToggle: Vegetation is cleared when slice is hidden: True", - "AreaComponentSlices_SliceCreationAndVisibilityToggle: Slice has been created successfully (entity with blender component): True", - "AreaComponentSlices_SliceCreationAndVisibilityToggle: result=SUCCESS", - ] - - hydra.launch_and_validate_results( - request, - test_directory, - editor, - "AreaComponentSlices_SliceCreationAndVisibilityToggle.py", - expected_lines=expected_lines, - cfg_args=cfg_args - ) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_AssetListCombiner.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_AssetListCombiner.py deleted file mode 100755 index 5deaa9199e..0000000000 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_AssetListCombiner.py +++ /dev/null @@ -1,63 +0,0 @@ -""" -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 -""" - -import os -import pytest -import logging - -# Bail on the test if ly_test_tools doesn't exist. -pytest.importorskip("ly_test_tools") -import ly_test_tools.environment.file_system as file_system -import editor_python_test_tools.hydra_test_utils as hydra - -logger = logging.getLogger(__name__) -test_directory = os.path.join(os.path.dirname(__file__), "EditorScripts") - - -@pytest.mark.parametrize('project', ['AutomatedTesting']) -@pytest.mark.parametrize('level', ['tmp_level']) -@pytest.mark.usefixtures("automatic_process_killer") -@pytest.mark.parametrize("launcher_platform", ['windows_editor']) -class TestAssetListCombiner(object): - - @pytest.fixture(autouse=True) - def setup_teardown(self, request, workspace, project, level): - def teardown(): - file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", level)], True, True) - request.addfinalizer(teardown) - - file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", level)], True, True) - - @pytest.mark.test_case_id("C4762374", "C4762373") - @pytest.mark.SUITE_periodic - @pytest.mark.dynveg_misc - def test_AssetListCombiner_CombinedDescriptorsExpressInConfiguredArea(self, request, editor, level, - launcher_platform): - - expected_lines = [ - "'Asset List 1' created", - "'Asset List 2' created", - "'Asset List 3' created", - "'Surface Entity' created", - "'Spawner Entity' created", - "Spawner Entity Configuration|Descriptor Providers: SUCCESS", - "Spawner Entity Configuration|Gradient|Gradient Entity Id: SUCCESS", - "instance count validation: True (found=200, expected=200.0)", - "Spawner Entity Configuration|Descriptor Providers|[1]: SUCCESS", - "instance count validation: True (found=400, expected=400)", - "instance count validation: True (found=0, expected=0)", - "AssetListCombiner_CombinedDescriptors: result=SUCCESS" - ] - - hydra.launch_and_validate_results( - request, - test_directory, - editor, - "AssetListCombiner_CombinedDescriptorsExpressInConfiguredArea.py", - expected_lines=expected_lines, - cfg_args=[level] - ) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_AssetWeightSelector.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_AssetWeightSelector.py deleted file mode 100755 index f3cb2f4647..0000000000 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_AssetWeightSelector.py +++ /dev/null @@ -1,60 +0,0 @@ -""" -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 -""" - -""" -C6269654: Vegetation areas using weight selectors properly distribute instances according to Sort By Weight setting -""" - -import os -import pytest -import logging - -# Bail on the test if ly_test_tools doesn't exist. -pytest.importorskip('ly_test_tools') -import ly_test_tools.environment.file_system as file_system -import editor_python_test_tools.hydra_test_utils as hydra - -logger = logging.getLogger(__name__) -test_directory = os.path.join(os.path.dirname(__file__), 'EditorScripts') - - -@pytest.mark.parametrize("project", ["AutomatedTesting"]) -@pytest.mark.parametrize("level", ["tmp_level"]) -@pytest.mark.usefixtures("automatic_process_killer") -@pytest.mark.parametrize("launcher_platform", ['windows_editor']) -class TestAssetWeightSelector(object): - - @pytest.fixture(autouse=True) - def setup_teardown(self, request, workspace, project, level): - def teardown(): - file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", level)], True, True) - request.addfinalizer(teardown) - - file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", level)], True, True) - - @pytest.mark.test_case_id("C6269654", "C4762368") - @pytest.mark.SUITE_sandbox - @pytest.mark.dynveg_filter - def test_AssetWeightSelector_InstancesExpressBasedOnWeight(self, request, editor, level, launcher_platform): - - expected_lines = [ - "'Instance Spawner' created", - "Instance Spawner Configuration|Embedded Assets|[1]|Instance|Slice Asset: SUCCESS", - "'Planting Surface' created", - "Configuration|Embedded Assets|[0]|Weight set to 50.0", - "Instance Spawner Configuration|Allow Empty Assets: SUCCESS", - "AssetWeightSelector_SortByWeight: result=SUCCESS", - ] - - hydra.launch_and_validate_results( - request, - test_directory, - editor, - "AssetWeightSelector_InstancesExpressBasedOnWeight.py", - expected_lines, - cfg_args=[level] - ) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_DistanceBetweenFilter.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_DistanceBetweenFilter.py deleted file mode 100755 index d525e4a599..0000000000 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_DistanceBetweenFilter.py +++ /dev/null @@ -1,78 +0,0 @@ -""" -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 -""" - -import os -import pytest -import logging - -# Bail on the test if ly_test_tools doesn't exist. -pytest.importorskip('ly_test_tools') -import ly_test_tools.environment.file_system as file_system -import editor_python_test_tools.hydra_test_utils as hydra - -logger = logging.getLogger(__name__) -test_directory = os.path.join(os.path.dirname(__file__), 'EditorScripts') - - -@pytest.mark.parametrize("project", ["AutomatedTesting"]) -@pytest.mark.parametrize("level", ["tmp_level"]) -@pytest.mark.usefixtures("automatic_process_killer") -@pytest.mark.parametrize("launcher_platform", ['windows_editor']) -class TestDistanceBetweenFilter(object): - - @pytest.fixture(autouse=True) - def setup_teardown(self, request, workspace, project, level): - def teardown(): - file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", level)], True, True) - request.addfinalizer(teardown) - - file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", level)], True, True) - - @pytest.mark.test_case_id("C4851066") - @pytest.mark.SUITE_periodic - @pytest.mark.dynveg_filter - @pytest.mark.xfail(reason="https://github.com/o3de/o3de/issues/4155") - def test_DistanceBetweenFilter_InstancesPlantAtSpecifiedRadius(self, request, editor, level, launcher_platform): - - expected_lines = [ - "Configuration|Radius Min set to 1.0", - "Configuration|Radius Min set to 2.0", - "Configuration|Radius Min set to 16.0", - "DistanceBetweenFilterComponent: result=SUCCESS" - ] - - hydra.launch_and_validate_results( - request, - test_directory, - editor, - "DistanceBetweenFilter_InstancesPlantAtSpecifiedRadius.py", - expected_lines, - cfg_args=[level] - ) - - @pytest.mark.test_case_id("C4814458") - @pytest.mark.SUITE_periodic - @pytest.mark.dynveg_filter - @pytest.mark.xfail(reason="https://github.com/o3de/o3de/issues/4155") - def test_DistanceBetweenFilterOverrides_InstancesPlantAtSpecifiedRadius(self, request, editor, level, - launcher_platform): - - expected_lines = [ - "Configuration|Embedded Assets|[0]|Distance Between Filter (Radius)|Radius Min set to 1.0", - "Configuration|Embedded Assets|[0]|Distance Between Filter (Radius)|Radius Min set to 2.0", - "Configuration|Embedded Assets|[0]|Distance Between Filter (Radius)|Radius Min set to 16.0", - "DistanceBetweenFilterComponentOverrides: result=SUCCESS" - ] - - hydra.launch_and_validate_results( - request, - test_directory, - editor, - "DistanceBetweenFilterOverrides_InstancesPlantAtSpecifiedRadius.py", - expected_lines, - cfg_args=[level] - ) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_DynVeg_Regressions.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_DynVeg_Regressions.py deleted file mode 100755 index b15e1d552d..0000000000 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_DynVeg_Regressions.py +++ /dev/null @@ -1,78 +0,0 @@ -""" -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 -""" - -import os -import pytest - -# Bail on the test if ly_test_tools doesn't exist. -pytest.importorskip('ly_test_tools') - -import editor_python_test_tools.hydra_test_utils as hydra -import ly_test_tools.environment.file_system as file_system - -test_directory = os.path.join(os.path.dirname(__file__), 'EditorScripts') - - -@pytest.mark.parametrize('project', ['AutomatedTesting']) -@pytest.mark.parametrize('level', ['tmp_level']) -@pytest.mark.usefixtures("automatic_process_killer") -@pytest.mark.parametrize("launcher_platform", ['windows_editor']) -class Test_DynVeg_Regressions(object): - @pytest.fixture(autouse=True) - def setup_teardown(self, request, workspace, project, level): - def teardown(): - # delete temp level - file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", level)], True, True) - - # Setup - add the teardown finalizer - request.addfinalizer(teardown) - - # Make sure the temp level doesn't already exist - file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", level)], True, True) - - @pytest.mark.test_case_id("C29470845") - @pytest.mark.SUITE_periodic - @pytest.mark.dynveg_regression - def test_SurfaceDataRefreshes_RemainsStable(self, request, editor, level, launcher_platform): - - expected_lines = [ - "SurfaceDataRefreshes_RemainsStable: test started", - "SurfaceDataRefreshes_RemainsStable: test finished", - "SurfaceDataRefreshes_RemainsStable: result=SUCCESS" - ] - - unexpected_lines = [ - "Sector update mode is 'RebuildSurfaceCache' but sector doesn't exist" - ] - - hydra.launch_and_validate_results( - request, - test_directory, - editor, - 'SurfaceDataRefreshes_RemainsStable.py', - expected_lines=expected_lines, - unexpected_lines=unexpected_lines, - cfg_args=[level] - ) - - @pytest.mark.SUITE_periodic - def test_VegetationInstances_DespawnWhenOutOfRange(self, request, editor, level, launcher_platform): - - expected_lines = [ - "VegetationInstances_DespawnWhenOutOfRange: test started", - "VegetationInstances_DespawnWhenOutOfRange: test finished", - "VegetationInstances_DespawnWhenOutOfRange: result=SUCCESS" - ] - - hydra.launch_and_validate_results( - request, - test_directory, - editor, - 'VegetationInstances_DespawnWhenOutOfRange.py', - expected_lines=expected_lines, - cfg_args=[level] - ) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_DynamicSliceInstanceSpawner.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_DynamicSliceInstanceSpawner.py deleted file mode 100755 index 0f83f44094..0000000000 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_DynamicSliceInstanceSpawner.py +++ /dev/null @@ -1,140 +0,0 @@ -""" -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 -""" - -import os -import pytest -import logging - -# Bail on the test if ly_test_tools doesn't exist. -pytest.importorskip('ly_test_tools') -import ly_test_tools.environment.file_system as file_system -import ly_test_tools._internal.pytest_plugin as internal_plugin -import editor_python_test_tools.hydra_test_utils as hydra -from ly_remote_console.remote_console_commands import RemoteConsole as RemoteConsole - -logger = logging.getLogger(__name__) -test_directory = os.path.join(os.path.dirname(__file__), 'EditorScripts') - - -@pytest.mark.parametrize('project', ['AutomatedTesting']) -@pytest.mark.parametrize('level', ['tmp_level']) -@pytest.mark.usefixtures("automatic_process_killer") -class TestDynamicSliceInstanceSpawner(object): - - @pytest.fixture - def remote_console_instance(self, request): - console = RemoteConsole() - - def teardown(): - if console.connected: - console.stop() - - request.addfinalizer(teardown) - - return console - - @pytest.mark.test_case_id("C28851763") - @pytest.mark.SUITE_main - @pytest.mark.dynveg_area - @pytest.mark.parametrize("launcher_platform", ['windows_editor']) - def test_DynamicSliceInstanceSpawner_DynamicSliceSpawnerWorks(self, request, editor, level, workspace, project, - launcher_platform): - - # Skip test if running against Debug build - if "debug" in internal_plugin.build_directory: - pytest.skip("Does not execute against debug builds.") - - # Ensure temp level does not already exist - file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", level)], True, True) - - cfg_args = [level] - - expected_lines = [ - "DynamicSliceInstanceSpawner: test started", - "DynamicSliceInstanceSpawner: test finished", - "DynamicSliceInstanceSpawner: result=SUCCESS" - ] - - hydra.launch_and_validate_results(request, test_directory, editor, - 'DynamicSliceInstanceSpawner_DynamicSliceSpawnerWorks.py', - expected_lines=expected_lines, cfg_args=cfg_args) - - # Cleanup our temp level - file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", level)], True, True) - - @pytest.mark.test_case_id('C2574330') - @pytest.mark.BAT - @pytest.mark.SUITE_periodic - @pytest.mark.dynveg_area - @pytest.mark.parametrize("launcher_platform", ['windows_editor']) - def test_DynamicSliceInstanceSpawner_Embedded_E2E_Editor(self, workspace, request, editor, level, project, - launcher_platform): - # Ensure temp level does not already exist - file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", level)], True, True) - - expected_lines = [ - "'Instance Spawner' created", - "'Planting Surface' created", - "DynamicSliceInstanceSpawnerEmbeddedEditor: Expected 400 instances - Found 400 instances", - "DynamicSliceInstanceSpawnerEmbeddedEditor: result=SUCCESS" - ] - - hydra.launch_and_validate_results(request, test_directory, editor, "DynamicSliceInstanceSpawner_Embedded_E2E.py", - expected_lines, cfg_args=[level]) - - @pytest.mark.test_case_id('C2574330') - @pytest.mark.BAT - @pytest.mark.SUITE_periodic - @pytest.mark.dynveg_area - @pytest.mark.parametrize("launcher_platform", ['windows']) - def test_DynamicSliceInstanceSpawner_Embedded_E2E_Launcher(self, workspace, launcher, level, - remote_console_instance, project, launcher_platform): - - expected_lines = [ - "Instances found in area = 400" - ] - - hydra.launch_and_validate_results_launcher(launcher, level, remote_console_instance, expected_lines) - - # Cleanup our temp level - file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", level)], True, True) - - @pytest.mark.test_case_id('C4762367') - @pytest.mark.SUITE_periodic - @pytest.mark.dynveg_area - @pytest.mark.parametrize("launcher_platform", ['windows_editor']) - def test_DynamicSliceInstanceSpawner_External_E2E_Editor(self, workspace, request, editor, level, project, - launcher_platform): - # Ensure temp level does not already exist - file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", level)], True, True) - - expected_lines = [ - "Spawner entity created", - "'Planting Surface' created", - "DynamicSliceInstanceSpawnerExternalEditor: Expected 400 instances - Found 400 instances", - "DynamicSliceInstanceSpawnerExternalEditor: result=SUCCESS" - ] - - hydra.launch_and_validate_results(request, test_directory, editor, "DynamicSliceInstanceSpawner_External_E2E.py", - expected_lines, cfg_args=[level]) - - @pytest.mark.test_case_id('C4762367') - @pytest.mark.SUITE_periodic - @pytest.mark.dynveg_area - @pytest.mark.parametrize("launcher_platform", ['windows']) - def test_DynamicSliceInstanceSpawner_External_E2E_Launcher(self, workspace, launcher, level, - remote_console_instance, project, launcher_platform): - - expected_lines = [ - "Instances found in area = 400" - ] - - hydra.launch_and_validate_results_launcher(launcher, level, remote_console_instance, expected_lines) - - # Cleanup our temp level - file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", level)], True, True) - diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_EmptyInstanceSpawner.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_EmptyInstanceSpawner.py deleted file mode 100755 index 1fc88b816f..0000000000 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_EmptyInstanceSpawner.py +++ /dev/null @@ -1,54 +0,0 @@ -""" -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 -""" - -import os -import pytest -import logging - -# Bail on the test if ly_test_tools doesn't exist. -pytest.importorskip('ly_test_tools') -import ly_test_tools.environment.file_system as file_system -import ly_test_tools._internal.pytest_plugin as internal_plugin -import editor_python_test_tools.hydra_test_utils as hydra - -logger = logging.getLogger(__name__) -test_directory = os.path.join(os.path.dirname(__file__), 'EditorScripts') - - -@pytest.mark.parametrize('project', ['AutomatedTesting']) -@pytest.mark.parametrize('level', ['tmp_level']) -@pytest.mark.usefixtures("automatic_process_killer") -@pytest.mark.parametrize("launcher_platform", ['windows_editor']) -class TestEmptyInstanceSpawner(object): - - @pytest.fixture(autouse=True) - def setup_teardown(self, request, workspace, project, level): - def teardown(): - file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", level)], True, True) - request.addfinalizer(teardown) - - file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", level)], True, True) - - @pytest.mark.test_case_id("C28851762") - @pytest.mark.SUITE_main - @pytest.mark.dynveg_area - def test_EmptyInstanceSpawner_EmptySpawnerWorks(self, request, editor, level, launcher_platform): - - # Skip test if running against Debug build - if "debug" in internal_plugin.build_directory: - pytest.skip("Does not execute against debug builds.") - - cfg_args = [level] - - expected_lines = [ - "EmptyInstanceSpawner: test started", - "EmptyInstanceSpawner: test finished", - "EmptyInstanceSpawner: result=SUCCESS" - ] - - hydra.launch_and_validate_results(request, test_directory, editor, 'EmptyInstanceSpawner_EmptySpawnerWorks.py', - expected_lines=expected_lines, cfg_args=cfg_args) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_InstanceSpawnerPriority.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_InstanceSpawnerPriority.py deleted file mode 100755 index 955c20a37d..0000000000 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_InstanceSpawnerPriority.py +++ /dev/null @@ -1,63 +0,0 @@ -""" -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 -""" - -""" -C5747383: Vegetation areas with a higher Layer Priority plant over those with a lower Layer Priority -C4762382: Vegetation areas with a higher Sub Priority plant over those with a lower Sub Priority -""" - -import os -import pytest -import logging - -# Bail on the test if ly_test_tools doesn't exist. -pytest.importorskip('ly_test_tools') -import ly_test_tools.environment.file_system as file_system -import editor_python_test_tools.hydra_test_utils as hydra - -logger = logging.getLogger(__name__) -test_directory = os.path.join(os.path.dirname(__file__), 'EditorScripts') - - -@pytest.mark.parametrize("project", ["AutomatedTesting"]) -@pytest.mark.parametrize("level", ["tmp_level"]) -@pytest.mark.usefixtures("automatic_process_killer") -@pytest.mark.parametrize("launcher_platform", ['windows_editor']) -class TestInstanceSpawnerPriority(object): - - @pytest.fixture(autouse=True) - def setup_teardown(self, request, workspace, project, level): - def teardown(): - file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", level)], True, True) - request.addfinalizer(teardown) - - file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", level)], True, True) - - @pytest.mark.test_case_id("C5747383", "C4762382") - @pytest.mark.SUITE_periodic - @pytest.mark.dynveg_misc - def test_InstanceSpawnerPriority_LayerAndSubPriority_HigherValuesPlantOverLower(self, request, editor, level, - launcher_platform): - - expected_lines = [ - "'Instance Spawner' created", - "'Instance Blocker' created", - "'Planting Surface' created", - "Instance Blocker Configuration|Layer Priority: SUCCESS", - "Instance Spawner Configuration|Sub Priority: SUCCESS", - "Instance Blocker Configuration|Sub Priority: SUCCESS", - "InstanceSpawnerPriority: result=SUCCESS", - ] - - hydra.launch_and_validate_results( - request, - test_directory, - editor, - "InstanceSpawnerPriority_LayerAndSubPriority.py", - expected_lines, - cfg_args=[level] - ) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_LayerBlender.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_LayerBlender.py deleted file mode 100755 index 13f2a569c1..0000000000 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_LayerBlender.py +++ /dev/null @@ -1,104 +0,0 @@ -""" -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 -""" - -""" -C2627906: A simple Vegetation Layer Blender area can be created. -The specified assets plant in the specified blend area and are visible in the Viewport in -Edit Mode, Game Mode. -""" - -import os -import pytest - -pytest.importorskip("ly_test_tools") - -import ly_remote_console.remote_console_commands as remote_console_commands -import ly_test_tools.environment.waiter as waiter -import ly_test_tools.environment.file_system as file_system -import editor_python_test_tools.hydra_test_utils as hydra - - -test_directory = os.path.join(os.path.dirname(__file__), "EditorScripts") -remote_console_port = 4600 -listener_timeout = 120 - - -@pytest.mark.parametrize("project", ["AutomatedTesting"]) -@pytest.mark.parametrize("level", ["tmp_level"]) -@pytest.mark.usefixtures("automatic_process_killer") -class TestLayerBlender(object): - - @pytest.fixture - def remote_console_instance(self, request): - console = remote_console_commands.RemoteConsole() - - def teardown(): - if console.connected: - console.stop() - - request.addfinalizer(teardown) - - return console - - @pytest.mark.test_case_id("C2627906") - @pytest.mark.BAT - @pytest.mark.SUITE_periodic - @pytest.mark.dynveg_area - @pytest.mark.parametrize("launcher_platform", ['windows_editor']) - def test_LayerBlender_E2E_Editor(self, workspace, request, editor, project, level, launcher_platform): - # Make sure temp level doesn't already exist - file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", level)], True, True) - - expected_lines = [ - "'Purple Spawner' created", - "'Pink Spawner' created", - "'Surface Entity' created", - "Entity has a Vegetation Layer Spawner component", - "Entity has a Vegetation Asset List component", - "Entity has a Box Shape component", - "Purple Spawner Box Shape|Box Configuration|Dimensions: SUCCESS", - "Pink Spawner Box Shape|Box Configuration|Dimensions: SUCCESS", - "Purple Spawner Configuration|Embedded Assets|[0]: SUCCESS", - "Pink Spawner Configuration|Embedded Assets|[0]: SUCCESS", - "'Blender' created", - "Entity has a Vegetation Layer Blender component", - "Entity has a Box Shape component", - "Blender Configuration|Vegetation Areas: SUCCESS", - "Blender Box Shape|Box Configuration|Dimensions: SUCCESS", - "LayerBlender_E2E_Editor: result=SUCCESS" - ] - - hydra.launch_and_validate_results( - request, - test_directory, - editor, - "LayerBlender_E2E_Editor.py", - expected_lines, - cfg_args=[level] - ) - - @pytest.mark.test_case_id("C2627906") - @pytest.mark.BAT - @pytest.mark.SUITE_periodic - @pytest.mark.dynveg_area - @pytest.mark.parametrize("launcher_platform", ['windows']) - def test_LayerBlender_E2E_Launcher(self, workspace, project, launcher, level, remote_console_instance, - launcher_platform): - - launcher.args.extend(["-rhi=Null"]) - launcher.start() - assert launcher.is_alive(), "Launcher failed to start" - - # Wait for test script to quit the launcher. If wait_for returns exc, test was not successful - waiter.wait_for(lambda: not launcher.is_alive(), timeout=300) - - # Verify launcher quit successfully and did not crash - ret_code = launcher.get_returncode() - assert ret_code == 0, "Test failed. See Game.log for details" - - # Cleanup our temp level - file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", level)], True, True) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_LayerBlocker.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_LayerBlocker.py deleted file mode 100755 index d0889f17a4..0000000000 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_LayerBlocker.py +++ /dev/null @@ -1,56 +0,0 @@ -""" -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 -""" - -import os -import pytest -import logging - -# Bail on the test if ly_test_tools doesn't exist. -pytest.importorskip('ly_test_tools') -import ly_test_tools.environment.file_system as file_system -import editor_python_test_tools.hydra_test_utils as hydra - -logger = logging.getLogger(__name__) -test_directory = os.path.join(os.path.dirname(__file__), 'EditorScripts') - - -@pytest.mark.parametrize("project", ["AutomatedTesting"]) -@pytest.mark.parametrize("level", ["tmp_level"]) -@pytest.mark.usefixtures("automatic_process_killer") -@pytest.mark.parametrize("launcher_platform", ['windows_editor']) -class TestLayerBlocker(object): - - @pytest.fixture(autouse=True) - def setup_teardown(self, request, workspace, project, level): - def teardown(): - file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", level)], True, True) - request.addfinalizer(teardown) - - file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", level)], True, True) - - @pytest.mark.test_case_id("C2793772") - @pytest.mark.SUITE_periodic - @pytest.mark.dynveg_area - def test_LayerBlocker_InstancesBlockedInConfiguredArea(self, request, editor, level, launcher_platform): - - expected_lines = [ - "'Instance Spawner' created", - "'Surface Entity' created", - "instance count validation: True (found=400, expected=400)", - "'Blocker Area' created", - "instance count validation: True (found=384, expected=384)", - "LayerBlocker_InstancesBlocked: result=SUCCESS" - ] - - hydra.launch_and_validate_results( - request, - test_directory, - editor, - "LayerBlocker_InstancesBlockedInConfiguredArea.py", - expected_lines, - cfg_args=[level] - ) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_LayerSpawner.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_LayerSpawner.py deleted file mode 100755 index 65d994f841..0000000000 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_LayerSpawner.py +++ /dev/null @@ -1,142 +0,0 @@ -""" -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 -""" - -import os -import pytest -import logging - -# Bail on the test if ly_test_tools doesn't exist. -pytest.importorskip("ly_test_tools") -import ly_test_tools.environment.file_system as file_system -import editor_python_test_tools.hydra_test_utils as hydra - -logger = logging.getLogger(__name__) -test_directory = os.path.join(os.path.dirname(__file__), "EditorScripts") - - -@pytest.mark.parametrize("project", ["AutomatedTesting"]) -@pytest.mark.parametrize("level", ["tmp_level"]) -@pytest.mark.usefixtures("automatic_process_killer") -@pytest.mark.parametrize("launcher_platform", ['windows_editor']) -class TestLayerSpawner(object): - @pytest.fixture(autouse=True) - def setup_teardown(self, request, workspace, project, level): - # Cleanup our temp level - file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", level)], True, True) - - def teardown(): - # Cleanup our temp level - file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", level)], True, True) - - request.addfinalizer(teardown) - - @pytest.mark.test_case_id("C4762381") - @pytest.mark.SUITE_periodic - @pytest.mark.dynveg_misc - def test_LayerSpawner_InheritBehaviorFlag(self, request, editor, level, workspace, launcher_platform): - - expected_lines = [ - "LayerSpawner_InheritBehavior: test started", - "LayerSpawner_InheritBehavior: Vegetation is not planted when Inherit Behavior flag is checked: True", - "LayerSpawner_InheritBehavior: Vegetation plant when Inherit Behavior flag is unchecked: True", - "LayerSpawner_InheritBehavior: result=SUCCESS", - ] - - hydra.launch_and_validate_results( - request, - test_directory, - editor, - "LayerSpawner_InheritBehaviorFlag.py", - expected_lines=expected_lines, - cfg_args=[level] - ) - - @pytest.mark.test_case_id("C2802020") - @pytest.mark.SUITE_periodic - @pytest.mark.dynveg_misc - def test_LayerSpawner_InstancesPlantInAllSupportedShapes(self, request, editor, level, launcher_platform): - - expected_lines = [ - "'Instance Spawner' created", - "'Surface Entity' created", - "Entity has a Vegetation Reference Shape component", - "Entity has a Box Shape component", - "box Box Shape|Box Configuration|Dimensions: SUCCESS", - "Entity has a Capsule Shape component", - "capsule Capsule Shape|Capsule Configuration|Height: SUCCESS", - "capsule Capsule Shape|Capsule Configuration|Radius: SUCCESS", - "Entity has a Tube Shape component", - "Entity has a Spline component", - "Entity has a Sphere Shape component", - "sphere Sphere Shape|Sphere Configuration|Radius: SUCCESS", - "Entity has a Cylinder Shape component", - "cylinder Cylinder Shape|Cylinder Configuration|Radius: SUCCESS", - "cylinder Cylinder Shape|Cylinder Configuration|Height: SUCCESS", - "Entity has a Polygon Prism Shape component", - "Entity has a Compound Shape component", - "Compound Configuration|Child Shape Entities|[0]: SUCCESS", - "Compound Configuration|Child Shape Entities|[1]: SUCCESS", - "Compound Configuration|Child Shape Entities|[2]: SUCCESS", - "Compound Configuration|Child Shape Entities|[3]: SUCCESS", - "Compound Configuration|Child Shape Entities|[4]: SUCCESS", - "Compound Configuration|Child Shape Entities|[5]: SUCCESS", - "Instance Spawner Configuration|Shape Entity Id: SUCCESS", - "TestLayerSpawner_AllShapesPlant: result=SUCCESS", - ] - - hydra.launch_and_validate_results( - request, - test_directory, - editor, - "LayerSpawner_InstancesPlantInAllSupportedShapes.py", - expected_lines, - cfg_args=[level] - ) - - @pytest.mark.test_case_id("C4765973") - @pytest.mark.SUITE_periodic - @pytest.mark.dynveg_misc - @pytest.mark.xfail(reason="https://github.com/o3de/o3de/issues/2303") - def test_LayerSpawner_FilterStageToggle(self, request, editor, level, workspace, launcher_platform): - - expected_lines = [ - "LayerSpawner_FilterStageToggle: test started", - "LayerSpawner_FilterStageToggle: Preprocess filter stage vegetation instance count is as expected: True", - "LayerSpawner_FilterStageToggle: Postprocess filter vegetation instance stage count is as expected: True", - "LayerSpawner_FilterStageToggle: result=SUCCESS", - ] - - hydra.launch_and_validate_results( - request, - test_directory, - editor, - "LayerSpawner_FilterStageToggle.py", - expected_lines=expected_lines, - cfg_args=[level] - ) - - @pytest.mark.test_case_id("C30000751") - @pytest.mark.SUITE_sandbox - @pytest.mark.dynveg_misc - @pytest.mark.skip # https://github.com/o3de/o3de/issues/2038 - def test_LayerSpawner_InstancesRefreshUsingCorrectViewportCamera(self, request, editor, level, launcher_platform): - - expected_lines = [ - "LayerSpawner_InstanceCameraRefresh: test started", - "LayerSpawner_InstanceCameraRefresh: test finished", - "LayerSpawner_InstanceCameraRefresh: result=SUCCESS" - ] - - hydra.launch_and_validate_results( - request, - test_directory, - editor, - "LayerSpawner_InstancesRefreshUsingCorrectViewportCamera.py", - expected_lines, - cfg_args=[level], - null_renderer=False - ) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_MeshBlocker.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_MeshBlocker.py deleted file mode 100755 index 197ab6e585..0000000000 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_MeshBlocker.py +++ /dev/null @@ -1,89 +0,0 @@ -""" -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 -""" - - -import logging -import os -import pytest -# Bail on the test if ly_test_tools doesn't exist. -pytest.importorskip('ly_test_tools') - -import editor_python_test_tools.hydra_test_utils as hydra -import ly_test_tools.environment.file_system as file_system - -test_directory = os.path.join(os.path.dirname(__file__), 'EditorScripts') -logger = logging.getLogger(__name__) - - -@pytest.mark.parametrize("project", ["AutomatedTesting"]) -@pytest.mark.parametrize("level", ["tmp_level"]) -@pytest.mark.usefixtures("automatic_process_killer") -@pytest.mark.parametrize("launcher_platform", ['windows_editor']) -class TestMeshBlocker(object): - @pytest.fixture(autouse=True) - def setup_teardown(self, request, workspace, editor, project, level): - pass - - def teardown(): - # delete temp level - file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", level)], True, True) - - # Setup - add the teardown finalizer - request.addfinalizer(teardown) - # Make sure the temp level doesn't already exist - file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", level)], True, True) - - """ - C3980834: A simple Vegetation Blocker Mesh can be created - """ - @pytest.mark.test_case_id("C3980834") - @pytest.mark.SUITE_periodic - @pytest.mark.dynveg_area - @pytest.mark.xfail # LYN-3273 - def test_MeshBlocker_InstancesBlockedByMesh(self, request, editor, level, launcher_platform): - expected_lines = [ - "'Instance Spawner' created", - "'Surface Entity' created", - "'Blocker Entity' created", - "instance count validation: True (found=160, expected=160)", - "MeshBlocker_InstancesBlockedByMesh: result=SUCCESS" - ] - - hydra.launch_and_validate_results( - request, - test_directory, - editor, - "MeshBlocker_InstancesBlockedByMesh.py", - expected_lines, - cfg_args=[level] - ) - - """ - C4766030: Mesh Height Percent Min/Max values can be set to fine tune the blocked area - """ - @pytest.mark.test_case_id("C4766030") - @pytest.mark.SUITE_periodic - @pytest.mark.dynveg_area - @pytest.mark.xfail # LYN-3273 - def test_MeshBlocker_InstancesBlockedByMeshHeightTuning(self, request, editor, level, launcher_platform): - expected_lines = [ - "'Instance Spawner' created", - "'Surface Entity' created", - "'Blocker Entity' created", - "Blocker Entity Configuration|Mesh Height Percent Max: SUCCESS", - "instance count validation: True (found=127, expected=127)", - "MeshBlocker_InstancesBlockedByMeshHeightTuning: result=SUCCESS", - ] - - hydra.launch_and_validate_results( - request, - test_directory, - editor, - "MeshBlocker_InstancesBlockedByMeshHeightTuning.py", - expected_lines, - cfg_args=[level] - ) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_MeshSurfaceTagEmitter.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_MeshSurfaceTagEmitter.py deleted file mode 100755 index fd3a2f6514..0000000000 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_MeshSurfaceTagEmitter.py +++ /dev/null @@ -1,82 +0,0 @@ -""" -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 -""" - -import os -import pytest -import logging - -# Bail on the test if ly_test_tools doesn't exist. -pytest.importorskip("ly_test_tools") -import ly_test_tools.environment.file_system as file_system -import editor_python_test_tools.hydra_test_utils as hydra - -logger = logging.getLogger(__name__) -test_directory = os.path.join(os.path.dirname(__file__), "EditorScripts") - - -@pytest.mark.parametrize('project', ['AutomatedTesting']) -@pytest.mark.parametrize('level', ['tmp_level']) -@pytest.mark.usefixtures("automatic_process_killer") -@pytest.mark.parametrize("launcher_platform", ['windows_editor']) -class TestMeshSurfaceTagEmitter(object): - - @pytest.fixture(autouse=True) - def setup_teardown(self, request, workspace, project, level): - def teardown(): - file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", level)], True, True) - request.addfinalizer(teardown) - - file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", level)], True, True) - - @pytest.mark.test_case_id("C2908172") - @pytest.mark.SUITE_periodic - @pytest.mark.dynveg_surfacetagemitter - def test_MeshSurfaceTagEmitter_DependentOnMeshComponent(self, request, editor, level, launcher_platform): - - expected_lines = [ - "Entity has a Mesh Surface Tag Emitter component", - "New Entity Created", - "Mesh Surface Tag Emitter is Disabled", - "Entity has a Mesh component", - "Mesh Surface Tag Emitter is Enabled", - "MeshSurfaceTagEmitter_DependentOnMeshComponent: result=SUCCESS" - ] - - unexpected_lines = [ - "Mesh Surface Tag Emitter is Enabled. But It should be disabled before adding Mesh", - "Mesh Surface Tag Emitter is Disabled. But It should be enabled after adding Mesh", - ] - - hydra.launch_and_validate_results( - request, - test_directory, - editor, - "MeshSurfaceTagEmitter_DependentOnMeshComponent.py", - expected_lines, - unexpected_lines, - cfg_args=[level] - ) - - @pytest.mark.test_case_id("C2908174") - @pytest.mark.SUITE_periodic - @pytest.mark.dynveg_surfacetagemitter - def test_MeshSurfaceTagEmitter_SurfaceTagsAddRemoveSuccessfully(self, request, editor, level, launcher_platform): - - expected_lines = [ - "Added SurfaceTag: container count is 1", - "Removed SurfaceTag: container count is 0", - "MeshSurfaceTagEmitter_SurfaceTagsAddRemoveSucessfully: result=SUCCESS" - ] - - hydra.launch_and_validate_results( - request, - test_directory, - editor, - "MeshSurfaceTagEmitter_SurfaceTagsAddRemoveSuccessfully.py", - expected_lines, - cfg_args=[level] - ) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_PhysXColliderSurfaceTagEmitter.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_PhysXColliderSurfaceTagEmitter.py deleted file mode 100755 index 03b7a968f8..0000000000 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_PhysXColliderSurfaceTagEmitter.py +++ /dev/null @@ -1,53 +0,0 @@ -""" -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 -""" - -import os -import pytest -import logging - -# Bail on the test if ly_test_tools doesn't exist. -pytest.importorskip("ly_test_tools") -import ly_test_tools.environment.file_system as file_system -import editor_python_test_tools.hydra_test_utils as hydra - -logger = logging.getLogger(__name__) -test_directory = os.path.join(os.path.dirname(__file__), "EditorScripts") - - -@pytest.mark.parametrize('project', ['AutomatedTesting']) -@pytest.mark.parametrize('level', ['tmp_level']) -@pytest.mark.usefixtures("automatic_process_killer") -@pytest.mark.parametrize("launcher_platform", ['windows_editor']) -class TestPhysXColliderSurfaceTagEmitter(object): - - @pytest.fixture(autouse=True) - def setup_teardown(self, request, workspace, project, level): - def teardown(): - file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", level)], True, True) - request.addfinalizer(teardown) - - file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", level)], True, True) - - @pytest.mark.test_case_id("C29053640") - @pytest.mark.SUITE_periodic - @pytest.mark.dynveg_surfacetagemitter - def test_PhysXColliderSurfaceTagEmitter_E2E_Editor(self, request, editor, level, launcher_platform): - - expected_lines = [ - "PhysXColliderSurfaceTagEmitter_E2E_Editor: test started", - "PhysXColliderSurfaceTagEmitter_E2E_Editor: test finished", - "PhysXColliderSurfaceTagEmitter_E2E_Editor: result=SUCCESS" - ] - - hydra.launch_and_validate_results( - request, - test_directory, - editor, - "PhysXColliderSurfaceTagEmitter_E2E_Editor.py", - expected_lines=expected_lines, - cfg_args=[level] - ) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_PositionModifier.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_PositionModifier.py deleted file mode 100755 index ebbd58557e..0000000000 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_PositionModifier.py +++ /dev/null @@ -1,79 +0,0 @@ -""" -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 -""" - -import os -import pytest -import logging - -# Bail on the test if ly_test_tools doesn't exist. -pytest.importorskip('ly_test_tools') -import ly_test_tools.environment.file_system as file_system -import editor_python_test_tools.hydra_test_utils as hydra - -logger = logging.getLogger(__name__) -test_directory = os.path.join(os.path.dirname(__file__), 'EditorScripts') - - -@pytest.mark.parametrize("project", ["AutomatedTesting"]) -@pytest.mark.parametrize("level", ["tmp_level"]) -@pytest.mark.usefixtures("automatic_process_killer") -@pytest.mark.parametrize("launcher_platform", ['windows_editor']) -class TestPositionModifier(object): - - @pytest.fixture(autouse=True) - def setup_teardown(self, request, workspace, project, level): - def teardown(): - file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", level)], True, True) - request.addfinalizer(teardown) - - file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", level)], True, True) - - @pytest.mark.test_case_id("C4874099", "C4814461") - @pytest.mark.SUITE_periodic - @pytest.mark.dynveg_modifier - def test_PositionModifier_ComponentAndOverrides_InstancesPlantAtSpecifiedOffsets(self, request, editor, level, - launcher_platform): - - expected_lines = [ - "'Instance Spawner' created", - "Vegetation Position Modifier component was added to entity", - "'Planting Surface' created", - "Entity has a Constant Gradient component", - "PositionModifierComponentAndOverrides_InstanceOffset: result=SUCCESS", - ] - - hydra.launch_and_validate_results( - request, - test_directory, - editor, - "PositionModifier_ComponentAndOverrides_InstancesPlantAtSpecifiedOffsets.py", - expected_lines, - cfg_args=[level] - ) - - @pytest.mark.test_case_id("C4874100") - @pytest.mark.SUITE_periodic - @pytest.mark.dynveg_modifier - @pytest.mark.xfail(reason="https://github.com/o3de/o3de/issues/2303") - def test_PositionModifier_AutoSnapToSurfaceWorks(self, request, editor, level, launcher_platform): - - expected_lines = [ - "'Instance Spawner' created", - "'Planting Surface' created", - "Instance Spawner Configuration|Position X|Range Min: SUCCESS", - "Instance Spawner Configuration|Position X|Range Max: SUCCESS", - "PositionModifier_AutoSnapToSurface: result=SUCCESS" - ] - - hydra.launch_and_validate_results( - request, - test_directory, - editor, - "PositionModifier_AutoSnapToSurfaceWorks.py", - expected_lines, - cfg_args=[level] - ) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_RotationModifier.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_RotationModifier.py deleted file mode 100755 index 1dea332f7d..0000000000 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_RotationModifier.py +++ /dev/null @@ -1,97 +0,0 @@ -""" -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 -""" - -import logging -import os -import pytest - -# Bail on the test if ly_test_tools doesn't exist. -pytest.importorskip("ly_test_tools") - -import editor_python_test_tools.hydra_test_utils as hydra -import ly_test_tools.environment.file_system as file_system - -logger = logging.getLogger(__name__) - -test_directory = os.path.join(os.path.dirname(__file__), "EditorScripts") - - -@pytest.mark.parametrize("project", ["AutomatedTesting"]) -@pytest.mark.parametrize("level", ["tmp_level"]) -@pytest.mark.usefixtures("automatic_process_killer") -@pytest.mark.parametrize("launcher_platform", ['windows_editor']) -class TestRotationModifier(object): - @pytest.fixture(autouse=True) - def setup_teardown(self, request, workspace, project, level): - def teardown(): - # delete temp level - file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", level)], True, True) - - # Setup - add the teardown finalizer - request.addfinalizer(teardown) - - file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", level)], True, True) - - @pytest.mark.test_case_id("C4896922") - @pytest.mark.SUITE_periodic - @pytest.mark.dynveg_modifier - def test_RotationModifier_InstancesRotateWithinRange(self, request, editor, level, launcher_platform) -> None: - """ - Launches editor and run test script to test that rotation modifier works for all axis. - Manual test case: C4896922 - """ - - expected_lines = [ - "'Spawner Entity' created", - "'Surface Entity' created", - "'Gradient Entity' created", - "Entity has a Vegetation Asset List component", - "Entity has a Vegetation Layer Spawner component", - "Entity has a Vegetation Rotation Modifier component", - "Entity has a Box Shape component", - "Entity has a Constant Gradient component", - "RotationModifier_InstancesRotateWithinRange: result=SUCCESS" - ] - - hydra.launch_and_validate_results( - request, - test_directory, - editor, - "RotationModifier_InstancesRotateWithinRange.py", - expected_lines, - cfg_args=[level] - ) - - @pytest.mark.test_case_id("C4814460") - @pytest.mark.SUITE_periodic - @pytest.mark.dynveg_modifier - def test_RotationModifierOverrides_InstancesRotateWithinRange(self, request, editor, level, launcher_platform) -> None: - - expected_lines = [ - "'Spawner Entity' created", - "'Surface Entity' created", - "'Gradient Entity' created", - "Entity has a Vegetation Layer Spawner component", - "Entity has a Vegetation Asset List component", - "Spawner Entity Box Shape|Box Configuration|Dimensions: SUCCESS", - "Entity has a Vegetation Rotation Modifier component", - "Spawner Entity Configuration|Embedded Assets|[0]|Rotation Modifier|Override Enabled: SUCCESS", - "Spawner Entity Configuration|Allow Per-Item Overrides: SUCCESS", - "Entity has a Constant Gradient component", - "Entity has a Box Shape component", - "Spawner Entity Configuration|Rotation Z|Gradient|Gradient Entity Id: SUCCESS", - "RotationModifierOverrides_InstancesRotateWithinRange: result=SUCCESS" - ] - - hydra.launch_and_validate_results( - request, - test_directory, - editor, - "RotationModifierOverrides_InstancesRotateWithinRange.py", - expected_lines, - cfg_args=[level] - ) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_ScaleModifier.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_ScaleModifier.py deleted file mode 100755 index 62f6c0bbad..0000000000 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_ScaleModifier.py +++ /dev/null @@ -1,91 +0,0 @@ -""" -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 -""" - -""" -C4814462: Vegetation instances have random scale between 0.1 and 1.0 applied. -""" - -import os -import pytest - -# Bail on the test if ly_test_tools doesn't exist. -pytest.importorskip("ly_test_tools") - -import editor_python_test_tools.hydra_test_utils as hydra -import ly_test_tools.environment.file_system as file_system - -test_directory = os.path.join(os.path.dirname(__file__), "EditorScripts") - - -@pytest.mark.parametrize("project", ["AutomatedTesting"]) -@pytest.mark.parametrize("level", ["tmp_level"]) -@pytest.mark.usefixtures("automatic_process_killer") -@pytest.mark.parametrize("launcher_platform", ['windows_editor']) -class TestScaleOverrideWorksSuccessfully(object): - - @pytest.fixture(autouse=True) - def setup_teardown(self, request, workspace, project, level): - def teardown(): - file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", level)], True, True) - request.addfinalizer(teardown) - - file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", level)], True, True) - - @pytest.mark.test_case_id("C4814462") - @pytest.mark.SUITE_periodic - @pytest.mark.dynveg_modifier - def test_ScaleModifierOverrides_InstancesProperlyScale(self, request, editor, level, launcher_platform): - - expected_lines = [ - "'Spawner Entity' created", - "'Surface Entity' created", - "Entity has a Vegetation Scale Modifier component", - "'Gradient Entity' created", - "Scale Min and Scale Max are set to 0.1 and 1.0 in Vegetation Asset List", - "Entity has a Random Noise Gradient component", - "Entity has a Gradient Transform Modifier component", - "Entity has a Box Shape component", - "Spawner Entity Configuration|Gradient|Gradient Entity Id: SUCCESS", - "ScaleModifierOverrides_InstancesProperlyScale: result=SUCCESS" - ] - - unexpected_lines = ["Scale Min and Scale Max are not set to 0.1 and 1.0 in Vegetation Asset List"] - - hydra.launch_and_validate_results( - request, - test_directory, - editor, - "ScaleModifierOverrides_InstancesProperlyScale.py", - expected_lines, - unexpected_lines, - cfg_args=[level] - ) - - @pytest.mark.test_case_id("C4896937") - @pytest.mark.SUITE_periodic - @pytest.mark.dynveg_modifier - def test_ScaleModifier_InstancesProperlyScale(self, request, editor, level, launcher_platform): - - expected_lines = [ - "'Spawner Entity' created", - "Entity has a Vegetation Scale Modifier component", - "'Surface Entity' created", - "'Gradient Entity' created", - "Spawner Entity Configuration|Gradient|Gradient Entity Id: SUCCESS", - "Spawner Entity Configuration|Range Min: SUCCESS", - "Spawner Entity Configuration|Range Max: SUCCESS", - "ScaleModifier_InstancesProperlyScale: result=SUCCESS" - ] - - hydra.launch_and_validate_results( - request, - test_directory, - editor, - "ScaleModifier_InstancesProperlyScale.py", - expected_lines, - cfg_args=[level] - ) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_ShapeIntersectionFilter.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_ShapeIntersectionFilter.py deleted file mode 100755 index d2ed0318ca..0000000000 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_ShapeIntersectionFilter.py +++ /dev/null @@ -1,56 +0,0 @@ -""" -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 -""" - -import os -import pytest -import logging - -# Bail on the test if ly_test_tools doesn't exist. -pytest.importorskip('ly_test_tools') -import ly_test_tools.environment.file_system as file_system -import editor_python_test_tools.hydra_test_utils as hydra - -logger = logging.getLogger(__name__) -test_directory = os.path.join(os.path.dirname(__file__), 'EditorScripts') - - -@pytest.mark.parametrize("project", ["AutomatedTesting"]) -@pytest.mark.parametrize("level", ["tmp_level"]) -@pytest.mark.usefixtures("automatic_process_killer") -@pytest.mark.parametrize("launcher_platform", ['windows_editor']) -class TestShapeIntersectionFilter(object): - - @pytest.fixture(autouse=True) - def setup_teardown(self, request, workspace, project, level): - def teardown(): - file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", level)], True, True) - request.addfinalizer(teardown) - - file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", level)], True, True) - - @pytest.mark.test_case_id("C4874094") - @pytest.mark.SUITE_periodic - @pytest.mark.dynveg_filter - def test_ShapeIntersectionFilter_InstancesPlantInAssignedShape(self, request, editor, level, launcher_platform): - - expected_lines = [ - "'Instance Spawner' created", - "'Planting Surface' created", - "instance count validation: True (found=49, expected=49)", - "instance count validation: True (found=121, expected=121)", - "instance count validation: True (found=400, expected=400)", - "ShapeIntersectionFilter_InstancePlanting: result=SUCCESS" - ] - - hydra.launch_and_validate_results( - request, - test_directory, - editor, - "ShapeIntersectionFilter_InstancesPlantInAssignedShape.py", - expected_lines, - cfg_args=[level] - ) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_SlopeAlignmentModifier.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_SlopeAlignmentModifier.py deleted file mode 100755 index 3a01521d8e..0000000000 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_SlopeAlignmentModifier.py +++ /dev/null @@ -1,82 +0,0 @@ -""" -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 -""" - -""" -C4896941 - Surface Alignment functions as expected -C4814459 - Surface Alignment overrides function as expected -""" - -import os -import pytest - -# Bail on the test if ly_test_tools doesn't exist. -pytest.importorskip("ly_test_tools") -import editor_python_test_tools.hydra_test_utils as hydra -import ly_test_tools.environment.file_system as file_system - -test_directory = os.path.join(os.path.dirname(__file__), "EditorScripts") - - -@pytest.mark.parametrize("project", ["AutomatedTesting"]) -@pytest.mark.parametrize("level", ["tmp_level"]) -@pytest.mark.usefixtures("automatic_process_killer") -@pytest.mark.parametrize("launcher_platform", ['windows_editor']) -class TestSlopeAlignmentModifier(object): - - @pytest.fixture(autouse=True) - def setup_teardown(self, request, workspace, project, level): - def teardown(): - file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", level)], True, True) - request.addfinalizer(teardown) - - file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", level)], True, True) - - @pytest.mark.test_case_id("C4896941") - @pytest.mark.SUITE_periodic - @pytest.mark.dynveg_modifier - @pytest.mark.xfail(reason="https://github.com/o3de/o3de/issues/2303") - def test_SlopeAlignmentModifier_InstanceSurfaceAlignment(self, request, editor, level, launcher_platform): - - expected_lines = [ - "Vegetation Slope Alignment Modifier component was added to entity", - "Instance Spawner Configuration|Alignment Coefficient Min: SUCCESS", - "Constant Gradient component was added to entity", - "Instance Spawner Configuration|Gradient|Gradient Entity Id: SUCCESS", - "SlopeAlignmentModifier: result=SUCCESS" - ] - - hydra.launch_and_validate_results( - request, - test_directory, - editor, - "SlopeAlignmentModifier_InstanceSurfaceAlignment.py", - expected_lines, - cfg_args=[level] - ) - - @pytest.mark.test_case_id("C4814459") - @pytest.mark.SUITE_periodic - @pytest.mark.dynveg_modifier - @pytest.mark.xfail(reason="https://github.com/o3de/o3de/issues/2303") - def test_SlopeAlignmentModifierOverrides_InstanceSurfaceAlignment(self, request, editor, level, launcher_platform): - - expected_lines = [ - "Instance Spawner Configuration|Allow Per-Item Overrides: SUCCESS", - "Instance Spawner Configuration|Embedded Assets|[0]|Surface Slope Alignment|Override Enabled: SUCCESS", - "Instance Spawner Configuration|Embedded Assets|[0]|Surface Slope Alignment|Max: SUCCESS", - "Instance Spawner Configuration|Embedded Assets|[0]|Surface Slope Alignment|Min: SUCCESS", - "SlopeAlignmentModifierOverrides: result=SUCCESS" - ] - - hydra.launch_and_validate_results( - request, - test_directory, - editor, - "SlopeAlignmentModifierOverrides_InstanceSurfaceAlignment.py", - expected_lines, - cfg_args=[level] - ) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_SlopeFilter.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_SlopeFilter.py deleted file mode 100755 index 3065b2df61..0000000000 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_SlopeFilter.py +++ /dev/null @@ -1,96 +0,0 @@ -""" -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 -""" - -import os -import pytest -import logging - -# Bail on the test if ly_test_tools doesn't exist. -pytest.importorskip("ly_test_tools") -import ly_test_tools.environment.file_system as file_system -import editor_python_test_tools.hydra_test_utils as hydra - -logger = logging.getLogger(__name__) -test_directory = os.path.join(os.path.dirname(__file__), "EditorScripts") - - -@pytest.mark.parametrize("project", ["AutomatedTesting"]) -@pytest.mark.parametrize("level", ["tmp_level"]) -@pytest.mark.usefixtures("automatic_process_killer") -@pytest.mark.parametrize("launcher_platform", ['windows_editor']) -class TestSlopeFilter(object): - @pytest.fixture(autouse=True) - def setup_teardown(self, request, workspace, project, level): - # Cleanup our temp level - file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", level)], True, True) - - def teardown(): - # Cleanup our temp level - file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", level)], True, True) - request.addfinalizer(teardown) - - @pytest.mark.test_case_id("C4874097") - @pytest.mark.SUITE_periodic - @pytest.mark.dynveg_filter - def test_SlopeFilter_FilterStageToggle(self, request, editor, level, workspace, launcher_platform): - cfg_args = [level] - - expected_lines = [ - "SlopeFilter_FilterStageToggle: test started", - "SlopeFilter_FilterStageToggle: Vegetation plant only in the areas where the Box overlaps with the vegetation area's boundaries: True", - "SlopeFilter_FilterStageToggle: Vegetation plant only in the areas where the Cylinder overlaps with the vegetation area's boundaries: True", - "SlopeFilter_FilterStageToggle: Vegetation instances count equal to expected value for PREPROCESS filter stage: True", - "SlopeFilter_FilterStageToggle: Vegetation instances count equal to expected value for POSTPROCESS filter stage: True", - "SlopeFilter_FilterStageToggle: result=SUCCESS", - ] - - unexpected_lines = [ - "SlopeFilter_FilterStageToggle: Vegetation plant only in the areas where the Box overlaps with the vegetation area's boundaries: False", - "SlopeFilter_FilterStageToggle: Vegetation plant only in the areas where the Cylinder overlaps with the vegetation area's boundaries: False", - "SlopeFilter_FilterStageToggle: Vegetation instances count equal to expected value for PREPROCESS filter stage: False", - "SlopeFilter_FilterStageToggle: Vegetation instances count equal to expected value for POSTPROCESS filter stage: False", - ] - - hydra.launch_and_validate_results( - request, - test_directory, - editor, - "SlopeFilter_FilterStageToggle.py", - expected_lines=expected_lines, - unexpected_lines=unexpected_lines, - cfg_args=cfg_args - ) - - @pytest.mark.test_case_id("C4814464", "C4874096") - @pytest.mark.SUITE_periodic - @pytest.mark.dynveg_filter - @pytest.mark.xfail(reason="https://github.com/o3de/o3de/issues/2303") - def test_SlopeFilter_ComponentAndOverrides_InstancesPlantOnValidSlopes(self, request, editor, level, - launcher_platform): - - expected_lines = [ - "'Instance Spawner' created", - "'Planting Surface' created", - "'Sloped Planting Surface' created", - "instance count validation: True (found=1720, expected=1720)", - "Instance Spawner Configuration|Slope Min: SUCCESS", - "Instance Spawner Configuration|Slope Max: SUCCESS", - "instance count validation: True (found=48, expected=48)", - "Instance Spawner Configuration|Embedded Assets|[0]|Slope Filter|Min: SUCCESS", - "Instance Spawner Configuration|Embedded Assets|[0]|Slope Filter|Max: SUCCESS", - "instance count validation: True (found=12, expected=12)", - "SlopeFilter_InstancesPlantOnValidSlope: result=SUCCESS" - ] - - hydra.launch_and_validate_results( - request, - test_directory, - editor, - "SlopeFilter_ComponentAndOverrides_InstancesPlantOnValidSlope.py", - expected_lines, - cfg_args=[level] - ) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_SurfaceMaskFilter.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_SurfaceMaskFilter.py deleted file mode 100755 index 7101a8e286..0000000000 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_SurfaceMaskFilter.py +++ /dev/null @@ -1,150 +0,0 @@ -""" -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 -""" - -import os -import pytest - -# Bail on the test if ly_test_tools doesn't exist. -pytest.importorskip("ly_test_tools") - -import editor_python_test_tools.hydra_test_utils as hydra -import ly_test_tools.environment.file_system as file_system - -test_directory = os.path.join(os.path.dirname(__file__), "EditorScripts") - - -@pytest.mark.parametrize("project", ["AutomatedTesting"]) -@pytest.mark.parametrize("level", ["tmp_level"]) -@pytest.mark.usefixtures("automatic_process_killer") -@pytest.mark.parametrize("launcher_platform", ['windows_editor']) -class TestSurfaceMaskFilter(object): - @pytest.fixture(autouse=True) - def setup_teardown(self, request, workspace, project, level): - def teardown(): - # delete temp level - file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", level)], True, True) - - # Setup - add the teardown finalizer - request.addfinalizer(teardown) - - # Make sure the temp level doesn't already exist - file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", level)], True, True) - - # Simple validation test to ensure that SurfaceTag can be created, set to a value, and compared to another SurfaceTag. - @pytest.mark.SUITE_periodic - @pytest.mark.dynveg_filter - def test_SurfaceMaskFilter_BasicSurfaceTagCreation(self, request, level, editor, launcher_platform): - - expected_lines = [ - "SurfaceTag test started", - "SurfaceTag equal tag comparison is True expected True", - "SurfaceTag not equal tag comparison is False expected False", - "SurfaceTag test finished", - "TestSurfaceMaskFilter_BasicSurfaceTagCreation: result=SUCCESS" - ] - - hydra.launch_and_validate_results( - request, - test_directory, - editor, - 'SurfaceMaskFilter_BasicSurfaceTagCreation.py', - expected_lines=expected_lines, - cfg_args=[level] - ) - - @pytest.mark.test_case_id("C2561342") - @pytest.mark.SUITE_periodic - @pytest.mark.dynveg_filter - def test_SurfaceMaskFilter_ExclusiveSurfaceTags_Function(self, request, editor, level, launcher_platform): - - expected_lines = [ - "'Instance Spawner' created", - "Instance Spawner Box Shape|Box Configuration|Dimensions: SUCCESS", - "Instance Spawner Configuration|Embedded Assets|[0]: SUCCESS", - "'Surface Entity 1' created", - "Surface Entity 1 Box Shape|Box Configuration|Dimensions: SUCCESS", - "Surface Entity 1 Configuration|Generated Tags: SUCCESS", - "'Surface Entity 2' created", - "Surface Entity 2 Box Shape|Box Configuration|Dimensions: SUCCESS", - "Surface Entity 2 Configuration|Generated Tags: SUCCESS", - "SurfaceMaskFilter_ExclusionList: Expected 39 instances - Found 39 instances", - "Instance Spawner Configuration|Exclusion|Weight Max: SUCCESS", - "SurfaceMaskFilter_ExclusionList: Expected 169 instances - Found 169 instances", - "SurfaceMaskFilter_ExclusionList: result=SUCCESS" - ] - - unexpected_lines = ["Failed to add an Exclusive surface mask filter of terrainHole"] - - hydra.launch_and_validate_results( - request, - test_directory, - editor, - "SurfaceMaskFilter_ExclusionList.py", - expected_lines, - unexpected_lines, - cfg_args=[level] - ) - - @pytest.mark.test_case_id("C2561341") - @pytest.mark.SUITE_periodic - @pytest.mark.dynveg_filter - def test_SurfaceMaskFilter_InclusiveSurfaceTags_Function(self, request, editor, level, launcher_platform): - - expected_lines = [ - "'Instance Spawner' created", - "Instance Spawner Box Shape|Box Configuration|Dimensions: SUCCESS", - "Instance Spawner Configuration|Embedded Assets|[0]: SUCCESS", - "'Surface Entity 1' created", - "Surface Entity 1 Box Shape|Box Configuration|Dimensions: SUCCESS", - "Surface Entity 1 Configuration|Generated Tags: SUCCESS", - "'Surface Entity 2' created", - "Surface Entity 2 Box Shape|Box Configuration|Dimensions: SUCCESS", - "Surface Entity 2 Configuration|Generated Tags: SUCCESS", - "SurfaceMaskFilter_InclusionList: Expected 130 instances - Found 130 instances", - "Instance Spawner Configuration|Inclusion|Weight Max: SUCCESS", - "SurfaceMaskFilter_InclusionList: Expected 0 instances - Found 0 instances", - "SurfaceMaskFilter_InclusionList: result=SUCCESS" - ] - - unexpected_lines = ["Failed to add an Inclusive surface mask filter of terrainHole"] - - hydra.launch_and_validate_results( - request, - test_directory, - editor, - "SurfaceMaskFilter_InclusionList.py", - expected_lines, - unexpected_lines, - cfg_args=[level] - ) - - @pytest.mark.test_case_id("C3711666") - @pytest.mark.SUITE_periodic - @pytest.mark.dynveg_filter - def test_SurfaceMaskFilterOverrides_MultipleDescriptorOverridesPlantAsExpected(self, request, editor, level, - launcher_platform): - - expected_lines = [ - "'Instance Spawner' created", - "'Surface Entity A' created", - "'Surface Entity B' created", - "'Surface Entity C' created", - "instance count validation: True (found=725, expected=725)", - "instance count validation: True (found=400, expected=400)", - "instance count validation: True (found=225, expected=225)", - "instance count validation: True (found=100, expected=100)", - "SurfaceMaskFilter_MultipleDescriptorOverrides: result=SUCCESS" - ] - - hydra.launch_and_validate_results( - request, - test_directory, - editor, - "SurfaceMaskFilterOverrides_MultipleDescriptorOverridesPlantAsExpected.py", - expected_lines, - cfg_args=[level] - ) diff --git a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_SystemSettings.py b/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_SystemSettings.py deleted file mode 100755 index 0b00a1fa83..0000000000 --- a/AutomatedTesting/Gem/PythonTests/largeworlds/dyn_veg/test_SystemSettings.py +++ /dev/null @@ -1,87 +0,0 @@ -""" -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 -""" - -import os -import pytest -import logging - -# Bail on the test if ly_test_tools doesn't exist. -pytest.importorskip("ly_test_tools") -import ly_test_tools.environment.file_system as file_system -import editor_python_test_tools.hydra_test_utils as hydra - -logger = logging.getLogger(__name__) -test_directory = os.path.join(os.path.dirname(__file__), "EditorScripts") - - -@pytest.mark.parametrize("project", ["AutomatedTesting"]) -@pytest.mark.parametrize("level", ["tmp_level"]) -@pytest.mark.usefixtures("automatic_process_killer") -@pytest.mark.parametrize("launcher_platform", ['windows_editor']) -class TestSystemSettings(object): - @pytest.fixture(autouse=True) - def setup_teardown(self, request, workspace, project, level): - def teardown(): - file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", level)], True, True) - - request.addfinalizer(teardown) - - file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", level)], True, True) - - @pytest.mark.test_case_id("C2646869") - @pytest.mark.SUITE_periodic - @pytest.mark.dynveg_misc - def test_SystemSettings_SectorPointDensity(self, request, editor, level, launcher_platform): - - expected_lines = [ - "SystemSettings_SectorPointDensity: test started", - "SystemSettings_SectorPointDensity: Vegetation instances count equal to expected value before changing sector point density: True", - "SystemSettings_SectorPointDensity: Vegetation instances count equal to expected value after changing sector point density: True", - "SystemSettings_SectorPointDensity: result=SUCCESS", - ] - - unexpected_lines = [ - "SystemSettings_SectorPointDensity: Vegetation instances count equal to expected value before changing sector point density: False", - "SystemSettings_SectorPointDensity: Vegetation instances count equal to expected value after changing sector point density: False", - ] - - hydra.launch_and_validate_results( - request, - test_directory, - editor, - "SystemSettings_SectorPointDensity.py", - expected_lines, - unexpected_lines=unexpected_lines, - cfg_args=[level] - ) - - @pytest.mark.test_case_id("C2646870") - @pytest.mark.SUITE_periodic - @pytest.mark.dynveg_misc - def test_SystemSettings_SectorSize(self, request, editor, level, launcher_platform): - - expected_lines = [ - "SystemSettings_SectorSize: test started", - "SystemSettings_SectorSize: Vegetation instances count equal to expected value before changing sector size: True", - "SystemSettings_SectorSize: Vegetation instances count equal to expected value after changing sector size: True", - "SystemSettings_SectorSize: result=SUCCESS", - ] - - unexpected_lines = [ - "SystemSettings_SectorSize: Vegetation instances count equal to expected value before changing sector size: False", - "SystemSettings_SectorSize: Vegetation instances count equal to expected value after changing sector size: False", - ] - - hydra.launch_and_validate_results( - request, - test_directory, - editor, - "SystemSettings_SectorSize.py", - expected_lines, - unexpected_lines=unexpected_lines, - cfg_args=[level] - ) diff --git a/CMakeLists.txt b/CMakeLists.txt index 43b0dd240e..e659270f84 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -29,11 +29,11 @@ include(cmake/PAL.cmake) include(cmake/PALTools.cmake) include(cmake/RuntimeDependencies.cmake) include(cmake/Configurations.cmake) # Requires to be after PAL so we get platform variable definitions -include(cmake/Install.cmake) include(cmake/Dependencies.cmake) include(cmake/Deployment.cmake) include(cmake/3rdParty.cmake) include(cmake/LYPython.cmake) +include(cmake/Install.cmake) include(cmake/LYWrappers.cmake) include(cmake/Gems.cmake) include(cmake/UnitTest.cmake) diff --git a/Code/Editor/Core/QtEditorApplication_linux.cpp b/Code/Editor/Core/QtEditorApplication_linux.cpp index fa39609308..175bef0238 100644 --- a/Code/Editor/Core/QtEditorApplication_linux.cpp +++ b/Code/Editor/Core/QtEditorApplication_linux.cpp @@ -8,11 +8,21 @@ #include "QtEditorApplication.h" +#ifdef PAL_TRAIT_LINUX_WINDOW_MANAGER_XCB +#include +#endif + namespace Editor { - bool EditorQtApplication::nativeEventFilter(const QByteArray& , void* , long* ) + bool EditorQtApplication::nativeEventFilter([[maybe_unused]] const QByteArray& eventType, void* message, long*) { - // TODO_KDAB_LINUX + if (GetIEditor()->IsInGameMode()) + { +#ifdef PAL_TRAIT_LINUX_WINDOW_MANAGER_XCB + AzFramework::LinuxXcbEventHandlerBus::Broadcast(&AzFramework::LinuxXcbEventHandler::HandleXcbEvent, static_cast(message)); +#endif + return true; + } return false; } } diff --git a/Code/Editor/Platform/Mac/EditorEntitlements.plist b/Code/Editor/Platform/Mac/EditorEntitlements.plist new file mode 100644 index 0000000000..cefa2bf93b --- /dev/null +++ b/Code/Editor/Platform/Mac/EditorEntitlements.plist @@ -0,0 +1,10 @@ + + + + + com.apple.security.cs.allow-dyld-environment-variables + + com.apple.security.cs.disable-library-validation + + + diff --git a/Code/Editor/Platform/Mac/editor_mac.cmake b/Code/Editor/Platform/Mac/editor_mac.cmake index fdccfafb46..eed955f2e4 100644 --- a/Code/Editor/Platform/Mac/editor_mac.cmake +++ b/Code/Editor/Platform/Mac/editor_mac.cmake @@ -12,28 +12,5 @@ set_target_properties(Editor PROPERTIES MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_LIST_DIR}/gui_info.plist RESOURCE ${CMAKE_CURRENT_LIST_DIR}/Images.xcassets XCODE_ATTRIBUTE_ASSETCATALOG_COMPILER_APPICON_NAME EditorAppIcon + ENTITLEMENT_FILE_PATH ${CMAKE_CURRENT_LIST_DIR}/EditorEntitlements.plist ) - -# We cannot use ly_add_target here because we're already including this file from inside ly_add_target -# So we need to setup target, dependencies and install logic manually. -add_executable(EditorDummy Platform/Mac/main_dummy.cpp) -add_executable(AZ::EditorDummy ALIAS EditorDummy) - -ly_target_link_libraries(EditorDummy - PRIVATE - AZ::AzCore - AZ::AzFramework) - -ly_add_dependencies(Editor EditorDummy) - -# Store the aliased target into a DIRECTORY property -set_property(DIRECTORY APPEND PROPERTY LY_DIRECTORY_TARGETS AZ::EditorDummy) - -# Store the directory path in a GLOBAL property so that it can be accessed -# in the layout install logic. Skip if the directory has already been added -get_property(ly_all_target_directories GLOBAL PROPERTY LY_ALL_TARGET_DIRECTORIES) -if(NOT CMAKE_CURRENT_SOURCE_DIR IN_LIST ly_all_target_directories) - set_property(GLOBAL APPEND PROPERTY LY_ALL_TARGET_DIRECTORIES ${CMAKE_CURRENT_SOURCE_DIR}) -endif() - -ly_install_add_install_path_setreg(Editor) \ No newline at end of file diff --git a/Code/Editor/Platform/Mac/gui_info.plist b/Code/Editor/Platform/Mac/gui_info.plist index cc87cbbb47..5b5f94e977 100644 --- a/Code/Editor/Platform/Mac/gui_info.plist +++ b/Code/Editor/Platform/Mac/gui_info.plist @@ -3,7 +3,7 @@ CFBundleExecutable - EditorDummy + Editor CFBundleIdentifier org.O3DE.Editor CFBundlePackageType diff --git a/Code/Editor/Platform/Mac/main_dummy.cpp b/Code/Editor/Platform/Mac/main_dummy.cpp deleted file mode 100644 index 348a32ab47..0000000000 --- a/Code/Editor/Platform/Mac/main_dummy.cpp +++ /dev/null @@ -1,75 +0,0 @@ -/* - * 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 -#include -#include -#include -#include - -#include - -int main(int argc, char* argv[]) -{ - // Create a ComponentApplication to initialize the AZ::SystemAllocator and initialize the SettingsRegistry - AZ::ComponentApplication::Descriptor desc; - AZ::ComponentApplication application; - application.Create(desc); - - AZStd::vector envVars; - - const char* homePath = std::getenv("HOME"); - envVars.push_back(AZStd::string::format("HOME=%s", homePath)); - - if (auto settingsRegistry = AZ::SettingsRegistry::Get(); settingsRegistry != nullptr) - { - const char* dyldLibPathOrig = std::getenv("DYLD_LIBRARY_PATH"); - AZStd::string dyldSearchPath = AZStd::string::format("DYLD_LIBRARY_PATH=%s", dyldLibPathOrig); - if (AZ::IO::FixedMaxPath projectModulePath; - settingsRegistry->Get(projectModulePath.Native(), AZ::SettingsRegistryMergeUtils::FilePathKey_ProjectConfigurationBinPath)) - { - dyldSearchPath.append(":"); - dyldSearchPath.append(projectModulePath.c_str()); - } - - if (AZ::IO::FixedMaxPath installedBinariesFolder; - settingsRegistry->Get(installedBinariesFolder.Native(), AZ::SettingsRegistryMergeUtils::FilePathKey_InstalledBinaryFolder)) - { - if (AZ::IO::FixedMaxPath engineRootFolder; - settingsRegistry->Get(engineRootFolder.Native(), AZ::SettingsRegistryMergeUtils::FilePathKey_EngineRootFolder)) - { - installedBinariesFolder = engineRootFolder / installedBinariesFolder; - dyldSearchPath.append(":"); - dyldSearchPath.append(installedBinariesFolder.c_str()); - } - } - envVars.push_back(dyldSearchPath); - } - - AZStd::string commandArgs; - for (int i = 1; i < argc; i++) - { - commandArgs.append(argv[i]); - commandArgs.append(" "); - } - - AzFramework::ProcessLauncher::ProcessLaunchInfo processLaunchInfo; - AZ::IO::Path processPath{ AZ::IO::PathView(AZ::Utils::GetExecutableDirectory()) }; - processPath /= "Editor"; - processLaunchInfo.m_processExecutableString = AZStd::move(processPath.Native()); - processLaunchInfo.m_commandlineParameters = commandArgs; - processLaunchInfo.m_environmentVariables = &envVars; - processLaunchInfo.m_showWindow = true; - - AzFramework::ProcessLauncher::LaunchUnwatchedProcess(processLaunchInfo); - - application.Destroy(); - - return 0; -} - diff --git a/Code/Editor/Plugins/EditorCommon/CMakeLists.txt b/Code/Editor/Plugins/EditorCommon/CMakeLists.txt index 2aff57cfb1..cd9f2e79c7 100644 --- a/Code/Editor/Plugins/EditorCommon/CMakeLists.txt +++ b/Code/Editor/Plugins/EditorCommon/CMakeLists.txt @@ -51,4 +51,5 @@ ly_add_target( AZ::AzCore AZ::AzToolsFramework AZ::AzQtComponents + Legacy::EditorCore ) diff --git a/Code/Framework/AzCore/AzCore/Debug/Profiler.h b/Code/Framework/AzCore/AzCore/Debug/Profiler.h index 8af48e47f6..56103e8314 100644 --- a/Code/Framework/AzCore/AzCore/Debug/Profiler.h +++ b/Code/Framework/AzCore/AzCore/Debug/Profiler.h @@ -59,6 +59,20 @@ namespace AZStd namespace AZ::Debug { + // interface for externally defined profiler systems + class Profiler + { + public: + AZ_RTTI(Profiler, "{3E5D6329-72D1-41BA-9158-68A349D1A4D5}"); + + Profiler() = default; + virtual ~Profiler() = default; + + // support for the extra macro args (e.g. format strings) will come in a later PR + virtual void BeginRegion(const Budget* budget, const char* eventName) = 0; + virtual void EndRegion(const Budget* budget) = 0; + }; + class ProfileScope { public: diff --git a/Code/Framework/AzCore/AzCore/Debug/Profiler.inl b/Code/Framework/AzCore/AzCore/Debug/Profiler.inl index 8ca8368ce1..74c0f553c4 100644 --- a/Code/Framework/AzCore/AzCore/Debug/Profiler.inl +++ b/Code/Framework/AzCore/AzCore/Debug/Profiler.inl @@ -6,6 +6,8 @@ * */ +#include + namespace AZ::Debug { template @@ -22,9 +24,11 @@ namespace AZ::Debug PIXBeginEvent(PIX_COLOR_INDEX(budget->Crc() & 0xff), eventName, args...); #endif budget->BeginProfileRegion(); -// TODO: injecting instrumentation for other profilers -// NOTE: external profiler registration won't occur inline in a header necessarily in this manner, but the exact mechanism -// will be introduced in a future PR + + if (auto profiler = AZ::Interface::Get(); profiler) + { + profiler->BeginRegion(budget, eventName); + } #endif } @@ -39,6 +43,10 @@ namespace AZ::Debug #if defined(USE_PIX) PIXEndEvent(); #endif + if (auto profiler = AZ::Interface::Get(); profiler) + { + profiler->EndRegion(budget); + } #endif } diff --git a/Code/Framework/AzCore/Platform/Common/UnixLike/AzCore/Module/DynamicModuleHandle_UnixLike.cpp b/Code/Framework/AzCore/Platform/Common/UnixLike/AzCore/Module/DynamicModuleHandle_UnixLike.cpp index 5cf854d49f..1ca6a7c4c7 100644 --- a/Code/Framework/AzCore/Platform/Common/UnixLike/AzCore/Module/DynamicModuleHandle_UnixLike.cpp +++ b/Code/Framework/AzCore/Platform/Common/UnixLike/AzCore/Module/DynamicModuleHandle_UnixLike.cpp @@ -72,6 +72,7 @@ namespace AZ { if (auto settingsRegistry = AZ::SettingsRegistry::Get(); settingsRegistry != nullptr) { + bool fileFound = false; if (AZ::IO::FixedMaxPath projectModulePath; settingsRegistry->Get(projectModulePath.Native(), AZ::SettingsRegistryMergeUtils::FilePathKey_ProjectConfigurationBinPath)) { @@ -79,6 +80,23 @@ namespace AZ if (AZ::IO::SystemFile::Exists(projectModulePath.c_str())) { m_fileName.assign(projectModulePath.c_str(), projectModulePath.Native().size()); + fileFound = true; + } + } + if (!fileFound) + { + if (AZ::IO::FixedMaxPath installedBinariesPath; + settingsRegistry->Get(installedBinariesPath.Native(), AZ::SettingsRegistryMergeUtils::FilePathKey_InstalledBinaryFolder)) + { + if (AZ::IO::FixedMaxPath engineRootFolder; + settingsRegistry->Get(engineRootFolder.Native(), AZ::SettingsRegistryMergeUtils::FilePathKey_EngineRootFolder)) + { + installedBinariesPath = engineRootFolder / installedBinariesPath / fullFilePath; + if (AZ::IO::SystemFile::Exists(installedBinariesPath.c_str())) + { + m_fileName.assign(installedBinariesPath.c_str(), installedBinariesPath.Native().size()); + } + } } } } diff --git a/Code/Framework/AzCore/Platform/Common/UnixLike/AzCore/Utils/Utils_UnixLike.cpp b/Code/Framework/AzCore/Platform/Common/UnixLike/AzCore/Utils/Utils_UnixLike.cpp index 9aff67a00f..2e31936057 100644 --- a/Code/Framework/AzCore/Platform/Common/UnixLike/AzCore/Utils/Utils_UnixLike.cpp +++ b/Code/Framework/AzCore/Platform/Common/UnixLike/AzCore/Utils/Utils_UnixLike.cpp @@ -9,6 +9,7 @@ #include #include +#include namespace AZ { @@ -39,6 +40,14 @@ namespace AZ AZ::IO::FixedMaxPath path{homePath}; return path.Native(); } + + struct passwd* pass = getpwuid(getuid()); + if (pass) + { + AZ::IO::FixedMaxPath path{pass->pw_dir}; + return path.Native(); + } + return {}; } diff --git a/Code/Framework/AzFramework/AzFramework/Archive/ZipDirCache.cpp b/Code/Framework/AzFramework/AzFramework/Archive/ZipDirCache.cpp index 328541c4d0..d17dbd0837 100644 --- a/Code/Framework/AzFramework/AzFramework/Archive/ZipDirCache.cpp +++ b/Code/Framework/AzFramework/AzFramework/Archive/ZipDirCache.cpp @@ -774,7 +774,7 @@ namespace AZ::IO::ZipDir return ZD_ERROR_INVALID_CALL; } - if (pFileEntry->nFileDataOffset != pFileEntry->INVALID_DATA_OFFSET) + if (pFileEntry->nFileDataOffset != FileEntryBase::INVALID_DATA_OFFSET) { return ZD_ERROR_SUCCESS; // the data offset has been successfully read.. } diff --git a/Code/Framework/AzFramework/AzFramework/Archive/ZipDirCacheFactory.cpp b/Code/Framework/AzFramework/AzFramework/Archive/ZipDirCacheFactory.cpp index 6adab594ed..5c5e93d441 100644 --- a/Code/Framework/AzFramework/AzFramework/Archive/ZipDirCacheFactory.cpp +++ b/Code/Framework/AzFramework/AzFramework/Archive/ZipDirCacheFactory.cpp @@ -553,7 +553,7 @@ namespace AZ::IO::ZipDir ////////////////////////////////////////////////////////////////////////// // give the CDR File Header entry, reads the local file header to validate - // and determine where the actual file lies + // and determine where the actual file resides void CacheFactory::AddFileEntry(char* strFilePath, const ZipFile::CDRFileHeader* pFileHeader, const SExtraZipFileData& extra) { if (pFileHeader->lLocalHeaderOffset > m_CDREnd.lCDROffset) @@ -600,8 +600,7 @@ namespace AZ::IO::ZipDir if (m_encryptedHeaders != ZipFile::HEADERS_NOT_ENCRYPTED) { // use CDR instead of local header - // The pak encryption tool asserts that there is no extra data at the end of the local file header, so don't add any extra data from the CDR header. - fileEntry.nFileDataOffset = pFileHeader->lLocalHeaderOffset + sizeof(ZipFile::LocalFileHeader) + pFileHeader->nFileNameLength; + fileEntry.nFileDataOffset = pFileHeader->lLocalHeaderOffset + sizeof(ZipFile::LocalFileHeader) + pFileHeader->nFileNameLength + pFileHeader->nExtraFieldLength; } else { diff --git a/Code/Framework/AzFramework/AzFramework/Archive/ZipDirStructures.cpp b/Code/Framework/AzFramework/AzFramework/Archive/ZipDirStructures.cpp index f9aa249339..cea517decd 100644 --- a/Code/Framework/AzFramework/AzFramework/Archive/ZipDirStructures.cpp +++ b/Code/Framework/AzFramework/AzFramework/Archive/ZipDirStructures.cpp @@ -187,8 +187,7 @@ namespace AZ::IO::ZipDir::ZipDirStructuresInternal // If src/dst overlap (in place decompress), then inflate in chunks, copying src locally to ensure // pointers don't foul each other. - bool bIndependantBlocks = ((pInput + nInputLen) <= pOutput) || (pInput >= (pOutput + nOutputLen)); - if (bIndependantBlocks) + if ((pInput + nInputLen) <= pOutput || pInput >= (pOutput + nOutputLen)) { pZStream->next_in = (Bytef*)pInput; pZStream->avail_in = aznumeric_cast(nInputLen); @@ -260,8 +259,7 @@ namespace AZ::IO::ZipDir::ZipDirStructuresInternal // If src/dst overlap (in place decompress), then inflate in chunks, copying src locally to ensure // pointers don't foul each other. - bool bIndependantBlocks = ((pIn + nIn) <= stream.next_out) || (pIn >= (stream.next_out + stream.avail_out)); - if (bIndependantBlocks) + if ((pIn + nIn) <= stream.next_out || pIn >= (stream.next_out + stream.avail_out)) { stream.next_in = pIn; stream.avail_in = nIn; @@ -498,18 +496,18 @@ namespace AZ::IO::ZipDir ////////////////////////////////////////////////////////////////////////// FileEntryBase::FileEntryBase(const ZipFile::CDRFileHeader& header, const SExtraZipFileData& extra) { - this->desc = header.desc; - this->nFileHeaderOffset = header.lLocalHeaderOffset; - //this->nFileDataOffset = INVALID_DATA_OFFSET; // we don't know yet - this->nMethod = header.nMethod; - this->nNameOffset = 0; // we don't know yet - this->nLastModTime = header.nLastModTime; - this->nLastModDate = header.nLastModDate; - this->nNTFS_LastModifyTime = extra.nLastModifyTime; + desc = header.desc; + nFileHeaderOffset = header.lLocalHeaderOffset; + + nMethod = header.nMethod; + nNameOffset = 0; // we don't know yet + nLastModTime = header.nLastModTime; + nLastModDate = header.nLastModDate; + nNTFS_LastModifyTime = extra.nLastModifyTime; // make an estimation (at least this offset should be there), but we don't actually know yet - this->nFileDataOffset = header.lLocalHeaderOffset + sizeof(ZipFile::LocalFileHeader) + header.nFileNameLength; - this->nEOFOffset = header.lLocalHeaderOffset + sizeof(ZipFile::LocalFileHeader) + header.nFileNameLength + header.desc.lSizeCompressed; + nFileDataOffset = header.lLocalHeaderOffset + sizeof(ZipFile::LocalFileHeader) + header.nFileNameLength + header.nExtraFieldLength; + nEOFOffset = nFileDataOffset + header.desc.lSizeCompressed; } // Uncompresses raw (without wrapping) data that is compressed with method 8 (deflated) in the Zip file @@ -817,8 +815,6 @@ namespace AZ::IO::ZipDir header.nFileNameLength = aznumeric_cast(nFileNameLength); header.nExtraFieldLength = 0; - pFileEntry->nFileDataOffset = pFileEntry->nFileHeaderOffset + sizeof(header) + header.nFileNameLength; - pFileEntry->nEOFOffset = pFileEntry->nFileDataOffset + pFileEntry->desc.lSizeCompressed; if (!AZ::IO::FileIOBase::GetDirectInstance()->Write(fileHandle, &header, sizeof(header))) { return ZD_ERROR_IO_FAILED; diff --git a/Code/Framework/AzFramework/AzFramework/Archive/ZipDirStructures.h b/Code/Framework/AzFramework/AzFramework/Archive/ZipDirStructures.h index 7e1d54b405..9295a7dd95 100644 --- a/Code/Framework/AzFramework/AzFramework/Archive/ZipDirStructures.h +++ b/Code/Framework/AzFramework/AzFramework/Archive/ZipDirStructures.h @@ -169,7 +169,7 @@ namespace AZ::IO::ZipDir inline static constexpr uint32_t INVALID_DATA_OFFSET = 0xFFFFFFFF; ZipFile::DataDescriptor desc{}; - uint32_t nFileDataOffset{}; // offset of the packed info inside the file; NOTE: this can be INVALID_DATA_OFFSET, if not calculated yet! + uint32_t nFileDataOffset{ INVALID_DATA_OFFSET }; // offset of the packed info inside the file; NOTE: this can be INVALID_DATA_OFFSET, if not calculated yet! uint32_t nFileHeaderOffset{ INVALID_DATA_OFFSET }; // offset of the local file header uint32_t nNameOffset{}; // offset of the file name in the name pool for the directory diff --git a/Code/Framework/AzFramework/Platform/Linux/AzFramework/Input/Devices/Keyboard/InputDeviceKeyboard_xcb.cpp b/Code/Framework/AzFramework/Platform/Linux/AzFramework/Input/Devices/Keyboard/InputDeviceKeyboard_xcb.cpp new file mode 100644 index 0000000000..8ff4c72d42 --- /dev/null +++ b/Code/Framework/AzFramework/Platform/Linux/AzFramework/Input/Devices/Keyboard/InputDeviceKeyboard_xcb.cpp @@ -0,0 +1,293 @@ +/* + * 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 +#include +#include + +#define explicit ExplicitIsACXXKeyword +#include +#undef explicit +#include +#include +#include + +namespace AzFramework +{ + class InputDeviceKeyboardXcb + : public InputDeviceKeyboard::Implementation + , public LinuxXcbEventHandlerBus::Handler + { + public: + AZ_CLASS_ALLOCATOR(InputDeviceKeyboardXcb, AZ::SystemAllocator, 0); + + using InputDeviceKeyboard::Implementation::Implementation; + InputDeviceKeyboardXcb(InputDeviceKeyboard& inputDevice) + : InputDeviceKeyboard::Implementation(inputDevice) + { + LinuxXcbEventHandlerBus::Handler::BusConnect(); + + auto* interface = AzFramework::LinuxXcbConnectionManagerInterface::Get(); + if (!interface) + { + AZ_Warning("ApplicationLinux", false, "XCB interface not available"); + return; + } + + auto* connection = AzFramework::LinuxXcbConnectionManagerInterface::Get()->GetXcbConnection(); + if (!connection) + { + AZ_Warning("ApplicationLinux", false, "XCB connection not available"); + return; + } + + AZStd::unique_ptr> xkbUseExtensionReply{ + xcb_xkb_use_extension_reply(connection, xcb_xkb_use_extension(connection, 1, 0), nullptr) + }; + if (!xkbUseExtensionReply) + { + AZ_Warning("ApplicationLinux", false, "Failed to initialize the xkb extension"); + return; + } + if (!xkbUseExtensionReply->supported) + { + AZ_Warning("ApplicationLinux", false, "The X server does not support the xkb extension"); + return; + } + + m_coreDeviceId = xkb_x11_get_core_keyboard_device_id(connection); + + m_xkbContext.reset(xkb_context_new(XKB_CONTEXT_NO_FLAGS)); + m_xkbKeymap.reset(xkb_x11_keymap_new_from_device(m_xkbContext.get(), connection, m_coreDeviceId, XKB_KEYMAP_COMPILE_NO_FLAGS)); + m_xkbState.reset(xkb_x11_state_new_from_device(m_xkbKeymap.get(), connection, m_coreDeviceId)); + + m_initialized = true; + } + + bool IsConnected() const override + { + return m_initialized; + } + + bool HasTextEntryStarted() const override + { + return false; + } + + void TextEntryStart(const InputDeviceKeyboard::VirtualKeyboardOptions& options) override + { + } + + void TextEntryStop() override + { + } + + void TickInputDevice() override + { + ProcessRawEventQueues(); + } + + void HandleXcbEvent(xcb_generic_event_t* event) override + { + if (!IsConnected()) + { + return; + } + + switch (event->response_type & ~0x80) + { + case XCB_KEY_PRESS: + { + auto* keyPress = reinterpret_cast(event); + + const InputChannelId* key = InputChannelFromKeyEvent(keyPress->detail); + if (key) + { + QueueRawKeyEvent(*key, true); + } + break; + } + case XCB_KEY_RELEASE: + { + auto* keyRelease = reinterpret_cast(event); + + const InputChannelId* key = InputChannelFromKeyEvent(keyRelease->detail); + if (key) + { + QueueRawKeyEvent(*key, false); + } + break; + } + } + } + + private: + [[nodiscard]] const InputChannelId* InputChannelFromKeyEvent(xcb_keycode_t code) const + { + const xcb_keysym_t keysym = xkb_state_key_get_one_sym(m_xkbState.get(), code); + + switch(keysym) + { + case XKB_KEY_0: return &InputDeviceKeyboard::Key::Alphanumeric0; + case XKB_KEY_1: return &InputDeviceKeyboard::Key::Alphanumeric1; + case XKB_KEY_2: return &InputDeviceKeyboard::Key::Alphanumeric2; + case XKB_KEY_3: return &InputDeviceKeyboard::Key::Alphanumeric3; + case XKB_KEY_4: return &InputDeviceKeyboard::Key::Alphanumeric4; + case XKB_KEY_5: return &InputDeviceKeyboard::Key::Alphanumeric5; + case XKB_KEY_6: return &InputDeviceKeyboard::Key::Alphanumeric6; + case XKB_KEY_7: return &InputDeviceKeyboard::Key::Alphanumeric7; + case XKB_KEY_8: return &InputDeviceKeyboard::Key::Alphanumeric8; + case XKB_KEY_9: return &InputDeviceKeyboard::Key::Alphanumeric9; + case XKB_KEY_A: + case XKB_KEY_a: return &InputDeviceKeyboard::Key::AlphanumericA; + case XKB_KEY_B: + case XKB_KEY_b: return &InputDeviceKeyboard::Key::AlphanumericB; + case XKB_KEY_C: + case XKB_KEY_c: return &InputDeviceKeyboard::Key::AlphanumericC; + case XKB_KEY_D: + case XKB_KEY_d: return &InputDeviceKeyboard::Key::AlphanumericD; + case XKB_KEY_E: + case XKB_KEY_e: return &InputDeviceKeyboard::Key::AlphanumericE; + case XKB_KEY_F: + case XKB_KEY_f: return &InputDeviceKeyboard::Key::AlphanumericF; + case XKB_KEY_G: + case XKB_KEY_g: return &InputDeviceKeyboard::Key::AlphanumericG; + case XKB_KEY_H: + case XKB_KEY_h: return &InputDeviceKeyboard::Key::AlphanumericH; + case XKB_KEY_I: + case XKB_KEY_i: return &InputDeviceKeyboard::Key::AlphanumericI; + case XKB_KEY_J: + case XKB_KEY_j: return &InputDeviceKeyboard::Key::AlphanumericJ; + case XKB_KEY_K: + case XKB_KEY_k: return &InputDeviceKeyboard::Key::AlphanumericK; + case XKB_KEY_L: + case XKB_KEY_l: return &InputDeviceKeyboard::Key::AlphanumericL; + case XKB_KEY_M: + case XKB_KEY_m: return &InputDeviceKeyboard::Key::AlphanumericM; + case XKB_KEY_N: + case XKB_KEY_n: return &InputDeviceKeyboard::Key::AlphanumericN; + case XKB_KEY_O: + case XKB_KEY_o: return &InputDeviceKeyboard::Key::AlphanumericO; + case XKB_KEY_P: + case XKB_KEY_p: return &InputDeviceKeyboard::Key::AlphanumericP; + case XKB_KEY_Q: + case XKB_KEY_q: return &InputDeviceKeyboard::Key::AlphanumericQ; + case XKB_KEY_R: + case XKB_KEY_r: return &InputDeviceKeyboard::Key::AlphanumericR; + case XKB_KEY_S: + case XKB_KEY_s: return &InputDeviceKeyboard::Key::AlphanumericS; + case XKB_KEY_T: + case XKB_KEY_t: return &InputDeviceKeyboard::Key::AlphanumericT; + case XKB_KEY_U: + case XKB_KEY_u: return &InputDeviceKeyboard::Key::AlphanumericU; + case XKB_KEY_V: + case XKB_KEY_v: return &InputDeviceKeyboard::Key::AlphanumericV; + case XKB_KEY_W: + case XKB_KEY_w: return &InputDeviceKeyboard::Key::AlphanumericW; + case XKB_KEY_X: + case XKB_KEY_x: return &InputDeviceKeyboard::Key::AlphanumericX; + case XKB_KEY_Y: + case XKB_KEY_y: return &InputDeviceKeyboard::Key::AlphanumericY; + case XKB_KEY_Z: + case XKB_KEY_z: return &InputDeviceKeyboard::Key::AlphanumericZ; + case XKB_KEY_BackSpace: return &InputDeviceKeyboard::Key::EditBackspace; + case XKB_KEY_Caps_Lock: return &InputDeviceKeyboard::Key::EditCapsLock; + case XKB_KEY_Return: return &InputDeviceKeyboard::Key::EditEnter; + case XKB_KEY_space: return &InputDeviceKeyboard::Key::EditSpace; + case XKB_KEY_Tab: return &InputDeviceKeyboard::Key::EditTab; + case XKB_KEY_Escape: return &InputDeviceKeyboard::Key::Escape; + case XKB_KEY_F1: return &InputDeviceKeyboard::Key::Function01; + case XKB_KEY_F2: return &InputDeviceKeyboard::Key::Function02; + case XKB_KEY_F3: return &InputDeviceKeyboard::Key::Function03; + case XKB_KEY_F4: return &InputDeviceKeyboard::Key::Function04; + case XKB_KEY_F5: return &InputDeviceKeyboard::Key::Function05; + case XKB_KEY_F6: return &InputDeviceKeyboard::Key::Function06; + case XKB_KEY_F7: return &InputDeviceKeyboard::Key::Function07; + case XKB_KEY_F8: return &InputDeviceKeyboard::Key::Function08; + case XKB_KEY_F9: return &InputDeviceKeyboard::Key::Function09; + case XKB_KEY_F10: return &InputDeviceKeyboard::Key::Function10; + case XKB_KEY_F11: return &InputDeviceKeyboard::Key::Function11; + case XKB_KEY_F12: return &InputDeviceKeyboard::Key::Function12; + case XKB_KEY_F13: return &InputDeviceKeyboard::Key::Function13; + case XKB_KEY_F14: return &InputDeviceKeyboard::Key::Function14; + case XKB_KEY_F15: return &InputDeviceKeyboard::Key::Function15; + case XKB_KEY_F16: return &InputDeviceKeyboard::Key::Function16; + case XKB_KEY_F17: return &InputDeviceKeyboard::Key::Function17; + case XKB_KEY_F18: return &InputDeviceKeyboard::Key::Function18; + case XKB_KEY_F19: return &InputDeviceKeyboard::Key::Function19; + case XKB_KEY_F20: return &InputDeviceKeyboard::Key::Function20; + case XKB_KEY_Alt_L: return &InputDeviceKeyboard::Key::ModifierAltL; + case XKB_KEY_Alt_R: return &InputDeviceKeyboard::Key::ModifierAltR; + case XKB_KEY_Control_L: return &InputDeviceKeyboard::Key::ModifierCtrlL; + case XKB_KEY_Control_R: return &InputDeviceKeyboard::Key::ModifierCtrlR; + case XKB_KEY_Shift_L: return &InputDeviceKeyboard::Key::ModifierShiftL; + case XKB_KEY_Shift_R: return &InputDeviceKeyboard::Key::ModifierShiftR; + case XKB_KEY_Super_L: return &InputDeviceKeyboard::Key::ModifierSuperL; + case XKB_KEY_Super_R: return &InputDeviceKeyboard::Key::ModifierSuperR; + case XKB_KEY_Down: return &InputDeviceKeyboard::Key::NavigationArrowDown; + case XKB_KEY_Left: return &InputDeviceKeyboard::Key::NavigationArrowLeft; + case XKB_KEY_Right: return &InputDeviceKeyboard::Key::NavigationArrowRight; + case XKB_KEY_Up: return &InputDeviceKeyboard::Key::NavigationArrowUp; + case XKB_KEY_Delete: return &InputDeviceKeyboard::Key::NavigationDelete; + case XKB_KEY_End: return &InputDeviceKeyboard::Key::NavigationEnd; + case XKB_KEY_Home: return &InputDeviceKeyboard::Key::NavigationHome; + case XKB_KEY_Insert: return &InputDeviceKeyboard::Key::NavigationInsert; + case XKB_KEY_Page_Down: return &InputDeviceKeyboard::Key::NavigationPageDown; + case XKB_KEY_Page_Up: return &InputDeviceKeyboard::Key::NavigationPageUp; + case XKB_KEY_Num_Lock: return &InputDeviceKeyboard::Key::NumLock; + case XKB_KEY_KP_0: return &InputDeviceKeyboard::Key::NumPad0; + case XKB_KEY_KP_1: return &InputDeviceKeyboard::Key::NumPad1; + case XKB_KEY_KP_2: return &InputDeviceKeyboard::Key::NumPad2; + case XKB_KEY_KP_3: return &InputDeviceKeyboard::Key::NumPad3; + case XKB_KEY_KP_4: return &InputDeviceKeyboard::Key::NumPad4; + case XKB_KEY_KP_5: return &InputDeviceKeyboard::Key::NumPad5; + case XKB_KEY_KP_6: return &InputDeviceKeyboard::Key::NumPad6; + case XKB_KEY_KP_7: return &InputDeviceKeyboard::Key::NumPad7; + case XKB_KEY_KP_8: return &InputDeviceKeyboard::Key::NumPad8; + case XKB_KEY_KP_9: return &InputDeviceKeyboard::Key::NumPad9; + case XKB_KEY_KP_Add: return &InputDeviceKeyboard::Key::NumPadAdd; + case XKB_KEY_KP_Decimal: return &InputDeviceKeyboard::Key::NumPadDecimal; + case XKB_KEY_KP_Divide: return &InputDeviceKeyboard::Key::NumPadDivide; + case XKB_KEY_KP_Enter: return &InputDeviceKeyboard::Key::NumPadEnter; + case XKB_KEY_KP_Multiply: return &InputDeviceKeyboard::Key::NumPadMultiply; + case XKB_KEY_KP_Subtract: return &InputDeviceKeyboard::Key::NumPadSubtract; + case XKB_KEY_apostrophe: return &InputDeviceKeyboard::Key::PunctuationApostrophe; + case XKB_KEY_backslash: return &InputDeviceKeyboard::Key::PunctuationBackslash; + case XKB_KEY_bracketleft: return &InputDeviceKeyboard::Key::PunctuationBracketL; + case XKB_KEY_bracketright: return &InputDeviceKeyboard::Key::PunctuationBracketR; + case XKB_KEY_comma: return &InputDeviceKeyboard::Key::PunctuationComma; + case XKB_KEY_equal: return &InputDeviceKeyboard::Key::PunctuationEquals; + case XKB_KEY_hyphen: return &InputDeviceKeyboard::Key::PunctuationHyphen; + case XKB_KEY_period: return &InputDeviceKeyboard::Key::PunctuationPeriod; + case XKB_KEY_semicolon: return &InputDeviceKeyboard::Key::PunctuationSemicolon; + case XKB_KEY_slash: return &InputDeviceKeyboard::Key::PunctuationSlash; + case XKB_KEY_grave: + case XKB_KEY_asciitilde: return &InputDeviceKeyboard::Key::PunctuationTilde; + case XKB_KEY_ISO_Group_Shift: return &InputDeviceKeyboard::Key::SupplementaryISO; + case XKB_KEY_Pause: return &InputDeviceKeyboard::Key::WindowsSystemPause; + case XKB_KEY_Print: return &InputDeviceKeyboard::Key::WindowsSystemPrint; + case XKB_KEY_Scroll_Lock: return &InputDeviceKeyboard::Key::WindowsSystemScrollLock; + default: return nullptr; + } + } + + template + using DeleterForFreeFn = AZStd::integral_constant; + + AZStd::unique_ptr> m_xkbContext; + AZStd::unique_ptr> m_xkbKeymap; + AZStd::unique_ptr> m_xkbState; + int m_coreDeviceId{-1}; + bool m_initialized{false}; + }; + + InputDeviceKeyboard::Implementation* InputDeviceKeyboard::Implementation::Create(InputDeviceKeyboard& inputDevice) + { + return aznew InputDeviceKeyboardXcb(inputDevice); + } +} // namespace AzFramework diff --git a/Code/Framework/AzFramework/Platform/Linux/AzFramework/Windowing/NativeWindow_Linux_xcb.cpp b/Code/Framework/AzFramework/Platform/Linux/AzFramework/Windowing/NativeWindow_Linux_xcb.cpp index 005c1858a3..cfc0222214 100644 --- a/Code/Framework/AzFramework/Platform/Linux/AzFramework/Windowing/NativeWindow_Linux_xcb.cpp +++ b/Code/Framework/AzFramework/Platform/Linux/AzFramework/Windowing/NativeWindow_Linux_xcb.cpp @@ -62,8 +62,16 @@ namespace AzFramework uint32_t eventMask = XCB_CW_BACK_PIXEL | XCB_CW_EVENT_MASK; + const uint32_t interestedEvents = + XCB_EVENT_MASK_STRUCTURE_NOTIFY + | XCB_EVENT_MASK_BUTTON_PRESS + | XCB_EVENT_MASK_BUTTON_RELEASE + | XCB_EVENT_MASK_KEY_PRESS + | XCB_EVENT_MASK_KEY_RELEASE + | XCB_EVENT_MASK_POINTER_MOTION + ; uint32_t valueList[] = { xcbRootScreen->black_pixel, - XCB_EVENT_MASK_STRUCTURE_NOTIFY }; + interestedEvents }; xcb_void_cookie_t xcbCheckResult; diff --git a/Code/Framework/AzFramework/Platform/Linux/platform_linux.cmake b/Code/Framework/AzFramework/Platform/Linux/platform_linux.cmake index 4480f86c1d..dc77ca0abf 100644 --- a/Code/Framework/AzFramework/Platform/Linux/platform_linux.cmake +++ b/Code/Framework/AzFramework/Platform/Linux/platform_linux.cmake @@ -11,10 +11,16 @@ if (${PAL_TRAIT_LINUX_WINDOW_MANAGER} STREQUAL "xcb") find_library(XCB_LIBRARY xcb) + find_library(XCB_XKB_LIBRARY xcb-xkb) + find_library(XKBCOMMON_LIBRARY xkbcommon) + find_library(XKBCOMMON_X11_LIBRARY xkbcommon-x11) set(LY_BUILD_DEPENDENCIES PRIVATE ${XCB_LIBRARY} + ${XKBCOMMON_LIBRARY} + ${XKBCOMMON_X11_LIBRARY} + ${XCB_XKB_LIBRARY} ) set(LY_COMPILE_DEFINITIONS PUBLIC PAL_TRAIT_LINUX_WINDOW_MANAGER_XCB) diff --git a/Code/Framework/AzFramework/Platform/Linux/platform_linux_files.cmake b/Code/Framework/AzFramework/Platform/Linux/platform_linux_files.cmake index 4330675fc7..93767322b2 100644 --- a/Code/Framework/AzFramework/Platform/Linux/platform_linux_files.cmake +++ b/Code/Framework/AzFramework/Platform/Linux/platform_linux_files.cmake @@ -25,7 +25,7 @@ set(FILES AzFramework/Windowing/NativeWindow_Linux_xcb.h AzFramework/Windowing/NativeWindow_Linux_xcb.cpp ../Common/Unimplemented/AzFramework/Input/Devices/Gamepad/InputDeviceGamepad_Unimplemented.cpp - ../Common/Unimplemented/AzFramework/Input/Devices/Keyboard/InputDeviceKeyboard_Unimplemented.cpp + AzFramework/Input/Devices/Keyboard/InputDeviceKeyboard_xcb.cpp ../Common/Unimplemented/AzFramework/Input/Devices/Motion/InputDeviceMotion_Unimplemented.cpp ../Common/Unimplemented/AzFramework/Input/Devices/Mouse/InputDeviceMouse_Unimplemented.cpp ../Common/Unimplemented/AzFramework/Input/Devices/Touch/InputDeviceTouch_Unimplemented.cpp diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/API/ViewportEditorModeTrackerInterface.h b/Code/Framework/AzToolsFramework/AzToolsFramework/API/ViewportEditorModeTrackerInterface.h new file mode 100644 index 0000000000..bcc8afbe6a --- /dev/null +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/API/ViewportEditorModeTrackerInterface.h @@ -0,0 +1,43 @@ +/* + * 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 + +#include +#include +#include + +namespace AzToolsFramework +{ + //! The AZ::Interface of the central editor mode tracker for all viewports. + class ViewportEditorModeTrackerInterface + { + public: + AZ_RTTI(ViewportEditorModeTrackerInterface, "{7D72A4F7-2147-4ED9-A315-E456A3BE3CF6}"); + + virtual ~ViewportEditorModeTrackerInterface() = default; + + //! Activates the specified editor mode for the specified viewport. + virtual AZ::Outcome ActivateMode( + const ViewportEditorModeInfo& viewportEditorModeInfo, ViewportEditorMode mode) = 0; + + //! Deactivates the specified editor mode for the specified viewport. + virtual AZ::Outcome DeactivateMode( + const ViewportEditorModeInfo& viewportEditorModeInfo, ViewportEditorMode mode) = 0; + + //! Attempts to retrieve the editor mode state for the specified viewport, otherwise returns nullptr. + virtual const ViewportEditorModesInterface* GetViewportEditorModes(const ViewportEditorModeInfo& viewportEditorModeInfo) const = 0; + + //! Returns the number of viewports currently being tracked. + virtual size_t GetTrackedViewportCount() const = 0; + + //! Returns true if the specified viewport is being tracked, otherwise false. + virtual bool IsViewportModeTracked(const ViewportEditorModeInfo& viewportEditorModeInfo) const = 0; + }; +} // namespace AzToolsFramework + diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/API/ViewportEditorModeTrackerNotificationBus.h b/Code/Framework/AzToolsFramework/AzToolsFramework/API/ViewportEditorModeTrackerNotificationBus.h new file mode 100644 index 0000000000..42a1cb0113 --- /dev/null +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/API/ViewportEditorModeTrackerNotificationBus.h @@ -0,0 +1,66 @@ +/* + * 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 + +#include +#include +#include + +namespace AzToolsFramework +{ + //! Enumeration of each viewport editor mode. + enum class ViewportEditorMode : AZ::u8 + { + Default, + Component, + Focus, + Pick + }; + + //! Viewport identifier and other relevant viewport data. + struct ViewportEditorModeInfo + { + using IdType = AzFramework::ViewportId; + IdType m_id = ViewportUi::DefaultViewportId; //!< The unique identifier for a given viewport. + }; + + //! Interface for the editor modes of a given viewport. + class ViewportEditorModesInterface + { + public: + virtual ~ViewportEditorModesInterface() = default; + + //! Returns true if the specified editor mode is active, otherwise false. + virtual bool IsModeActive(ViewportEditorMode mode) const = 0; + }; + + //! Provides a bus to notify when the different editor modes are entered/exit. + class ViewportEditorModeNotifications + : public AZ::EBusTraits + { + public: + ////////////////////////////////////////////////////////////////////////// + // EBusTraits overrides + static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Multiple; + static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::ById; + using BusIdType = ViewportEditorModeInfo::IdType; + ////////////////////////////////////////////////////////////////////////// + + //! Notifies subscribers of the a given viewport to the activation of the specified editor mode. + virtual void OnEditorModeActivated([[maybe_unused]] const ViewportEditorModesInterface& editorModeState, [[maybe_unused]] ViewportEditorMode mode) + { + } + + //! Notifies subscribers of the a given viewport to the deactivation of the specified editor mode. + virtual void OnEditorModeDeactivated([[maybe_unused]] const ViewportEditorModesInterface& editorModeState, [[maybe_unused]] ViewportEditorMode mode) + { + } + }; + using ViewportEditorModeNotificationsBus = AZ::EBus; +} // namespace AzToolsFramework diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/AssetBrowserTableModel.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/AssetBrowserTableModel.cpp index fdbd4287ce..469a235b9f 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/AssetBrowserTableModel.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBrowser/AssetBrowserTableModel.cpp @@ -46,7 +46,7 @@ namespace AzToolsFramework QModelIndex AssetBrowserTableModel::mapToSource(const QModelIndex& proxyIndex) const { - Q_ASSERT(!proxyIndex.isValid() || proxyIndex.model() != this); + Q_ASSERT(!proxyIndex.isValid() || proxyIndex.model() == this); if (!proxyIndex.isValid() || !m_indexMap.contains(proxyIndex.row())) { return QModelIndex(); diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/ViewportEditorModeTracker.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/ViewportEditorModeTracker.cpp new file mode 100644 index 0000000000..4adddb02e1 --- /dev/null +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/ViewportEditorModeTracker.cpp @@ -0,0 +1,149 @@ +/* + * 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 +#include + +namespace AzToolsFramework +{ + AZ::Outcome ViewportEditorModes::ActivateMode(ViewportEditorMode mode) + { + if (const AZ::u32 modeIndex = static_cast(mode); + modeIndex < NumEditorModes) + { + m_editorModes[modeIndex] = true; + return AZ::Success(); + } + else + { + return AZ::Failure( + AZStd::string::format("Cannot activate mode %u, mode is not recognized", modeIndex)); + } + } + + AZ::Outcome ViewportEditorModes::DeactivateMode(ViewportEditorMode mode) + { + if (const AZ::u32 modeIndex = static_cast(mode); modeIndex < NumEditorModes) + { + m_editorModes[modeIndex] = false; + return AZ::Success(); + } + else + { + return AZ::Failure( + AZStd::string::format("Cannot deactivate mode %u, mode is not recognized", modeIndex)); + } + } + + bool ViewportEditorModes::IsModeActive(ViewportEditorMode mode) const + { + return m_editorModes[static_cast(mode)]; + } + + void ViewportEditorModeTracker::RegisterInterface() + { + if (AZ::Interface::Get() == nullptr) + { + AZ::Interface::Register(this); + } + } + + void ViewportEditorModeTracker::UnregisterInterface() + { + if (AZ::Interface::Get() != nullptr) + { + AZ::Interface::Unregister(this); + } + } + + AZ::Outcome ViewportEditorModeTracker::ActivateMode( + const ViewportEditorModeInfo& viewportEditorModeInfo, ViewportEditorMode mode) + { + auto& editorModes = m_viewportEditorModesMap[viewportEditorModeInfo.m_id]; + if (editorModes.IsModeActive(mode)) + { + return AZ::Failure(AZStd::string::format( + "Duplicate call to ActivateMode for mode '%u' on id '%i'", static_cast(mode), viewportEditorModeInfo.m_id)); + } + + if (const auto result = editorModes.ActivateMode(mode); + !result.IsSuccess()) + { + return result; + } + + ViewportEditorModeNotificationsBus::Event( + viewportEditorModeInfo.m_id, &ViewportEditorModeNotificationsBus::Events::OnEditorModeActivated, editorModes, mode); + + return AZ::Success(); + } + + AZ::Outcome ViewportEditorModeTracker::DeactivateMode( + const ViewportEditorModeInfo& viewportEditorModeInfo, ViewportEditorMode mode) + { + ViewportEditorModes* editorModes = nullptr; + bool modeWasActive = true; + if (m_viewportEditorModesMap.count(viewportEditorModeInfo.m_id)) + { + editorModes = &m_viewportEditorModesMap.at(viewportEditorModeInfo.m_id); + if (!editorModes->IsModeActive(mode)) + { + return AZ::Failure(AZStd::string::format( + "Duplicate call to DeactivateMode for mode '%u' on id '%i'", static_cast(mode), viewportEditorModeInfo.m_id)); + } + } + else + { + modeWasActive = false; + editorModes = &m_viewportEditorModesMap[viewportEditorModeInfo.m_id]; + } + + if(const auto result = editorModes->DeactivateMode(mode); + !result.IsSuccess()) + { + return result; + } + + ViewportEditorModeNotificationsBus::Event( + viewportEditorModeInfo.m_id, &ViewportEditorModeNotificationsBus::Events::OnEditorModeDeactivated, *editorModes, mode); + + if (modeWasActive) + { + return AZ::Success(); + } + else + { + return AZ::Failure(AZStd::string::format( + "Call to DeactivateMode for mode '%u' on id '%i' without precursor call to ActivateMode", static_cast(mode), + viewportEditorModeInfo.m_id)); + } + } + + const ViewportEditorModesInterface* ViewportEditorModeTracker::GetViewportEditorModes(const ViewportEditorModeInfo& viewportEditorModeInfo) const + { + if (auto editorModes = m_viewportEditorModesMap.find(viewportEditorModeInfo.m_id); + editorModes != m_viewportEditorModesMap.end()) + { + return &editorModes->second; + } + else + { + return nullptr; + } + } + + size_t ViewportEditorModeTracker::GetTrackedViewportCount() const + { + return m_viewportEditorModesMap.size(); + } + + bool ViewportEditorModeTracker::IsViewportModeTracked(const ViewportEditorModeInfo& viewportEditorModeInfo) const + { + return m_viewportEditorModesMap.count(viewportEditorModeInfo.m_id) > 0; + } +} // namespace AzToolsFramework diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/ViewportEditorModeTracker.h b/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/ViewportEditorModeTracker.h new file mode 100644 index 0000000000..6ae68b39b2 --- /dev/null +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportSelection/ViewportEditorModeTracker.h @@ -0,0 +1,61 @@ +/* + * 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 + +#include +#include +#include +#include +#include + +namespace AzToolsFramework +{ + //! The encapsulation of the editor modes for a given viewport. + class ViewportEditorModes + : public ViewportEditorModesInterface + { + public: + //! The number of currently supported viewport editor modes. + static constexpr AZ::u8 NumEditorModes = 4; + + //! Sets the specified mode as active. + AZ::Outcome ActivateMode(ViewportEditorMode mode); + + // Sets the specified mode as inactive. + AZ::Outcome DeactivateMode(ViewportEditorMode mode); + + // ViewportEditorModesInterface ... + bool IsModeActive(ViewportEditorMode mode) const override; + private: + AZStd::array m_editorModes{}; //!< State flags to track active/inactive status of viewport editor modes. + }; + + //! The implementation of the central editor mode state tracker for all viewports. + class ViewportEditorModeTracker + : public ViewportEditorModeTrackerInterface + { + public: + //! Registers this object with the AZ::Interface. + void RegisterInterface(); + + //! Unregisters this object with the AZ::Interface. + void UnregisterInterface(); + + // ViewportEditorModeTrackerInterface overrides ... + AZ::Outcome ActivateMode(const ViewportEditorModeInfo& viewportEditorModeInfo, ViewportEditorMode mode) override; + AZ::Outcome DeactivateMode(const ViewportEditorModeInfo& viewportEditorModeInfo, ViewportEditorMode mode) override; + const ViewportEditorModesInterface* GetViewportEditorModes(const ViewportEditorModeInfo& viewportEditorModeInfo) const override; + size_t GetTrackedViewportCount() const override; + bool IsViewportModeTracked(const ViewportEditorModeInfo& viewportEditorModeInfo) const override; + + private: + using ViewportEditorModesMap = AZStd::unordered_map; + ViewportEditorModesMap m_viewportEditorModesMap; //!< Editor mode state per viewport. + }; +} // namespace AzToolsFramework diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/aztoolsframework_files.cmake b/Code/Framework/AzToolsFramework/AzToolsFramework/aztoolsframework_files.cmake index 2d9e75a115..3932190d8e 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/aztoolsframework_files.cmake +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/aztoolsframework_files.cmake @@ -34,6 +34,7 @@ set(FILES API/EditorAnimationSystemRequestBus.h API/EditorEntityAPI.h API/EditorLevelNotificationBus.h + API/ViewportEditorModeTrackerNotificationBus.h API/EditorVegetationRequestsBus.h API/EditorPythonConsoleBus.h API/EditorPythonRunnerRequestsBus.h @@ -44,6 +45,7 @@ set(FILES API/EntityCompositionNotificationBus.h API/EditorViewportIconDisplayInterface.h API/ViewPaneOptions.h + API/ViewportEditorModeTrackerInterface.h Application/Ticker.h Application/Ticker.cpp Application/EditorEntityManager.cpp @@ -538,6 +540,8 @@ set(FILES ViewportSelection/EditorTransformComponentSelectionRequestBus.cpp ViewportSelection/EditorVisibleEntityDataCache.h ViewportSelection/EditorVisibleEntityDataCache.cpp + ViewportSelection/ViewportEditorModeTracker.cpp + ViewportSelection/ViewportEditorModeTracker.h ToolsFileUtils/ToolsFileUtils.h AssetBrowser/AssetBrowserBus.h AssetBrowser/AssetBrowserSourceDropBus.h diff --git a/Code/Framework/AzToolsFramework/Tests/Viewport/ViewportEditorModeTests.cpp b/Code/Framework/AzToolsFramework/Tests/Viewport/ViewportEditorModeTests.cpp new file mode 100644 index 0000000000..3954ef6dc6 --- /dev/null +++ b/Code/Framework/AzToolsFramework/Tests/Viewport/ViewportEditorModeTests.cpp @@ -0,0 +1,498 @@ +/* + * 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 +#include +#include + +namespace UnitTest +{ + using ViewportEditorMode = AzToolsFramework::ViewportEditorMode; + using ViewportEditorModes = AzToolsFramework::ViewportEditorModes; + using ViewportEditorModeTracker = AzToolsFramework::ViewportEditorModeTracker; + using ViewportEditorModeInfo = AzToolsFramework::ViewportEditorModeInfo; + using ViewportId = ViewportEditorModeInfo::IdType; + using ViewportEditorModesInterface = AzToolsFramework::ViewportEditorModesInterface; + + void ActivateModeAndExpectSuccess(ViewportEditorModes& editorModeState, ViewportEditorMode mode) + { + const auto result = editorModeState.ActivateMode(mode); + EXPECT_TRUE(result.IsSuccess()); + } + + void DeactivateModeAndExpectSuccess(ViewportEditorModes& editorModeState, ViewportEditorMode mode) + { + const auto result = editorModeState.DeactivateMode(mode); + EXPECT_TRUE(result.IsSuccess()); + } + + void SetAllModesActive(ViewportEditorModes& editorModeState) + { + for (auto mode = 0; mode < ViewportEditorModes::NumEditorModes; mode++) + { + ActivateModeAndExpectSuccess(editorModeState, static_cast(mode)); + } + } + + void SetAllModesInactive(ViewportEditorModes& editorModeState) + { + for (auto mode = 0; mode < ViewportEditorModes::NumEditorModes; mode++) + { + DeactivateModeAndExpectSuccess(editorModeState, static_cast(mode)); + } + } + + // Fixture for testing editor mode states + class ViewportEditorModesTestsFixture + : public ::testing::Test + { + public: + ViewportEditorModes m_editorModes; + }; + + // Fixture for testing editor mode states with parameterized test arguments + class ViewportEditorModesTestsFixtureWithParams + : public ViewportEditorModesTestsFixture + , public ::testing::WithParamInterface + { + public: + void SetUp() override + { + m_selectedEditorMode = GetParam(); + } + + ViewportEditorMode m_selectedEditorMode; + }; + + // Fixture for testing the viewport editor mode state tracker + class ViewportEditorModeTrackerTestFixture + : public ToolsApplicationFixture + { + public: + ViewportEditorModeTracker m_viewportEditorModeTracker; + }; + + // Subscriber of viewport editor mode notifications for a single viewport that expects a single mode to be activated/deactivated + class ViewportEditorModeNotificationsBusHandler + : private AzToolsFramework::ViewportEditorModeNotificationsBus::Handler + { + public: + struct ReceivedEvents + { + bool m_onEnter = false; + bool m_onExit = false; + }; + + using EditModeTracker = AZStd::unordered_map; + + ViewportEditorModeNotificationsBusHandler(ViewportId viewportId) + : m_viewportSubscription(viewportId) + { + AzToolsFramework::ViewportEditorModeNotificationsBus::Handler::BusConnect(m_viewportSubscription); + } + + ~ViewportEditorModeNotificationsBusHandler() + { + AzToolsFramework::ViewportEditorModeNotificationsBus::Handler::BusDisconnect(); + } + + ViewportId GetViewportSubscription() const + { + return m_viewportSubscription; + } + + const EditModeTracker& GetEditorModes() const + { + return m_editorModes; + } + + void OnEditorModeActivated([[maybe_unused]]const ViewportEditorModesInterface& editorModeState, ViewportEditorMode mode) override + { + m_editorModes[mode].m_onEnter = true; + } + + virtual void OnEditorModeDeactivated([[maybe_unused]] const ViewportEditorModesInterface& editorModeState, ViewportEditorMode mode) override + { + m_editorModes[mode].m_onExit = true; + } + + private: + ViewportId m_viewportSubscription; + EditModeTracker m_editorModes; + + }; + + // Fixture for testing viewport editor mode notifications publishing + class ViewportEditorModePublisherTestFixture + : public ViewportEditorModeTrackerTestFixture + { + public: + + void SetUpEditorFixtureImpl() override + { + for (auto mode = 0; mode < ViewportEditorModes::NumEditorModes; mode++) + { + m_editorModeHandlers[mode] = AZStd::make_unique(mode); + } + } + + void TearDownEditorFixtureImpl() override + { + for (auto mode = 0; mode < ViewportEditorModes::NumEditorModes; mode++) + { + m_editorModeHandlers[mode].reset(); + } + } + + AZStd::array, ViewportEditorModes::NumEditorModes> m_editorModeHandlers; + }; + + TEST_F(ViewportEditorModesTestsFixture, NumberOfEditorModesIsEqualTo4) + { + EXPECT_EQ(ViewportEditorModes::NumEditorModes, 4); + } + + TEST_F(ViewportEditorModesTestsFixture, InitialEditorModeStateHasAllInactiveModes) + { + for (auto mode = 0; mode < ViewportEditorModes::NumEditorModes; mode++) + { + EXPECT_FALSE(m_editorModes.IsModeActive(static_cast(mode))); + } + } + + TEST_P(ViewportEditorModesTestsFixtureWithParams, SettingModeActiveActivatesOnlyThatMode) + { + ActivateModeAndExpectSuccess(m_editorModes, m_selectedEditorMode); + + for (auto mode = 0; mode < ViewportEditorModes::NumEditorModes; mode++) + { + const auto editorMode = static_cast(mode); + if (editorMode == m_selectedEditorMode) + { + EXPECT_TRUE(m_editorModes.IsModeActive(static_cast(editorMode))); + } + else + { + EXPECT_FALSE(m_editorModes.IsModeActive(static_cast(editorMode))); + } + } + } + + TEST_P(ViewportEditorModesTestsFixtureWithParams, SettingModeInactiveInactivatesOnlyThatMode) + { + SetAllModesActive(m_editorModes); + DeactivateModeAndExpectSuccess(m_editorModes, m_selectedEditorMode); + + for (auto mode = 0; mode < ViewportEditorModes::NumEditorModes; mode++) + { + const auto editorMode = static_cast(mode); + if (editorMode == m_selectedEditorMode) + { + EXPECT_FALSE(m_editorModes.IsModeActive(editorMode)); + } + else + { + EXPECT_TRUE(m_editorModes.IsModeActive(editorMode)); + } + } + } + + TEST_P(ViewportEditorModesTestsFixtureWithParams, SettingMultipleModesActiveActivatesAllThoseModesNonMutuallyExclusively) + { + for (auto mode = 0; mode < ViewportEditorModes::NumEditorModes - 1; mode++) + { + // Given only the selected mode active + SetAllModesInactive(m_editorModes); + { + ActivateModeAndExpectSuccess(m_editorModes, m_selectedEditorMode); + } + + const auto editorMode = static_cast(mode); + if (editorMode == m_selectedEditorMode) + { + continue; + } + + // When other modes are activated + ActivateModeAndExpectSuccess(m_editorModes, editorMode); + + for (auto expectedMode = 0; expectedMode < ViewportEditorModes::NumEditorModes; expectedMode++) + { + const auto expectedEditorMode = static_cast(expectedMode); + if (expectedEditorMode == editorMode || expectedEditorMode == m_selectedEditorMode) + { + // Expect the activated modes to be active + EXPECT_TRUE(m_editorModes.IsModeActive(expectedEditorMode)); + } + else + { + // Expect the modes not active to be inactive + EXPECT_FALSE(m_editorModes.IsModeActive(expectedEditorMode)); + } + } + } + } + + TEST_P(ViewportEditorModesTestsFixtureWithParams, SettingMultipleModesInactiveInactivatesAllThoseModesNonMutuallyExclusively) + { + for (auto mode = 0; mode < ViewportEditorModes::NumEditorModes - 1; mode++) + { + // Given only the selected mode inactive + SetAllModesActive(m_editorModes); + DeactivateModeAndExpectSuccess(m_editorModes, m_selectedEditorMode); + + const auto editorMode = static_cast(mode); + if (editorMode == m_selectedEditorMode) + { + continue; + } + + // When other modes are deactivated + DeactivateModeAndExpectSuccess(m_editorModes, editorMode); + + for (auto expectedMode = 0; expectedMode < ViewportEditorModes::NumEditorModes; expectedMode++) + { + const auto expectedEditorMode = static_cast(expectedMode); + if (expectedEditorMode == editorMode || expectedEditorMode == m_selectedEditorMode) + { + // Expect the deactivated modes to be inactive + EXPECT_FALSE(m_editorModes.IsModeActive(expectedEditorMode)); + } + else + { + // Expects the modes not deactivated to still be active + EXPECT_TRUE(m_editorModes.IsModeActive(expectedEditorMode)); + } + } + } + } + + INSTANTIATE_TEST_CASE_P( + AllEditorModes, + ViewportEditorModesTestsFixtureWithParams, + ::testing::Values( + AzToolsFramework::ViewportEditorMode::Default, + AzToolsFramework::ViewportEditorMode::Component, + AzToolsFramework::ViewportEditorMode::Focus, + AzToolsFramework::ViewportEditorMode::Pick)); + + TEST_F(ViewportEditorModesTestsFixture, SettingOutOfBoundsModeActiveReturnsError) + { + const auto result = m_editorModes.ActivateMode(static_cast(ViewportEditorModes::NumEditorModes)); + EXPECT_FALSE(result.IsSuccess()); + } + + TEST_F(ViewportEditorModesTestsFixture, SettingOutOfBoundsModeInactiveReturnsError) + { + const auto result = m_editorModes.DeactivateMode(static_cast(ViewportEditorModes::NumEditorModes)); + EXPECT_FALSE(result.IsSuccess()); + } + + TEST_F(ViewportEditorModeTrackerTestFixture, InitialCentralStateTrackerHasNoViewportEditorModess) + { + EXPECT_EQ(m_viewportEditorModeTracker.GetTrackedViewportCount(), 0); + } + + TEST_F(ViewportEditorModeTrackerTestFixture, RegisteringViewportEditorModeForNonExistentIdCreatesViewportEditorModesForThatId) + { + // Given a viewport not currently being tracked + const ViewportId viewportid = 0; + EXPECT_FALSE(m_viewportEditorModeTracker.IsViewportModeTracked({ viewportid })); + EXPECT_EQ(m_viewportEditorModeTracker.GetViewportEditorModes({ viewportid }), nullptr); + + // When a mode is activated for that viewport + const auto editorMode = ViewportEditorMode::Default; + m_viewportEditorModeTracker.ActivateMode({ viewportid }, editorMode); + const auto* viewportEditorModeState = m_viewportEditorModeTracker.GetViewportEditorModes({ viewportid }); + + // Expect that viewport to now be tracked + EXPECT_TRUE(m_viewportEditorModeTracker.IsViewportModeTracked({ viewportid })); + EXPECT_NE(viewportEditorModeState, nullptr); + + // Expect the mode for that viewport to be active + EXPECT_TRUE(viewportEditorModeState->IsModeActive(editorMode)); + } + + TEST_F(ViewportEditorModeTrackerTestFixture, UnregisteringViewportEditorModeForNonExistentIdCreatesViewportEditorModesForThatIdButReturnsError) + { + // Given a viewport not currently being tracked + const ViewportId viewportid = 0; + EXPECT_FALSE(m_viewportEditorModeTracker.IsViewportModeTracked({ viewportid })); + EXPECT_EQ(m_viewportEditorModeTracker.GetViewportEditorModes({ viewportid }), nullptr); + + // When a mode is deactivated for that viewport + const auto editorMode = ViewportEditorMode::Default; + const auto expectedErrorMsg = AZStd::string::format( + "Call to DeactivateMode for mode '%u' on id '%i' without precursor call to ActivateMode", static_cast(editorMode), viewportid); + const auto result = m_viewportEditorModeTracker.DeactivateMode({ viewportid }, editorMode); + + // Expect an error due to no precursor activation of that mode + EXPECT_FALSE(result.IsSuccess()); + EXPECT_EQ(result.GetError(), expectedErrorMsg); + + // Expect that viewport to now be tracked + const auto* viewportEditorModeState = m_viewportEditorModeTracker.GetViewportEditorModes({ viewportid }); + EXPECT_TRUE(m_viewportEditorModeTracker.IsViewportModeTracked({ viewportid })); + + // Expect the mode for that viewport to be inactive + EXPECT_NE(viewportEditorModeState, nullptr); + EXPECT_FALSE(viewportEditorModeState->IsModeActive(editorMode)); + } + + TEST_F(ViewportEditorModeTrackerTestFixture, GettingNonExistentViewportEditorModesForIdReturnsNull) + { + const ViewportId viewportid = 0; + EXPECT_FALSE(m_viewportEditorModeTracker.IsViewportModeTracked({ viewportid })); + EXPECT_EQ(m_viewportEditorModeTracker.GetViewportEditorModes({ viewportid }), nullptr); + } + + TEST_F(ViewportEditorModeTrackerTestFixture, RegisteringViewportEditorModesForExistingIdInThatStateReturnsError) + { + // Given a viewport not currently tracked + const ViewportId viewportid = 0; + EXPECT_FALSE(m_viewportEditorModeTracker.IsViewportModeTracked({ viewportid })); + EXPECT_EQ(m_viewportEditorModeTracker.GetViewportEditorModes({ viewportid }), nullptr); + + const auto editorMode = ViewportEditorMode::Default; + { + // When the mode is activated for the viewport + const auto result = m_viewportEditorModeTracker.ActivateMode({ viewportid }, editorMode); + + // Expect no error as there is no duplicate activation + EXPECT_TRUE(result.IsSuccess()); + + // Expect the mode to be active for the viewport + const auto* viewportEditorModeState = m_viewportEditorModeTracker.GetViewportEditorModes({ viewportid }); + EXPECT_TRUE(m_viewportEditorModeTracker.IsViewportModeTracked({ viewportid })); + EXPECT_NE(viewportEditorModeState, nullptr); + EXPECT_TRUE(viewportEditorModeState->IsModeActive(editorMode)); + } + { + // When the mode is activated again for the viewport + const auto result = m_viewportEditorModeTracker.ActivateMode({ viewportid }, editorMode); + + // Expect an error for the duplicate activation + const auto expectedErrorMsg = AZStd::string::format( + "Duplicate call to ActivateMode for mode '%u' on id '%i'", static_cast(editorMode), viewportid); + EXPECT_FALSE(result.IsSuccess()); + EXPECT_EQ(result.GetError(), expectedErrorMsg); + + // Expect the mode to still be active for the viewport + const auto* viewportEditorModeState = m_viewportEditorModeTracker.GetViewportEditorModes({ viewportid }); + EXPECT_TRUE(m_viewportEditorModeTracker.IsViewportModeTracked({ viewportid })); + EXPECT_NE(viewportEditorModeState, nullptr); + EXPECT_TRUE(viewportEditorModeState->IsModeActive(editorMode)); + } + } + + TEST_F(ViewportEditorModeTrackerTestFixture, UnregisteringViewportEditorModesForExistingIdNotInThatStateReturnssError) + { + // Given a viewport not currently tracked + const ViewportId viewportid = 0; + EXPECT_FALSE(m_viewportEditorModeTracker.IsViewportModeTracked({ viewportid })); + EXPECT_EQ(m_viewportEditorModeTracker.GetViewportEditorModes({ viewportid }), nullptr); + + const auto editorMode = ViewportEditorMode::Default; + { + // When the mode is activated and then deactivated for the viewport + m_viewportEditorModeTracker.ActivateMode({ viewportid }, editorMode); + const auto result = m_viewportEditorModeTracker.DeactivateMode({ viewportid }, editorMode); + + // Expect no error as there is no duplicate deactivation + EXPECT_TRUE(result.IsSuccess()); + + // Expect the mode to be inctive for the viewport + const auto* viewportEditorModeState = m_viewportEditorModeTracker.GetViewportEditorModes({ viewportid }); + EXPECT_TRUE(m_viewportEditorModeTracker.IsViewportModeTracked({ viewportid })); + EXPECT_NE(viewportEditorModeState, nullptr); + EXPECT_FALSE(viewportEditorModeState->IsModeActive(editorMode)); + } + { + // When the mode is deactivated again for the viewport + const auto result = m_viewportEditorModeTracker.DeactivateMode({ viewportid }, editorMode); + + // Expect an error for the duplicate deactivation + const auto expectedErrorMsg = AZStd::string::format( + "Duplicate call to DeactivateMode for mode '%u' on id '%i'", static_cast(editorMode), viewportid); + EXPECT_FALSE(result.IsSuccess()); + EXPECT_EQ(result.GetError(), expectedErrorMsg); + + // Expect the mode to still be inactive for the viewport + const auto* viewportEditorModeState = m_viewportEditorModeTracker.GetViewportEditorModes({ viewportid }); + EXPECT_TRUE(m_viewportEditorModeTracker.IsViewportModeTracked({ viewportid })); + EXPECT_NE(viewportEditorModeState, nullptr); + EXPECT_FALSE(viewportEditorModeState->IsModeActive(editorMode)); + } + } + + TEST_F( + ViewportEditorModePublisherTestFixture, + RegisteringViewportEditorModesForExistingIdPublishesOnViewportEditorModeRegisterEventForAllSubscribers) + { + // Given a set of subscribers tracking the editor modes for their exclusive viewport + for (auto mode = 0; mode < ViewportEditorModes::NumEditorModes; mode++) + { + // Expect each subscriber to have received no editor mode state changes + EXPECT_EQ(m_editorModeHandlers[mode]->GetEditorModes().size(), 0); + } + + // When each editor mode is activated by the state tracker for a specific viewport + for (auto mode = 0; mode < ViewportEditorModes::NumEditorModes; mode++) + { + const ViewportId viewportId = mode; + const ViewportEditorMode editorMode = static_cast(mode); + m_viewportEditorModeTracker.ActivateMode({ viewportId }, editorMode); + } + + for (auto mode = 0; mode < ViewportEditorModes::NumEditorModes; mode++) + { + // Expect only the subscribers of each viewport to have received the editor mode activated event + const ViewportEditorMode editorMode = static_cast(mode); + const auto& editorModes = m_editorModeHandlers[mode]->GetEditorModes(); + EXPECT_EQ(editorModes.size(), 1); + EXPECT_EQ(editorModes.count(editorMode), 1); + const auto& expectedEditorModeSet = editorModes.find(editorMode); + EXPECT_NE(expectedEditorModeSet, editorModes.end()); + EXPECT_TRUE(expectedEditorModeSet->second.m_onEnter); + EXPECT_FALSE(expectedEditorModeSet->second.m_onExit); + } + } + + TEST_F( + ViewportEditorModePublisherTestFixture, + UnregisteringViewportEditorModesForExistingIdPublishesOnViewportEditorModeUnregisterEventForAllSubscribers) + { + // Given a set of subscribers tracking the editor modes for their exclusive viewport + for (auto mode = 0; mode < ViewportEditorModes::NumEditorModes; mode++) + { + EXPECT_EQ(m_editorModeHandlers[mode]->GetEditorModes().size(), 0); + } + + // When each editor mode is activated deactivated by the state tracker for a specific viewport + for (auto mode = 0; mode < ViewportEditorModes::NumEditorModes; mode++) + { + const ViewportId viewportId = mode; + const ViewportEditorMode editorMode = static_cast(mode); + m_viewportEditorModeTracker.ActivateMode({ viewportId }, editorMode); + m_viewportEditorModeTracker.DeactivateMode({ viewportId }, editorMode); + } + + for (auto mode = 0; mode < ViewportEditorModes::NumEditorModes; mode++) + { + // Expect only the subscribers of each viewport to have received the editor mode activated and deactivated event + const ViewportEditorMode editorMode = static_cast(mode); + const auto& editorModes = m_editorModeHandlers[mode]->GetEditorModes(); + EXPECT_EQ(editorModes.size(), 1); + EXPECT_EQ(editorModes.count(editorMode), 1); + const auto& expectedEditorModeSet = editorModes.find(editorMode); + EXPECT_NE(expectedEditorModeSet, editorModes.end()); + EXPECT_TRUE(expectedEditorModeSet->second.m_onEnter); + EXPECT_TRUE(expectedEditorModeSet->second.m_onExit); + } + } +} // namespace UnitTest diff --git a/Code/Framework/AzToolsFramework/Tests/aztoolsframeworktests_files.cmake b/Code/Framework/AzToolsFramework/Tests/aztoolsframeworktests_files.cmake index 5ff41d9e6c..764afce266 100644 --- a/Code/Framework/AzToolsFramework/Tests/aztoolsframeworktests_files.cmake +++ b/Code/Framework/AzToolsFramework/Tests/aztoolsframeworktests_files.cmake @@ -110,6 +110,7 @@ set(FILES UI/EntityPropertyEditorTests.cpp UndoStack.cpp Viewport/ClusterTests.cpp + Viewport/ViewportEditorModeTests.cpp Viewport/ViewportScreenTests.cpp Viewport/ViewportUiClusterTests.cpp Viewport/ViewportUiDisplayTests.cpp diff --git a/Code/Tools/AssetProcessor/Platform/Mac/AssetProcessorEntitlements.plist b/Code/Tools/AssetProcessor/Platform/Mac/AssetProcessorEntitlements.plist new file mode 100644 index 0000000000..cefa2bf93b --- /dev/null +++ b/Code/Tools/AssetProcessor/Platform/Mac/AssetProcessorEntitlements.plist @@ -0,0 +1,10 @@ + + + + + com.apple.security.cs.allow-dyld-environment-variables + + com.apple.security.cs.disable-library-validation + + + diff --git a/Code/Tools/AssetProcessor/Platform/Mac/assetprocessor_mac.cmake b/Code/Tools/AssetProcessor/Platform/Mac/assetprocessor_mac.cmake index e34ce9d341..c5147d4024 100644 --- a/Code/Tools/AssetProcessor/Platform/Mac/assetprocessor_mac.cmake +++ b/Code/Tools/AssetProcessor/Platform/Mac/assetprocessor_mac.cmake @@ -12,28 +12,5 @@ set_target_properties(AssetProcessor PROPERTIES MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/Platform/Mac/gui_info.plist RESOURCE ${CMAKE_CURRENT_SOURCE_DIR}/Platform/Mac/Images.xcassets XCODE_ATTRIBUTE_ASSETCATALOG_COMPILER_APPICON_NAME AssetProcessorAppIcon + ENTITLEMENT_FILE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/Platform/Mac/AssetProcessorEntitlements.plist ) - -# We cannot use ly_add_target here because we're already including this file from inside ly_add_target -# So we need to setup target, dependencies and install logic manually. -add_executable(AssetProcessorDummy Platform/Mac/main_dummy.cpp) -add_executable(AZ::AssetProcessorDummy ALIAS AssetProcessorDummy) - -ly_target_link_libraries(AssetProcessorDummy - PRIVATE - AZ::AzCore - AZ::AzFramework) - -ly_add_dependencies(AssetProcessor AssetProcessorDummy) - -# Store the aliased target into a DIRECTORY property -set_property(DIRECTORY APPEND PROPERTY LY_DIRECTORY_TARGETS AZ::AssetProcessorDummy) - -# Store the directory path in a GLOBAL property so that it can be accessed -# in the layout install logic. Skip if the directory has already been added -get_property(ly_all_target_directories GLOBAL PROPERTY LY_ALL_TARGET_DIRECTORIES) -if(NOT CMAKE_CURRENT_SOURCE_DIR IN_LIST ly_all_target_directories) - set_property(GLOBAL APPEND PROPERTY LY_ALL_TARGET_DIRECTORIES ${CMAKE_CURRENT_SOURCE_DIR}) -endif() - -ly_install_add_install_path_setreg(AssetProcessor) \ No newline at end of file diff --git a/Code/Tools/AssetProcessor/Platform/Mac/gui_info.plist b/Code/Tools/AssetProcessor/Platform/Mac/gui_info.plist index 301f0c5cee..665abea1d3 100644 --- a/Code/Tools/AssetProcessor/Platform/Mac/gui_info.plist +++ b/Code/Tools/AssetProcessor/Platform/Mac/gui_info.plist @@ -11,7 +11,7 @@ CFBundleSignature ASPR CFBundleExecutable - AssetProcessorDummy + AssetProcessor CFBundleIdentifier com.Amazon.AssetProcessor diff --git a/Code/Tools/AssetProcessor/Platform/Mac/main_dummy.cpp b/Code/Tools/AssetProcessor/Platform/Mac/main_dummy.cpp deleted file mode 100644 index 3eed37e555..0000000000 --- a/Code/Tools/AssetProcessor/Platform/Mac/main_dummy.cpp +++ /dev/null @@ -1,75 +0,0 @@ -/* - * 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 -#include -#include -#include -#include - -#include - -int main(int argc, char* argv[]) -{ - // Create a ComponentApplication to initialize the AZ::SystemAllocator and initialize the SettingsRegistry - AZ::ComponentApplication::Descriptor desc; - AZ::ComponentApplication application; - application.Create(desc); - - AZStd::vector envVars; - - const char* homePath = std::getenv("HOME"); - envVars.push_back(AZStd::string::format("HOME=%s", homePath)); - - if (auto settingsRegistry = AZ::SettingsRegistry::Get(); settingsRegistry != nullptr) - { - const char* dyldLibPathOrig = std::getenv("DYLD_LIBRARY_PATH"); - AZStd::string dyldSearchPath = AZStd::string::format("DYLD_LIBRARY_PATH=%s", dyldLibPathOrig); - if (AZ::IO::FixedMaxPath projectModulePath; - settingsRegistry->Get(projectModulePath.Native(), AZ::SettingsRegistryMergeUtils::FilePathKey_ProjectConfigurationBinPath)) - { - dyldSearchPath.append(":"); - dyldSearchPath.append(projectModulePath.c_str()); - } - - if (AZ::IO::FixedMaxPath installedBinariesFolder; - settingsRegistry->Get(installedBinariesFolder.Native(), AZ::SettingsRegistryMergeUtils::FilePathKey_InstalledBinaryFolder)) - { - if (AZ::IO::FixedMaxPath engineRootFolder; - settingsRegistry->Get(engineRootFolder.Native(), AZ::SettingsRegistryMergeUtils::FilePathKey_EngineRootFolder)) - { - installedBinariesFolder = engineRootFolder / installedBinariesFolder; - dyldSearchPath.append(":"); - dyldSearchPath.append(installedBinariesFolder.c_str()); - } - } - envVars.push_back(dyldSearchPath); - } - - AZStd::string commandArgs; - for (int i = 1; i < argc; i++) - { - commandArgs.append(argv[i]); - commandArgs.append(" "); - } - - AzFramework::ProcessLauncher::ProcessLaunchInfo processLaunchInfo; - AZ::IO::Path processPath{ AZ::IO::PathView(AZ::Utils::GetExecutableDirectory()) }; - processPath /= "AssetProcessor"; - processLaunchInfo.m_processExecutableString = AZStd::move(processPath.Native()); - processLaunchInfo.m_commandlineParameters = commandArgs; - processLaunchInfo.m_environmentVariables = &envVars; - processLaunchInfo.m_showWindow = true; - - AzFramework::ProcessLauncher::LaunchUnwatchedProcess(processLaunchInfo); - - application.Destroy(); - - return 0; -} - diff --git a/Code/Tools/BundleLauncher/CMakeLists.txt b/Code/Tools/BundleLauncher/CMakeLists.txt new file mode 100644 index 0000000000..812cb21099 --- /dev/null +++ b/Code/Tools/BundleLauncher/CMakeLists.txt @@ -0,0 +1,24 @@ +# +# 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 +# +# + +# This is the launcher that will be used by the O3DE_SDK.app bundle +# generated by the cmake install process for Mac. +if(NOT ${PAL_PLATFORM_NAME} STREQUAL Mac) + return() +endif() + +ly_add_target( + NAME O3DE_SDK EXECUTABLE + NAMESPACE AZ + FILES_CMAKE + O3DE_SDK_files.cmake + BUILD_DEPENDENCIES + PRIVATE + AZ::AzCore + AZ::AzFramework +) diff --git a/Code/Tools/BundleLauncher/O3DE_SDK_Launcher.cpp b/Code/Tools/BundleLauncher/O3DE_SDK_Launcher.cpp new file mode 100644 index 0000000000..0c362ac829 --- /dev/null +++ b/Code/Tools/BundleLauncher/O3DE_SDK_Launcher.cpp @@ -0,0 +1,63 @@ +/* + * 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 +#include +#include +#include +#include + +#include +#include + +int main(int argc, char* argv[]) +{ + // We need to pass in the engine path since we won't be able to find it by searching upwards. + // We can't use any containers that use our custom allocator till after the call to ComponentApplication::Create() + AZ::IO::FixedMaxPath processPath = AZ::Utils::GetExecutableDirectory(); + AZ::IO::FixedMaxPath enginePath = (processPath / "../Engine").LexicallyNormal(); + auto enginePathParam = AZ::SettingsRegistryInterface::FixedValueString::format(R"(--engine-path="%s")", enginePath.c_str()); + // Uses the fixed_vector deduction guide to determine the type is AZStd::fixed_vector + AZStd::fixed_vector commandLineParams{ processPath.Native().data(), enginePathParam.data() }; + + + // Create a ComponentApplication to initialize the AZ::SystemAllocator and initialize the SettingsRegistry + AZ::ComponentApplication application(static_cast(commandLineParams.size()), commandLineParams.data()); + application.Create(AZ::ComponentApplication::Descriptor()); + + AZ::IO::FixedMaxPath installedBinariesFolder; + if (auto settingsRegistry = AZ::SettingsRegistry::Get(); settingsRegistry != nullptr) + { + if (settingsRegistry->Get(installedBinariesFolder.Native(), AZ::SettingsRegistryMergeUtils::FilePathKey_InstalledBinaryFolder)) + { + installedBinariesFolder = enginePath / installedBinariesFolder; + } + } + + AZ::IO::FixedMaxPath shellPath = "/bin/sh"; + AZStd::string parameters = AZStd::string::format("-c \"export LY_CMAKE_PATH=/usr/local/bin && \"%s/python/get_python.sh\"\"", enginePath.c_str()); + AzFramework::ProcessLauncher::ProcessLaunchInfo shellProcessLaunch; + shellProcessLaunch.m_processExecutableString = AZStd::move(shellPath.Native()); + shellProcessLaunch.m_commandlineParameters = parameters; + shellProcessLaunch.m_showWindow = true; + shellProcessLaunch.m_workingDirectory = enginePath.String(); + AZStd::unique_ptr shellProcess(AzFramework::ProcessWatcher::LaunchProcess(shellProcessLaunch, AzFramework::ProcessCommunicationType::COMMUNICATOR_TYPE_NONE)); + shellProcess->WaitForProcessToExit(120); + shellProcess.reset(); + + AZ::IO::FixedMaxPath projectManagerPath = installedBinariesFolder/"o3de.app"/"Contents"/"MacOS"/"o3de"; + AzFramework::ProcessLauncher::ProcessLaunchInfo processLaunchInfo; + processLaunchInfo.m_processExecutableString = AZStd::move(projectManagerPath.Native()); + processLaunchInfo.m_showWindow = true; + AzFramework::ProcessLauncher::LaunchUnwatchedProcess(processLaunchInfo); + + application.Destroy(); + + return 0; +} + diff --git a/Code/Tools/BundleLauncher/O3DE_SDK_files.cmake b/Code/Tools/BundleLauncher/O3DE_SDK_files.cmake new file mode 100644 index 0000000000..04989c9df2 --- /dev/null +++ b/Code/Tools/BundleLauncher/O3DE_SDK_files.cmake @@ -0,0 +1,11 @@ +# +# 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 +# +# + +set(FILES + O3DE_SDK_Launcher.cpp +) diff --git a/Code/Tools/BundleLauncher/info.plist b/Code/Tools/BundleLauncher/info.plist new file mode 100644 index 0000000000..5b3dd43b37 --- /dev/null +++ b/Code/Tools/BundleLauncher/info.plist @@ -0,0 +1,18 @@ + + + + + CFBundleExecutable + O3DE_SDK + CFBundleIdentifier + org.O3DE.O3DE_SDK + CFBundlePackageType + APPL + CFBundleSignature + ???? + NSHumanReadableCopyright + Copyright (c) Contributors to the Open 3D Engine Project. + NSPrincipalClass + NSApplication + + diff --git a/Code/Tools/CMakeLists.txt b/Code/Tools/CMakeLists.txt index 66c43e53e5..8107089433 100644 --- a/Code/Tools/CMakeLists.txt +++ b/Code/Tools/CMakeLists.txt @@ -20,3 +20,4 @@ add_subdirectory(GridHub) add_subdirectory(Standalone) add_subdirectory(TestImpactFramework) add_subdirectory(ProjectManager) +add_subdirectory(BundleLauncher) diff --git a/Code/Tools/ProjectManager/Platform/Mac/PAL_mac.cmake b/Code/Tools/ProjectManager/Platform/Mac/PAL_mac.cmake index 7a325ca97e..5cd1fb5a22 100644 --- a/Code/Tools/ProjectManager/Platform/Mac/PAL_mac.cmake +++ b/Code/Tools/ProjectManager/Platform/Mac/PAL_mac.cmake @@ -5,3 +5,4 @@ # SPDX-License-Identifier: Apache-2.0 OR MIT # # + diff --git a/Code/Tools/ProjectManager/Resources/Delete.svg b/Code/Tools/ProjectManager/Resources/Delete.svg new file mode 100644 index 0000000000..f932c71544 --- /dev/null +++ b/Code/Tools/ProjectManager/Resources/Delete.svg @@ -0,0 +1,4 @@ + + + + diff --git a/Code/Tools/ProjectManager/Resources/Edit.svg b/Code/Tools/ProjectManager/Resources/Edit.svg new file mode 100644 index 0000000000..3ee9bbbfae --- /dev/null +++ b/Code/Tools/ProjectManager/Resources/Edit.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/Code/Tools/ProjectManager/Resources/ProjectManager.qrc b/Code/Tools/ProjectManager/Resources/ProjectManager.qrc index 2e93e9eca9..30bcc1ace5 100644 --- a/Code/Tools/ProjectManager/Resources/ProjectManager.qrc +++ b/Code/Tools/ProjectManager/Resources/ProjectManager.qrc @@ -35,5 +35,8 @@ Backgrounds/DefaultBackground.jpg Backgrounds/FtueBackground.jpg FeatureTagClose.svg + Refresh.svg + Edit.svg + Delete.svg diff --git a/Code/Tools/ProjectManager/Resources/ProjectManager.qss b/Code/Tools/ProjectManager/Resources/ProjectManager.qss index 17c5077d83..30117d6636 100644 --- a/Code/Tools/ProjectManager/Resources/ProjectManager.qss +++ b/Code/Tools/ProjectManager/Resources/ProjectManager.qss @@ -518,3 +518,82 @@ QProgressBar::chunk { font-size: 12px; font-weight: 600; } + +/************** Engine **************/ + +#engineTab::tab-bar { + left: 60px; +} + +#engineTabBar::tab { + height: 50px; + background-color: transparent; + font-weight: 400; + font-size: 18px; + min-width: 160px; +} + +#engineTabBar::tab:selected { + border-bottom: 3px solid #94D2FF; + color: #94D2FF; + font-weight: 600; +} +#engineTabBar::tab:hover { + color: #94D2FF; + font-weight: 600; +} +#engineTabBar::tab:pressed { + color: #66bcfa; +} + +#engineTopFrame { + background-color:#1E252F; +} + +/************** Gem Repo **************/ + +#gemRepoHeaderLabel { + font-size: 12px; +} + +#gemRepoHeaderRefreshButton { + background-color: transparent; + qproperty-flat: true; + qproperty-iconSize: 14px; +} + +#gemRepoHeaderAddButton { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #888888, stop: 1.0 #555555); + qproperty-flat: true; + margin-right:30px; + min-width:120px; + max-width:120px; + min-height:24px; + max-height:24px; + border-radius: 3px; + text-align:center; + font-size:12px; + font-weight:600; +} +#gemRepoHeaderAddButton:hover { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #999999, stop: 1.0 #666666); +} +#gemRepoHeaderAddButton:pressed { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #555555, stop: 1.0 #777777); +} + +#gemRepoHeaderTable { + background-color: transparent; + max-height: 30px; +} + +#gemRepoListHeader { + background-color: transparent; +} + +#gemRepoInspector { + background: #444444; +} diff --git a/Code/Tools/ProjectManager/Resources/Refresh.svg b/Code/Tools/ProjectManager/Resources/Refresh.svg new file mode 100644 index 0000000000..80cc892c68 --- /dev/null +++ b/Code/Tools/ProjectManager/Resources/Refresh.svg @@ -0,0 +1,4 @@ + + + + diff --git a/Code/Tools/ProjectManager/Source/EngineScreenCtrl.cpp b/Code/Tools/ProjectManager/Source/EngineScreenCtrl.cpp new file mode 100644 index 0000000000..c78a9426db --- /dev/null +++ b/Code/Tools/ProjectManager/Source/EngineScreenCtrl.cpp @@ -0,0 +1,64 @@ +/* + * 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 +#include +#include + +#include +#include +#include + +namespace O3DE::ProjectManager +{ + EngineScreenCtrl::EngineScreenCtrl(QWidget* parent) + : ScreenWidget(parent) + { + QVBoxLayout* vLayout = new QVBoxLayout(); + vLayout->setContentsMargins(0, 0, 0, 0); + + QFrame* topBarFrameWidget = new QFrame(this); + topBarFrameWidget->setObjectName("engineTopFrame"); + QHBoxLayout* topBarHLayout = new QHBoxLayout(); + topBarHLayout->setContentsMargins(0, 0, 0, 0); + + topBarFrameWidget->setLayout(topBarHLayout); + + QTabWidget* tabWidget = new QTabWidget(); + tabWidget->setObjectName("engineTab"); + tabWidget->tabBar()->setObjectName("engineTabBar"); + tabWidget->tabBar()->setFocusPolicy(Qt::TabFocus); + + m_engineSettingsScreen = new EngineSettingsScreen(); + m_gemRepoScreen = new GemRepoScreen(); + + tabWidget->addTab(m_engineSettingsScreen, tr("General")); + tabWidget->addTab(m_gemRepoScreen, tr("Gem Repositories")); + topBarHLayout->addWidget(tabWidget); + + vLayout->addWidget(topBarFrameWidget); + + setLayout(vLayout); + } + + ProjectManagerScreen EngineScreenCtrl::GetScreenEnum() + { + return ProjectManagerScreen::UpdateProject; + } + + QString EngineScreenCtrl::GetTabText() + { + return tr("Engine"); + } + + bool EngineScreenCtrl::IsTab() + { + return true; + } + +} // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/EngineScreenCtrl.h b/Code/Tools/ProjectManager/Source/EngineScreenCtrl.h new file mode 100644 index 0000000000..9e799f13e7 --- /dev/null +++ b/Code/Tools/ProjectManager/Source/EngineScreenCtrl.h @@ -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 + * + */ +#pragma once + +#if !defined(Q_MOC_RUN) +#include +#endif + +namespace O3DE::ProjectManager +{ + QT_FORWARD_DECLARE_CLASS(EngineSettingsScreen) + QT_FORWARD_DECLARE_CLASS(GemRepoScreen) + + class EngineScreenCtrl + : public ScreenWidget + { + public: + explicit EngineScreenCtrl(QWidget* parent = nullptr); + ~EngineScreenCtrl() = default; + ProjectManagerScreen GetScreenEnum() override; + + QString GetTabText() override; + bool IsTab() override; + + EngineSettingsScreen* m_engineSettingsScreen = nullptr; + GemRepoScreen* m_gemRepoScreen = nullptr; + }; + +} // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/EngineSettingsScreen.cpp b/Code/Tools/ProjectManager/Source/EngineSettingsScreen.cpp index 0c24aac9f1..dec1c9a257 100644 --- a/Code/Tools/ProjectManager/Source/EngineSettingsScreen.cpp +++ b/Code/Tools/ProjectManager/Source/EngineSettingsScreen.cpp @@ -7,15 +7,16 @@ */ #include -#include -#include -#include -#include #include #include #include #include +#include +#include +#include +#include + namespace O3DE::ProjectManager { EngineSettingsScreen::EngineSettingsScreen(QWidget* parent) @@ -78,16 +79,6 @@ namespace O3DE::ProjectManager return ProjectManagerScreen::EngineSettings; } - QString EngineSettingsScreen::GetTabText() - { - return tr("Engine"); - } - - bool EngineSettingsScreen::IsTab() - { - return true; - } - void EngineSettingsScreen::OnTextChanged() { // save engine settings diff --git a/Code/Tools/ProjectManager/Source/EngineSettingsScreen.h b/Code/Tools/ProjectManager/Source/EngineSettingsScreen.h index 9d212c44b1..2f16400405 100644 --- a/Code/Tools/ProjectManager/Source/EngineSettingsScreen.h +++ b/Code/Tools/ProjectManager/Source/EngineSettingsScreen.h @@ -24,8 +24,6 @@ namespace O3DE::ProjectManager ~EngineSettingsScreen() = default; ProjectManagerScreen GetScreenEnum() override; - QString GetTabText() override; - bool IsTab() override; protected slots: void OnTextChanged(); diff --git a/Code/Tools/ProjectManager/Source/GemRepo/GemRepoInfo.cpp b/Code/Tools/ProjectManager/Source/GemRepo/GemRepoInfo.cpp new file mode 100644 index 0000000000..3e524d8ec8 --- /dev/null +++ b/Code/Tools/ProjectManager/Source/GemRepo/GemRepoInfo.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 + +namespace O3DE::ProjectManager +{ + GemRepoInfo::GemRepoInfo( + const QString& name, const QString& creator, const QString& summary, const QDateTime& lastUpdated, bool isEnabled = true) + : m_name(name) + , m_creator(creator) + , m_summary(summary) + , m_lastUpdated(lastUpdated) + , m_isEnabled(isEnabled) + { + } + + bool GemRepoInfo::IsValid() const + { + return !m_name.isEmpty(); + } + + bool GemRepoInfo::operator<(const GemRepoInfo& gemRepoInfo) const + { + return (m_lastUpdated < gemRepoInfo.m_lastUpdated); + } +} // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/GemRepo/GemRepoInfo.h b/Code/Tools/ProjectManager/Source/GemRepo/GemRepoInfo.h new file mode 100644 index 0000000000..6f4f828951 --- /dev/null +++ b/Code/Tools/ProjectManager/Source/GemRepo/GemRepoInfo.h @@ -0,0 +1,37 @@ +/* + * 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 + +#if !defined(Q_MOC_RUN) +#include +#include +#endif + +namespace O3DE::ProjectManager +{ + class GemRepoInfo + { + public: + GemRepoInfo() = default; + GemRepoInfo(const QString& name, const QString& creator, const QString& summary, const QDateTime& lastUpdated, bool isEnabled); + + bool IsValid() const; + + bool operator<(const GemRepoInfo& gemRepoInfo) const; + + QString m_path; + QString m_name = "Unknown Gem Repo Name"; + QString m_creator = "Unknown Creator"; + bool m_isEnabled = false; //! Is the repo currently enabled for this engine? + QString m_summary = "No summary provided."; + QString m_directoryLink; + QString m_repoLink; + QDateTime m_lastUpdated; + }; +} // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/GemRepo/GemRepoItemDelegate.cpp b/Code/Tools/ProjectManager/Source/GemRepo/GemRepoItemDelegate.cpp new file mode 100644 index 0000000000..88ccee2636 --- /dev/null +++ b/Code/Tools/ProjectManager/Source/GemRepo/GemRepoItemDelegate.cpp @@ -0,0 +1,222 @@ +/* + * 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 +#include + +#include +#include +#include + +namespace O3DE::ProjectManager +{ + GemRepoItemDelegate::GemRepoItemDelegate(QAbstractItemModel* model, QObject* parent) + : QStyledItemDelegate(parent) + , m_model(model) + { + m_refreshIcon = QIcon(":/Refresh.svg").pixmap(s_refreshIconSize, s_refreshIconSize); + m_editIcon = QIcon(":/Edit.svg").pixmap(s_iconSize, s_iconSize); + m_deleteIcon = QIcon(":/Delete.svg").pixmap(s_iconSize, s_iconSize); + } + + void GemRepoItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& modelIndex) const + { + if (!modelIndex.isValid()) + { + return; + } + + QStyleOptionViewItem options(option); + initStyleOption(&options, modelIndex); + + painter->setRenderHint(QPainter::Antialiasing); + + QRect fullRect, itemRect, contentRect; + CalcRects(options, fullRect, itemRect, contentRect); + QRect buttonRect = CalcButtonRect(contentRect); + + QFont standardFont(options.font); + standardFont.setPixelSize(static_cast(s_fontSize)); + QFontMetrics standardFontMetrics(standardFont); + + painter->save(); + painter->setClipping(true); + painter->setClipRect(fullRect); + painter->setFont(standardFont); + painter->setPen(m_textColor); + + // Draw background + painter->fillRect(fullRect, m_backgroundColor); + + // Draw item background + const QColor itemBackgroundColor = options.state & QStyle::State_MouseOver ? m_itemBackgroundColor.lighter(120) : m_itemBackgroundColor; + painter->fillRect(itemRect, itemBackgroundColor); + + // Draw border + if (options.state & QStyle::State_Selected) + { + painter->save(); + QPen borderPen(m_borderColor); + borderPen.setWidth(s_borderWidth); + painter->setPen(borderPen); + painter->drawRect(itemRect); + + painter->restore(); + } + + // Repo enabled + DrawButton(painter, buttonRect, modelIndex); + + // Repo name + QString repoName = GemRepoModel::GetName(modelIndex); + repoName = QFontMetrics(standardFont).elidedText(repoName, Qt::TextElideMode::ElideRight, s_nameMaxWidth); + + QRect repoNameRect = GetTextRect(standardFont, repoName, s_fontSize); + int currentHorizontalOffset = buttonRect.left() + s_buttonWidth + s_buttonSpacing; + repoNameRect.moveTo(currentHorizontalOffset, contentRect.center().y() - repoNameRect.height() / 2); + repoNameRect = painter->boundingRect(repoNameRect, Qt::TextSingleLine, repoName); + + painter->drawText(repoNameRect, Qt::TextSingleLine, repoName); + + // Rem repo creator + QString repoCreator = GemRepoModel::GetCreator(modelIndex); + repoCreator = standardFontMetrics.elidedText(repoCreator, Qt::TextElideMode::ElideRight, s_creatorMaxWidth); + + QRect repoCreatorRect = GetTextRect(standardFont, repoCreator, s_fontSize); + currentHorizontalOffset += s_nameMaxWidth + s_contentSpacing; + repoCreatorRect.moveTo(currentHorizontalOffset, contentRect.center().y() - repoCreatorRect.height() / 2); + repoCreatorRect = painter->boundingRect(repoCreatorRect, Qt::TextSingleLine, repoCreator); + + painter->drawText(repoCreatorRect, Qt::TextSingleLine, repoCreator); + + // Repo update + QString repoUpdatedDate = GemRepoModel::GetLastUpdated(modelIndex).toString("dd/MM/yyyy hh:mmap"); + repoUpdatedDate = standardFontMetrics.elidedText(repoUpdatedDate, Qt::TextElideMode::ElideRight, s_updatedMaxWidth); + + QRect repoUpdatedDateRect = GetTextRect(standardFont, repoUpdatedDate, s_fontSize); + currentHorizontalOffset += s_creatorMaxWidth + s_contentSpacing; + repoUpdatedDateRect.moveTo(currentHorizontalOffset, contentRect.center().y() - repoUpdatedDateRect.height() / 2); + repoUpdatedDateRect = painter->boundingRect(repoUpdatedDateRect, Qt::TextSingleLine, repoUpdatedDate); + + painter->drawText(repoUpdatedDateRect, Qt::TextSingleLine, repoUpdatedDate); + + // Draw refresh button + painter->drawPixmap( + repoUpdatedDateRect.left() + repoUpdatedDateRect.width() + s_refreshIconSpacing, + contentRect.center().y() - s_refreshIconSize / 3, // Dividing size by 3 centers much better + m_refreshIcon); + + if (options.state & QStyle::State_MouseOver) + { + DrawEditButtons(painter, contentRect); + } + + painter->restore(); + } + + QSize GemRepoItemDelegate::sizeHint(const QStyleOptionViewItem& option, const QModelIndex& modelIndex) const + { + QStyleOptionViewItem options(option); + initStyleOption(&options, modelIndex); + + int marginsHorizontal = s_itemMargins.left() + s_itemMargins.right() + s_contentMargins.left() + s_contentMargins.right(); + return QSize(marginsHorizontal + s_buttonWidth + s_buttonSpacing + s_nameMaxWidth + s_creatorMaxWidth + s_updatedMaxWidth + s_contentSpacing * 3, s_height); + } + + bool GemRepoItemDelegate::editorEvent(QEvent* event, QAbstractItemModel* model, const QStyleOptionViewItem& option, const QModelIndex& modelIndex) + { + if (!modelIndex.isValid()) + { + return false; + } + + if (event->type() == QEvent::KeyPress) + { + auto keyEvent = static_cast(event); + if (keyEvent->key() == Qt::Key_Space) + { + const bool isAdded = GemRepoModel::IsEnabled(modelIndex); + GemRepoModel::SetEnabled(*model, modelIndex, !isAdded); + return true; + } + } + + if (event->type() == QEvent::MouseButtonPress) + { + QMouseEvent* mouseEvent = static_cast(event); + + QRect fullRect, itemRect, contentRect; + CalcRects(option, fullRect, itemRect, contentRect); + const QRect buttonRect = CalcButtonRect(contentRect); + + if (buttonRect.contains(mouseEvent->pos())) + { + const bool isAdded = GemRepoModel::IsEnabled(modelIndex); + GemRepoModel::SetEnabled(*model, modelIndex, !isAdded); + return true; + } + } + + return QStyledItemDelegate::editorEvent(event, model, option, modelIndex); + } + + void GemRepoItemDelegate::CalcRects(const QStyleOptionViewItem& option, QRect& outFullRect, QRect& outItemRect, QRect& outContentRect) const + { + outFullRect = QRect(option.rect); + outItemRect = QRect(outFullRect.adjusted(s_itemMargins.left(), s_itemMargins.top(), -s_itemMargins.right(), -s_itemMargins.bottom())); + outContentRect = QRect(outItemRect.adjusted(s_contentMargins.left(), s_contentMargins.top(), -s_contentMargins.right(), -s_contentMargins.bottom())); + } + + QRect GemRepoItemDelegate::GetTextRect(QFont& font, const QString& text, qreal fontSize) const + { + font.setPixelSize(static_cast(fontSize)); + return QFontMetrics(font).boundingRect(text); + } + + QRect GemRepoItemDelegate::CalcButtonRect(const QRect& contentRect) const + { + const QPoint topLeft = QPoint(contentRect.left(), contentRect.top() + contentRect.height() / 2 - s_buttonHeight / 2); + const QSize size = QSize(s_buttonWidth, s_buttonHeight); + return QRect(topLeft, size); + } + + void GemRepoItemDelegate::DrawButton(QPainter* painter, const QRect& buttonRect, const QModelIndex& modelIndex) const + { + painter->save(); + QPoint circleCenter; + + const bool isEnabled = GemRepoModel::IsEnabled(modelIndex); + if (isEnabled) + { + painter->setBrush(m_buttonEnabledColor); + painter->setPen(m_buttonEnabledColor); + + circleCenter = buttonRect.center() + QPoint(buttonRect.width() / 2 - s_buttonBorderRadius + 1, 1); + } + else + { + circleCenter = buttonRect.center() + QPoint(-buttonRect.width() / 2 + s_buttonBorderRadius + 1, 1); + } + + // Rounded rect + painter->drawRoundedRect(buttonRect, s_buttonBorderRadius, s_buttonBorderRadius); + + // Circle + painter->setBrush(m_textColor); + painter->drawEllipse(circleCenter, s_buttonCircleRadius, s_buttonCircleRadius); + + painter->restore(); + } + + void GemRepoItemDelegate::DrawEditButtons(QPainter* painter, const QRect& contentRect) const + { + painter->drawPixmap(contentRect.right() - s_iconSize * 2 - s_iconSpacing, contentRect.center().y() - s_iconSize / 2, m_editIcon); + painter->drawPixmap(contentRect.right() - s_iconSize, contentRect.center().y() - s_iconSize / 2, m_deleteIcon); + } + +} // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/GemRepo/GemRepoItemDelegate.h b/Code/Tools/ProjectManager/Source/GemRepo/GemRepoItemDelegate.h new file mode 100644 index 0000000000..08d1fdffae --- /dev/null +++ b/Code/Tools/ProjectManager/Source/GemRepo/GemRepoItemDelegate.h @@ -0,0 +1,82 @@ +/* + * 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 + +#if !defined(Q_MOC_RUN) +#include +#include +#endif + +QT_FORWARD_DECLARE_CLASS(QAbstractItemModel) +QT_FORWARD_DECLARE_CLASS(QEvent) + +namespace O3DE::ProjectManager +{ + class GemRepoItemDelegate + : public QStyledItemDelegate + { + Q_OBJECT // AUTOMOC + + public: + explicit GemRepoItemDelegate(QAbstractItemModel* model, QObject* parent = nullptr); + ~GemRepoItemDelegate() = default; + + void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& modelIndex) const override; + bool editorEvent(QEvent* event, QAbstractItemModel* model, const QStyleOptionViewItem& option, const QModelIndex& modelIndex) override; + QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& modelIndex) const override; + + // Colors + const QColor m_textColor = QColor("#FFFFFF"); + const QColor m_backgroundColor = QColor("#333333"); // Outside of the actual repo item + const QColor m_itemBackgroundColor = QColor("#404040"); // Background color of the repo item + const QColor m_borderColor = QColor("#1E70EB"); + const QColor m_buttonEnabledColor = QColor("#1E70EB"); + + // Item + inline constexpr static int s_height = 72; // Repo item total height + inline constexpr static qreal s_fontSize = 12.0; + + // Margin and borders + inline constexpr static QMargins s_itemMargins = QMargins(/*left=*/0, /*top=*/8, /*right=*/60, /*bottom=*/8); // Item border distances + inline constexpr static QMargins s_contentMargins = QMargins(/*left=*/20, /*top=*/20, /*right=*/20, /*bottom=*/20); // Distances of the elements within an item to the item borders + inline constexpr static int s_borderWidth = 4; + + // Content + inline constexpr static int s_contentSpacing = 5; + inline constexpr static int s_nameMaxWidth = 145; + inline constexpr static int s_creatorMaxWidth = 115; + inline constexpr static int s_updatedMaxWidth = 125; + + // Button + inline constexpr static int s_buttonWidth = 32; + inline constexpr static int s_buttonHeight = 16; + inline constexpr static int s_buttonBorderRadius = 8; + inline constexpr static int s_buttonCircleRadius = s_buttonBorderRadius - 2; + inline constexpr static int s_buttonSpacing = 20; + + // Icon + inline constexpr static int s_iconSize = 24; + inline constexpr static int s_iconSpacing = 16; + inline constexpr static int s_refreshIconSize = 14; + inline constexpr static int s_refreshIconSpacing = 10; + + protected: + void CalcRects(const QStyleOptionViewItem& option, QRect& outFullRect, QRect& outItemRect, QRect& outContentRect) const; + QRect GetTextRect(QFont& font, const QString& text, qreal fontSize) const; + QRect CalcButtonRect(const QRect& contentRect) const; + void DrawButton(QPainter* painter, const QRect& contentRect, const QModelIndex& modelIndex) const; + void DrawEditButtons(QPainter* painter, const QRect& contentRect) const; + + QAbstractItemModel* m_model = nullptr; + + QPixmap m_refreshIcon; + QPixmap m_editIcon; + QPixmap m_deleteIcon; + }; +} // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/GemRepo/GemRepoListView.cpp b/Code/Tools/ProjectManager/Source/GemRepo/GemRepoListView.cpp new file mode 100644 index 0000000000..519d52cb35 --- /dev/null +++ b/Code/Tools/ProjectManager/Source/GemRepo/GemRepoListView.cpp @@ -0,0 +1,23 @@ +/* + * 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 +#include + +namespace O3DE::ProjectManager +{ + GemRepoListView::GemRepoListView(QAbstractItemModel* model, QWidget* parent) + : QListView(parent) + { + setObjectName("gemRepoListView"); + setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); + + setModel(model); + setItemDelegate(new GemRepoItemDelegate(model, this)); + } +} // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/GemRepo/GemRepoListView.h b/Code/Tools/ProjectManager/Source/GemRepo/GemRepoListView.h new file mode 100644 index 0000000000..0fd5d5c180 --- /dev/null +++ b/Code/Tools/ProjectManager/Source/GemRepo/GemRepoListView.h @@ -0,0 +1,28 @@ +/* + * 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 + +#if !defined(Q_MOC_RUN) +#include +#endif + +QT_FORWARD_DECLARE_CLASS(QAbstractItemModel) + +namespace O3DE::ProjectManager +{ + class GemRepoListView + : public QListView + { + Q_OBJECT // AUTOMOC + + public: + explicit GemRepoListView(QAbstractItemModel* model, QWidget* parent = nullptr); + ~GemRepoListView() = default; + }; +} // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/GemRepo/GemRepoModel.cpp b/Code/Tools/ProjectManager/Source/GemRepo/GemRepoModel.cpp new file mode 100644 index 0000000000..7a42c135e9 --- /dev/null +++ b/Code/Tools/ProjectManager/Source/GemRepo/GemRepoModel.cpp @@ -0,0 +1,94 @@ +/* + * 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 + +#include + +namespace O3DE::ProjectManager +{ + GemRepoModel::GemRepoModel(QObject* parent) + : QStandardItemModel(parent) + { + m_selectionModel = new QItemSelectionModel(this, parent); + } + + QItemSelectionModel* GemRepoModel::GetSelectionModel() const + { + return m_selectionModel; + } + + void GemRepoModel::AddGemRepo(const GemRepoInfo& gemRepoInfo) + { + QStandardItem* item = new QStandardItem(); + + item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable); + + item->setData(gemRepoInfo.m_name, RoleName); + item->setData(gemRepoInfo.m_creator, RoleCreator); + item->setData(gemRepoInfo.m_summary, RoleSummary); + item->setData(gemRepoInfo.m_isEnabled, RoleIsEnabled); + item->setData(gemRepoInfo.m_directoryLink, RoleDirectoryLink); + item->setData(gemRepoInfo.m_repoLink, RoleRepoLink); + item->setData(gemRepoInfo.m_lastUpdated, RoleLastUpdated); + item->setData(gemRepoInfo.m_path, RolePath); + + appendRow(item); + } + + void GemRepoModel::Clear() + { + clear(); + } + + QString GemRepoModel::GetName(const QModelIndex& modelIndex) + { + return modelIndex.data(RoleName).toString(); + } + + QString GemRepoModel::GetCreator(const QModelIndex& modelIndex) + { + return modelIndex.data(RoleCreator).toString(); + } + + QString GemRepoModel::GetSummary(const QModelIndex& modelIndex) + { + return modelIndex.data(RoleSummary).toString(); + } + + QString GemRepoModel::GetDirectoryLink(const QModelIndex& modelIndex) + { + return modelIndex.data(RoleDirectoryLink).toString(); + } + + QString GemRepoModel::GetRepoLink(const QModelIndex& modelIndex) + { + return modelIndex.data(RoleRepoLink).toString(); + } + + QDateTime GemRepoModel::GetLastUpdated(const QModelIndex& modelIndex) + { + return modelIndex.data(RoleLastUpdated).toDateTime(); + } + + QString GemRepoModel::GetPath(const QModelIndex& modelIndex) + { + return modelIndex.data(RolePath).toString(); + } + + bool GemRepoModel::IsEnabled(const QModelIndex& modelIndex) + { + return modelIndex.data(RoleIsEnabled).toBool(); + } + + void GemRepoModel::SetEnabled(QAbstractItemModel& model, const QModelIndex& modelIndex, bool isEnabled) + { + model.setData(modelIndex, isEnabled, RoleIsEnabled); + } + +} // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/GemRepo/GemRepoModel.h b/Code/Tools/ProjectManager/Source/GemRepo/GemRepoModel.h new file mode 100644 index 0000000000..2f1537d339 --- /dev/null +++ b/Code/Tools/ProjectManager/Source/GemRepo/GemRepoModel.h @@ -0,0 +1,58 @@ +/* + * 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 + +#if !defined(Q_MOC_RUN) +#include +#include +#endif + +QT_FORWARD_DECLARE_CLASS(QItemSelectionModel) + +namespace O3DE::ProjectManager +{ + class GemRepoModel + : public QStandardItemModel + { + Q_OBJECT // AUTOMOC + + public: + explicit GemRepoModel(QObject* parent = nullptr); + QItemSelectionModel* GetSelectionModel() const; + + void AddGemRepo(const GemRepoInfo& gemInfo); + void Clear(); + + static QString GetName(const QModelIndex& modelIndex); + static QString GetCreator(const QModelIndex& modelIndex); + static QString GetSummary(const QModelIndex& modelIndex); + static QString GetDirectoryLink(const QModelIndex& modelIndex); + static QString GetRepoLink(const QModelIndex& modelIndex); + static QDateTime GetLastUpdated(const QModelIndex& modelIndex); + static QString GetPath(const QModelIndex& modelIndex); + + static bool IsEnabled(const QModelIndex& modelIndex); + static void SetEnabled(QAbstractItemModel& model, const QModelIndex& modelIndex, bool isEnabled); + + private: + enum UserRole + { + RoleName = Qt::UserRole, + RoleCreator, + RoleSummary, + RoleIsEnabled, + RoleDirectoryLink, + RoleRepoLink, + RoleLastUpdated, + RolePath + }; + + QItemSelectionModel* m_selectionModel = nullptr; + }; +} // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/GemRepo/GemRepoScreen.cpp b/Code/Tools/ProjectManager/Source/GemRepo/GemRepoScreen.cpp new file mode 100644 index 0000000000..82de53a0d0 --- /dev/null +++ b/Code/Tools/ProjectManager/Source/GemRepo/GemRepoScreen.cpp @@ -0,0 +1,145 @@ +/* + * 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 +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace O3DE::ProjectManager +{ + GemRepoScreen::GemRepoScreen(QWidget* parent) + : ScreenWidget(parent) + { + m_gemRepoModel = new GemRepoModel(this); + + QVBoxLayout* vLayout = new QVBoxLayout(); + vLayout->setMargin(0); + vLayout->setSpacing(0); + setLayout(vLayout); + + QHBoxLayout* hLayout = new QHBoxLayout(); + hLayout->setMargin(0); + hLayout->setSpacing(0); + vLayout->addLayout(hLayout); + + hLayout->addSpacing(60); + + m_gemRepoInspector = new QFrame(this); + m_gemRepoInspector->setObjectName(tr("gemRepoInspector")); + m_gemRepoInspector->setFixedWidth(240); + + QVBoxLayout* middleVLayout = new QVBoxLayout(); + middleVLayout->setMargin(0); + middleVLayout->setSpacing(0); + + middleVLayout->addSpacing(30); + + QHBoxLayout* topMiddleHLayout = new QHBoxLayout(); + topMiddleHLayout->setMargin(0); + topMiddleHLayout->setSpacing(0); + + m_lastAllUpdateLabel = new QLabel(tr("Last Updated: Never"), this); + m_lastAllUpdateLabel->setObjectName("gemRepoHeaderLabel"); + topMiddleHLayout->addWidget(m_lastAllUpdateLabel); + + topMiddleHLayout->addSpacing(20); + + m_AllUpdateButton = new QPushButton(QIcon(":/Refresh.svg"), tr("Update All"), this); + m_AllUpdateButton->setObjectName("gemRepoHeaderRefreshButton"); + topMiddleHLayout->addWidget(m_AllUpdateButton); + + topMiddleHLayout->addSpacerItem(new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Minimum)); + + m_AddRepoButton = new QPushButton(tr("Add Repository"), this); + m_AddRepoButton->setObjectName("gemRepoHeaderAddButton"); + topMiddleHLayout->addWidget(m_AddRepoButton); + + middleVLayout->addLayout(topMiddleHLayout); + + middleVLayout->addSpacing(30); + + // Create a QTableWidget just for its header + // Using a seperate model allows the setup of a header exactly as needed + m_gemRepoHeaderTable = new QTableWidget(this); + m_gemRepoHeaderTable->setObjectName("gemRepoHeaderTable"); + m_gemRepoListHeader = m_gemRepoHeaderTable->horizontalHeader(); + m_gemRepoListHeader->setObjectName("gemRepoListHeader"); + m_gemRepoListHeader->setSectionResizeMode(QHeaderView::ResizeMode::Fixed); + + // Insert columns so the header labels will show up + m_gemRepoHeaderTable->insertColumn(0); + m_gemRepoHeaderTable->insertColumn(1); + m_gemRepoHeaderTable->insertColumn(2); + m_gemRepoHeaderTable->insertColumn(3); + m_gemRepoHeaderTable->setHorizontalHeaderLabels({ tr("Enabled"), tr("Repository Name"), tr("Creator"), tr("Updated") }); + + const int headerExtraMargin = 10; + m_gemRepoListHeader->resizeSection(0, GemRepoItemDelegate::s_buttonWidth + GemRepoItemDelegate::s_buttonSpacing - 3); + m_gemRepoListHeader->resizeSection(1, GemRepoItemDelegate::s_nameMaxWidth + GemRepoItemDelegate::s_contentSpacing - headerExtraMargin); + m_gemRepoListHeader->resizeSection(2, GemRepoItemDelegate::s_creatorMaxWidth + GemRepoItemDelegate::s_contentSpacing - headerExtraMargin); + m_gemRepoListHeader->resizeSection(3, GemRepoItemDelegate::s_updatedMaxWidth + GemRepoItemDelegate::s_contentSpacing - headerExtraMargin); + + // Required to set stylesheet in code as it will not be respected if set in qss + m_gemRepoHeaderTable->horizontalHeader()->setStyleSheet("QHeaderView::section { background-color:transparent; color:white; font-size:12px; text-align:left; border-style:none; }"); + middleVLayout->addWidget(m_gemRepoHeaderTable); + + m_gemRepoListView = new GemRepoListView(m_gemRepoModel, this); + middleVLayout->addWidget(m_gemRepoListView); + + hLayout->addLayout(middleVLayout); + hLayout->addWidget(m_gemRepoInspector); + + Reinit(); + } + + void GemRepoScreen::Reinit() + { + m_gemRepoModel->clear(); + FillModel(); + + // Select the first entry after everything got correctly sized + QTimer::singleShot(200, [=]{ + QModelIndex firstModelIndex = m_gemRepoListView->model()->index(0,0); + m_gemRepoListView->selectionModel()->select(firstModelIndex, QItemSelectionModel::ClearAndSelect); + }); + } + + void GemRepoScreen::FillModel() + { + AZ::Outcome, AZStd::string> allGemRepoInfosResult = PythonBindingsInterface::Get()->GetAllGemRepoInfos(); + if (allGemRepoInfosResult.IsSuccess()) + { + // Add all available repos to the model + const QVector allGemRepoInfos = allGemRepoInfosResult.GetValue(); + for (const GemRepoInfo& gemRepoInfo : allGemRepoInfos) + { + m_gemRepoModel->AddGemRepo(gemRepoInfo); + } + } + else + { + QMessageBox::critical(this, tr("Operation failed"), QString("Cannot retrieve gem repos for engine.\n\nError:\n%2").arg(allGemRepoInfosResult.GetError().c_str())); + } + } + + ProjectManagerScreen GemRepoScreen::GetScreenEnum() + { + return ProjectManagerScreen::GemRepos; + } +} // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/GemRepo/GemRepoScreen.h b/Code/Tools/ProjectManager/Source/GemRepo/GemRepoScreen.h new file mode 100644 index 0000000000..b5316db84f --- /dev/null +++ b/Code/Tools/ProjectManager/Source/GemRepo/GemRepoScreen.h @@ -0,0 +1,50 @@ +/* + * 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 + +#if !defined(Q_MOC_RUN) +#include +#endif + +QT_FORWARD_DECLARE_CLASS(QLabel) +QT_FORWARD_DECLARE_CLASS(QPushButton) +QT_FORWARD_DECLARE_CLASS(QHeaderView) +QT_FORWARD_DECLARE_CLASS(QTableWidget) + +namespace O3DE::ProjectManager +{ + QT_FORWARD_DECLARE_CLASS(GemRepoListView) + QT_FORWARD_DECLARE_CLASS(GemRepoModel) + + class GemRepoScreen + : public ScreenWidget + { + public: + explicit GemRepoScreen(QWidget* parent = nullptr); + ~GemRepoScreen() = default; + ProjectManagerScreen GetScreenEnum() override; + + void Reinit(); + + GemRepoModel* GetGemRepoModel() const { return m_gemRepoModel; } + + private: + void FillModel(); + + QTableWidget* m_gemRepoHeaderTable = nullptr; + QHeaderView* m_gemRepoListHeader = nullptr; + GemRepoListView* m_gemRepoListView = nullptr; + QFrame* m_gemRepoInspector = nullptr; + GemRepoModel* m_gemRepoModel = nullptr; + + QLabel* m_lastAllUpdateLabel; + QPushButton* m_AllUpdateButton; + QPushButton* m_AddRepoButton; + }; +} // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/ProjectManagerWindow.cpp b/Code/Tools/ProjectManager/Source/ProjectManagerWindow.cpp index a723ccb917..d2f2d969f1 100644 --- a/Code/Tools/ProjectManager/Source/ProjectManagerWindow.cpp +++ b/Code/Tools/ProjectManager/Source/ProjectManagerWindow.cpp @@ -22,7 +22,7 @@ namespace O3DE::ProjectManager QVector screenEnums = { ProjectManagerScreen::Projects, - ProjectManagerScreen::EngineSettings, + ProjectManagerScreen::Engine, ProjectManagerScreen::CreateProject, ProjectManagerScreen::UpdateProject }; diff --git a/Code/Tools/ProjectManager/Source/PythonBindings.cpp b/Code/Tools/ProjectManager/Source/PythonBindings.cpp index 18901946f3..6f8ff9abce 100644 --- a/Code/Tools/ProjectManager/Source/PythonBindings.cpp +++ b/Code/Tools/ProjectManager/Source/PythonBindings.cpp @@ -912,4 +912,45 @@ namespace O3DE::ProjectManager return AZ::Success(AZStd::move(templates)); } } + + GemRepoInfo PythonBindings::GemRepoInfoFromPath(pybind11::handle path, pybind11::handle pyEnginePath) + { + /* Placeholder Logic */ + (void)path; + (void)pyEnginePath; + + return GemRepoInfo(); + } + +//#define MOCK_GEM_REPO_INFO true + + AZ::Outcome, AZStd::string> PythonBindings::GetAllGemRepoInfos() + { + QVector gemRepos; + +#ifndef MOCK_GEM_REPO_INFO + auto result = ExecuteWithLockErrorHandling( + [&] + { + /* Placeholder Logic, o3de scripts need method added + * + for (auto path : m_manifest.attr("get_gem_repos")()) + { + gemRepos.push_back(GemRepoInfoFromPath(path, pybind11::none())); + } + * + */ + }); + if (!result.IsSuccess()) + { + return AZ::Failure(result.GetError().c_str()); + } +#else + gemRepos.push_back(GemRepoInfo("JohnCreates", "John Smith", "", QDateTime(QDate(2021, 8, 31), QTime(11, 57)), true)); + gemRepos.push_back(GemRepoInfo("JanesGems", "Jane Doe", "", QDateTime(QDate(2021, 9, 10), QTime(18, 23)), false)); +#endif // MOCK_GEM_REPO_INFO + + std::sort(gemRepos.begin(), gemRepos.end()); + return AZ::Success(AZStd::move(gemRepos)); + } } diff --git a/Code/Tools/ProjectManager/Source/PythonBindings.h b/Code/Tools/ProjectManager/Source/PythonBindings.h index 8482ac56e7..3b766c3797 100644 --- a/Code/Tools/ProjectManager/Source/PythonBindings.h +++ b/Code/Tools/ProjectManager/Source/PythonBindings.h @@ -56,12 +56,16 @@ namespace O3DE::ProjectManager // ProjectTemplate AZ::Outcome> GetProjectTemplates(const QString& projectPath = {}) override; + // Gem Repos + AZ::Outcome, AZStd::string> GetAllGemRepoInfos() override; + private: AZ_DISABLE_COPY_MOVE(PythonBindings); AZ::Outcome ExecuteWithLockErrorHandling(AZStd::function executionCallback); bool ExecuteWithLock(AZStd::function executionCallback); GemInfo GemInfoFromPath(pybind11::handle path, pybind11::handle pyProjectPath); + GemRepoInfo GemRepoInfoFromPath(pybind11::handle path, pybind11::handle pyEnginePath); ProjectInfo ProjectInfoFromPath(pybind11::handle path); ProjectTemplateInfo ProjectTemplateInfoFromPath(pybind11::handle path, pybind11::handle pyProjectPath); bool RegisterThisEngine(); diff --git a/Code/Tools/ProjectManager/Source/PythonBindingsInterface.h b/Code/Tools/ProjectManager/Source/PythonBindingsInterface.h index ca14a54630..9fd3002f93 100644 --- a/Code/Tools/ProjectManager/Source/PythonBindingsInterface.h +++ b/Code/Tools/ProjectManager/Source/PythonBindingsInterface.h @@ -17,6 +17,7 @@ #include #include #include +#include namespace O3DE::ProjectManager { @@ -56,14 +57,14 @@ namespace O3DE::ProjectManager /** * Get info about a Gem - * @param path the absolute path to the Gem + * @param projectPath the absolute path to the Gem * @return an outcome with GemInfo on success */ virtual AZ::Outcome GetGemInfo(const QString& path, const QString& projectPath = {}) = 0; /** * Get all available gem infos. This concatenates gems registered by the engine and the project. - * @param path The absolute path to the project. + * @param projectPath The absolute path to the project. * @return A list of gem infos. */ virtual AZ::Outcome, AZStd::string> GetAllGemInfos(const QString& projectPath) = 0; @@ -155,6 +156,14 @@ namespace O3DE::ProjectManager * @return an outcome with ProjectTemplateInfos on success */ virtual AZ::Outcome> GetProjectTemplates(const QString& projectPath = {}) = 0; + + // Gem Repos + + /** + * Get all available gem repo infos. Gathers all repos registered with the engine. + * @return A list of gem repo infos. + */ + virtual AZ::Outcome, AZStd::string> GetAllGemRepoInfos() = 0; }; using PythonBindingsInterface = AZ::Interface; diff --git a/Code/Tools/ProjectManager/Source/ScreenDefs.h b/Code/Tools/ProjectManager/Source/ScreenDefs.h index 97ebe19751..2ced8c08c9 100644 --- a/Code/Tools/ProjectManager/Source/ScreenDefs.h +++ b/Code/Tools/ProjectManager/Source/ScreenDefs.h @@ -23,7 +23,9 @@ namespace O3DE::ProjectManager Projects, UpdateProject, UpdateProjectSettings, - EngineSettings + Engine, + EngineSettings, + GemRepos }; static QHash s_ProjectManagerStringNames = { @@ -34,7 +36,9 @@ namespace O3DE::ProjectManager { "Projects", ProjectManagerScreen::Projects}, { "UpdateProject", ProjectManagerScreen::UpdateProject}, { "UpdateProjectSettings", ProjectManagerScreen::UpdateProjectSettings}, - { "EngineSettings", ProjectManagerScreen::EngineSettings} + { "Engine", ProjectManagerScreen::Engine}, + { "EngineSettings", ProjectManagerScreen::EngineSettings}, + { "GemRepos", ProjectManagerScreen::GemRepos} }; // need to define qHash for ProjectManagerScreen when using scoped enums diff --git a/Code/Tools/ProjectManager/Source/ScreenFactory.cpp b/Code/Tools/ProjectManager/Source/ScreenFactory.cpp index f3bddfdd27..44aa713e6a 100644 --- a/Code/Tools/ProjectManager/Source/ScreenFactory.cpp +++ b/Code/Tools/ProjectManager/Source/ScreenFactory.cpp @@ -13,7 +13,9 @@ #include #include #include +#include #include +#include namespace O3DE::ProjectManager { @@ -41,9 +43,15 @@ namespace O3DE::ProjectManager case (ProjectManagerScreen::UpdateProjectSettings): newScreen = new UpdateProjectSettingsScreen(parent); break; + case (ProjectManagerScreen::Engine): + newScreen = new EngineScreenCtrl(parent); + break; case (ProjectManagerScreen::EngineSettings): newScreen = new EngineSettingsScreen(parent); break; + case (ProjectManagerScreen::GemRepos): + newScreen = new GemRepoScreen(parent); + break; case (ProjectManagerScreen::Empty): default: newScreen = new ScreenWidget(parent); diff --git a/Code/Tools/ProjectManager/project_manager_files.cmake b/Code/Tools/ProjectManager/project_manager_files.cmake index 6d0f392e52..7a336972e0 100644 --- a/Code/Tools/ProjectManager/project_manager_files.cmake +++ b/Code/Tools/ProjectManager/project_manager_files.cmake @@ -56,6 +56,8 @@ set(FILES Source/ProjectsScreen.cpp Source/ProjectSettingsScreen.h Source/ProjectSettingsScreen.cpp + Source/EngineScreenCtrl.h + Source/EngineScreenCtrl.cpp Source/EngineSettingsScreen.h Source/EngineSettingsScreen.cpp Source/ProjectButtonWidget.h @@ -98,4 +100,14 @@ set(FILES Source/GemCatalog/GemRequirementListView.cpp Source/GemCatalog/GemSortFilterProxyModel.h Source/GemCatalog/GemSortFilterProxyModel.cpp + Source/GemRepo/GemRepoScreen.h + Source/GemRepo/GemRepoScreen.cpp + Source/GemRepo/GemRepoInfo.h + Source/GemRepo/GemRepoInfo.cpp + Source/GemRepo/GemRepoItemDelegate.h + Source/GemRepo/GemRepoItemDelegate.cpp + Source/GemRepo/GemRepoListView.h + Source/GemRepo/GemRepoListView.cpp + Source/GemRepo/GemRepoModel.h + Source/GemRepo/GemRepoModel.cpp ) diff --git a/Gems/Atom/Feature/Common/Code/CMakeLists.txt b/Gems/Atom/Feature/Common/Code/CMakeLists.txt index b8c414dcc6..9c4ffe5b5e 100644 --- a/Gems/Atom/Feature/Common/Code/CMakeLists.txt +++ b/Gems/Atom/Feature/Common/Code/CMakeLists.txt @@ -40,6 +40,8 @@ ly_add_target( Gem::Atom_Feature_Common.Public Gem::ImGui.imguilib #3rdParty::lux_core # AZ_TRAIT_LUXCORE_SUPPORTED is disabled in every platform, Issue #3915 will remove + RUNTIME_DEPENDENCIES + Gem::ImGui.imguilib ) ly_add_target( diff --git a/Gems/Atom/Feature/Common/Code/Source/AuxGeom/AuxGeomDrawQueue.cpp b/Gems/Atom/Feature/Common/Code/Source/AuxGeom/AuxGeomDrawQueue.cpp index 214643505a..2057c596e5 100644 --- a/Gems/Atom/Feature/Common/Code/Source/AuxGeom/AuxGeomDrawQueue.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/AuxGeom/AuxGeomDrawQueue.cpp @@ -565,7 +565,7 @@ namespace AZ AuxGeomBufferData* AuxGeomDrawQueue::Commit() { - AZ_ATOM_PROFILE_FUNCTION("AuxGeom", "AuxGeomDrawQueue: Commit"); + AZ_PROFILE_SCOPE(AzRender, "AuxGeomDrawQueue: Commit"); // get a mutually exclusive lock and then switch to the next buffer, returning a pointer to the current buffer (before the switch) // grab the lock @@ -585,7 +585,7 @@ namespace AZ void AuxGeomDrawQueue::ClearCurrentBufferData() { - AZ_ATOM_PROFILE_FUNCTION("AuxGeom", "AuxGeomDrawQueue: ClearCurrentBufferData"); + AZ_PROFILE_SCOPE(AzRender, "AuxGeomDrawQueue: ClearCurrentBufferData"); // no need for mutex here, this function is only called from a function holding a lock AuxGeomBufferData& data = m_buffers[m_currentBufferIndex]; @@ -649,7 +649,7 @@ namespace AZ AZ::u8 width, int32_t viewProjOverrideIndex) { - AZ_PROFILE_FUNCTION(AzRender); + AZ_PROFILE_SCOPE(AzRender, "AuxGeomDrawQueue: DrawPrimitiveWithSharedVerticesCommon"); // grab a mutex lock for the rest of this function so that a commit cannot happen during it and // other threads can't add geometry during it @@ -720,8 +720,7 @@ namespace AZ AZ::u8 width, int32_t viewProjOverrideIndex) { - AZ_PROFILE_FUNCTION(AzRender); - AZ_ATOM_PROFILE_FUNCTION("AuxGeom", "AuxGeomDrawQueue: DrawPrimitiveWithSharedVerticesCommon"); + AZ_PROFILE_SCOPE(AzRender, "AuxGeomDrawQueue: DrawPrimitiveWithSharedVerticesCommon"); AZ_Assert(indexCount >= verticesPerPrimitiveType && (indexCount % verticesPerPrimitiveType == 0), "Index count must be at least %d and must be a multiple of %d", diff --git a/Gems/Atom/Feature/Common/Code/Source/AuxGeom/AuxGeomFeatureProcessor.cpp b/Gems/Atom/Feature/Common/Code/Source/AuxGeom/AuxGeomFeatureProcessor.cpp index feeb1dedcc..ee6d3ba4a3 100644 --- a/Gems/Atom/Feature/Common/Code/Source/AuxGeom/AuxGeomFeatureProcessor.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/AuxGeom/AuxGeomFeatureProcessor.cpp @@ -80,7 +80,7 @@ namespace AZ void AuxGeomFeatureProcessor::Render(const FeatureProcessor::RenderPacket& fpPacket) { - AZ_ATOM_PROFILE_FUNCTION("AuxGeom", "AuxGeomFeatureProcessor: Render"); + AZ_PROFILE_SCOPE(AzRender, "AuxGeomFeatureProcessor: Render"); // Get the scene data and switch buffers so that other threads can continue to queue requests AuxGeomBufferData* bufferData = static_cast(m_sceneDrawQueue.get())->Commit(); diff --git a/Gems/Atom/Feature/Common/Code/Source/AuxGeom/DynamicPrimitiveProcessor.cpp b/Gems/Atom/Feature/Common/Code/Source/AuxGeom/DynamicPrimitiveProcessor.cpp index f35809f148..9af6493413 100644 --- a/Gems/Atom/Feature/Common/Code/Source/AuxGeom/DynamicPrimitiveProcessor.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/AuxGeom/DynamicPrimitiveProcessor.cpp @@ -70,7 +70,7 @@ namespace AZ void DynamicPrimitiveProcessor::PrepareFrame() { - AZ_ATOM_PROFILE_FUNCTION("AuxGeom", "DynamicPrimitiveProcessor: PrepareFrame"); + AZ_PROFILE_SCOPE(AzRender, "DynamicPrimitiveProcessor: PrepareFrame"); m_drawPackets.clear(); m_processSrgs.clear(); @@ -88,7 +88,7 @@ namespace AZ void DynamicPrimitiveProcessor::ProcessDynamicPrimitives(const AuxGeomBufferData* bufferData, const RPI::FeatureProcessor::RenderPacket& fpPacket) { - AZ_ATOM_PROFILE_FUNCTION("AuxGeom", "DynamicPrimitiveProcessor: ProcessDynamicPrimitives"); + AZ_PROFILE_SCOPE(AzRender, "DynamicPrimitiveProcessor: ProcessDynamicPrimitives"); RHI::DrawPacketBuilder drawPacketBuilder; const DynamicPrimitiveData& srcPrimitives = bufferData->m_primitiveData; diff --git a/Gems/Atom/Feature/Common/Code/Source/AuxGeom/FixedShapeProcessor.cpp b/Gems/Atom/Feature/Common/Code/Source/AuxGeom/FixedShapeProcessor.cpp index 0045311bef..8b5e439ca0 100644 --- a/Gems/Atom/Feature/Common/Code/Source/AuxGeom/FixedShapeProcessor.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/AuxGeom/FixedShapeProcessor.cpp @@ -108,8 +108,8 @@ namespace AZ } void FixedShapeProcessor::PrepareFrame() - { - AZ_ATOM_PROFILE_FUNCTION("AuxGeom", "FixedShapeProcessor: PrepareFrame"); + { + AZ_PROFILE_SCOPE(AzRender, "FixedShapeProcessor: PrepareFrame"); m_processSrgs.clear(); m_drawPackets.clear(); @@ -127,8 +127,7 @@ namespace AZ void FixedShapeProcessor::ProcessObjects(const AuxGeomBufferData* bufferData, const RPI::FeatureProcessor::RenderPacket& fpPacket) { - AZ_PROFILE_FUNCTION(AzRender); - AZ_ATOM_PROFILE_FUNCTION("AuxGeom", "FixedShapeProcessor: ProcessObjects"); + AZ_PROFILE_SCOPE(AzRender, "FixedShapeProcessor: ProcessObjects"); RHI::DrawPacketBuilder drawPacketBuilder; diff --git a/Gems/Atom/Feature/Common/Code/Source/CoreLights/CapsuleLightFeatureProcessor.cpp b/Gems/Atom/Feature/Common/Code/Source/CoreLights/CapsuleLightFeatureProcessor.cpp index 683295cf5b..849c930afd 100644 --- a/Gems/Atom/Feature/Common/Code/Source/CoreLights/CapsuleLightFeatureProcessor.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/CoreLights/CapsuleLightFeatureProcessor.cpp @@ -102,7 +102,7 @@ namespace AZ void CapsuleLightFeatureProcessor::Simulate(const FeatureProcessor::SimulatePacket& packet) { - AZ_ATOM_PROFILE_FUNCTION("RPI", "CapsuleLightFeatureProcessor: Simulate"); + AZ_PROFILE_SCOPE(RPI, "CapsuleLightFeatureProcessor: Simulate"); AZ_UNUSED(packet); if (m_deviceBufferNeedsUpdate) @@ -114,7 +114,7 @@ namespace AZ void CapsuleLightFeatureProcessor::Render(const CapsuleLightFeatureProcessor::RenderPacket& packet) { - AZ_ATOM_PROFILE_FUNCTION("RPI", "CapsuleLightFeatureProcessor: Render"); + AZ_PROFILE_SCOPE(RPI, "CapsuleLightFeatureProcessor: Render"); for (const RPI::ViewPtr& view : packet.m_views) { diff --git a/Gems/Atom/Feature/Common/Code/Source/CoreLights/DirectionalLightFeatureProcessor.cpp b/Gems/Atom/Feature/Common/Code/Source/CoreLights/DirectionalLightFeatureProcessor.cpp index c235f78595..702818dcd9 100644 --- a/Gems/Atom/Feature/Common/Code/Source/CoreLights/DirectionalLightFeatureProcessor.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/CoreLights/DirectionalLightFeatureProcessor.cpp @@ -196,7 +196,7 @@ namespace AZ void DirectionalLightFeatureProcessor::Simulate(const FeatureProcessor::SimulatePacket&) { - AZ_ATOM_PROFILE_FUNCTION("RPI", "DirectionalLightFeatureProcessor: Simulate"); + AZ_PROFILE_SCOPE(RPI, "DirectionalLightFeatureProcessor: Simulate"); if (m_shadowingLightHandle.IsValid()) { @@ -293,7 +293,7 @@ namespace AZ void DirectionalLightFeatureProcessor::Render(const FeatureProcessor::RenderPacket& packet) { - AZ_ATOM_PROFILE_FUNCTION("RPI", "DirectionalLightFeatureProcessor: Render"); + AZ_PROFILE_SCOPE(RPI, "DirectionalLightFeatureProcessor: Render"); if (m_shadowingLightHandle.IsValid()) { @@ -1232,7 +1232,7 @@ namespace AZ void DirectionalLightFeatureProcessor::SetFilterParameterToPass(LightHandle handle, const RPI::View* cameraView) { - AZ_ATOM_PROFILE_FUNCTION("DirectionalLightFeatureProcessor", "DirectionalLightFeatureProcessor::SetFilterParameterToPass"); + AZ_PROFILE_SCOPE(RPI, "DirectionalLightFeatureProcessor::SetFilterParameterToPass"); if (handle != m_shadowingLightHandle) { diff --git a/Gems/Atom/Feature/Common/Code/Source/CoreLights/DiskLightFeatureProcessor.cpp b/Gems/Atom/Feature/Common/Code/Source/CoreLights/DiskLightFeatureProcessor.cpp index dfbeea0ffe..ca2c038e46 100644 --- a/Gems/Atom/Feature/Common/Code/Source/CoreLights/DiskLightFeatureProcessor.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/CoreLights/DiskLightFeatureProcessor.cpp @@ -123,7 +123,7 @@ namespace AZ void DiskLightFeatureProcessor::Simulate(const FeatureProcessor::SimulatePacket& packet) { - AZ_ATOM_PROFILE_FUNCTION("RPI", "DiskLightFeatureProcessor: Simulate"); + AZ_PROFILE_SCOPE(RPI, "DiskLightFeatureProcessor: Simulate"); AZ_UNUSED(packet); if (m_deviceBufferNeedsUpdate) @@ -135,7 +135,7 @@ namespace AZ void DiskLightFeatureProcessor::Render(const DiskLightFeatureProcessor::RenderPacket& packet) { - AZ_ATOM_PROFILE_FUNCTION("RPI", "DiskLightFeatureProcessor: Simulate"); + AZ_PROFILE_SCOPE(RPI, "DiskLightFeatureProcessor: Simulate"); for (const RPI::ViewPtr& view : packet.m_views) { diff --git a/Gems/Atom/Feature/Common/Code/Source/CoreLights/PointLightFeatureProcessor.cpp b/Gems/Atom/Feature/Common/Code/Source/CoreLights/PointLightFeatureProcessor.cpp index af440e5040..bfb0b1252b 100644 --- a/Gems/Atom/Feature/Common/Code/Source/CoreLights/PointLightFeatureProcessor.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/CoreLights/PointLightFeatureProcessor.cpp @@ -119,7 +119,7 @@ namespace AZ void PointLightFeatureProcessor::Simulate(const FeatureProcessor::SimulatePacket& packet) { - AZ_ATOM_PROFILE_FUNCTION("RPI", "PointLightFeatureProcessor: Simulate"); + AZ_PROFILE_SCOPE(RPI, "PointLightFeatureProcessor: Simulate"); AZ_UNUSED(packet); if (m_deviceBufferNeedsUpdate) @@ -131,7 +131,7 @@ namespace AZ void PointLightFeatureProcessor::Render(const PointLightFeatureProcessor::RenderPacket& packet) { - AZ_ATOM_PROFILE_FUNCTION("RPI", "PointLightFeatureProcessor: Render"); + AZ_PROFILE_SCOPE(RPI, "PointLightFeatureProcessor: Render"); for (const RPI::ViewPtr& view : packet.m_views) { diff --git a/Gems/Atom/Feature/Common/Code/Source/CoreLights/PolygonLightFeatureProcessor.cpp b/Gems/Atom/Feature/Common/Code/Source/CoreLights/PolygonLightFeatureProcessor.cpp index 089adf4621..9dbf01ae26 100644 --- a/Gems/Atom/Feature/Common/Code/Source/CoreLights/PolygonLightFeatureProcessor.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/CoreLights/PolygonLightFeatureProcessor.cpp @@ -132,7 +132,7 @@ namespace AZ::Render void PolygonLightFeatureProcessor::Simulate(const FeatureProcessor::SimulatePacket& packet) { - AZ_ATOM_PROFILE_FUNCTION("RPI", "PolygonLightFeatureProcessor: Simulate"); + AZ_PROFILE_SCOPE(RPI, "PolygonLightFeatureProcessor: Simulate"); AZ_UNUSED(packet); if (m_deviceBufferNeedsUpdate) @@ -153,7 +153,7 @@ namespace AZ::Render void PolygonLightFeatureProcessor::Render(const PolygonLightFeatureProcessor::RenderPacket& packet) { - AZ_ATOM_PROFILE_FUNCTION("RPI", "PolygonLightFeatureProcessor: Render"); + AZ_PROFILE_SCOPE(RPI, "PolygonLightFeatureProcessor: Render"); for (const RPI::ViewPtr& view : packet.m_views) { diff --git a/Gems/Atom/Feature/Common/Code/Source/CoreLights/QuadLightFeatureProcessor.cpp b/Gems/Atom/Feature/Common/Code/Source/CoreLights/QuadLightFeatureProcessor.cpp index e22174225d..aadc8c2020 100644 --- a/Gems/Atom/Feature/Common/Code/Source/CoreLights/QuadLightFeatureProcessor.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/CoreLights/QuadLightFeatureProcessor.cpp @@ -107,7 +107,7 @@ namespace AZ void QuadLightFeatureProcessor::Simulate(const FeatureProcessor::SimulatePacket& packet) { - AZ_ATOM_PROFILE_FUNCTION("RPI", "QuadLightFeatureProcessor: Simulate"); + AZ_PROFILE_SCOPE(RPI, "QuadLightFeatureProcessor: Simulate"); AZ_UNUSED(packet); if (m_deviceBufferNeedsUpdate) @@ -119,7 +119,7 @@ namespace AZ void QuadLightFeatureProcessor::Render(const QuadLightFeatureProcessor::RenderPacket& packet) { - AZ_ATOM_PROFILE_FUNCTION("RPI", "QuadLightFeatureProcessor: Render"); + AZ_PROFILE_SCOPE(RPI, "QuadLightFeatureProcessor: Render"); for (const RPI::ViewPtr& view : packet.m_views) { diff --git a/Gems/Atom/Feature/Common/Code/Source/CoreLights/SimplePointLightFeatureProcessor.cpp b/Gems/Atom/Feature/Common/Code/Source/CoreLights/SimplePointLightFeatureProcessor.cpp index b8bbb03335..f49a5e94fc 100644 --- a/Gems/Atom/Feature/Common/Code/Source/CoreLights/SimplePointLightFeatureProcessor.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/CoreLights/SimplePointLightFeatureProcessor.cpp @@ -102,7 +102,7 @@ namespace AZ void SimplePointLightFeatureProcessor::Simulate(const FeatureProcessor::SimulatePacket& packet) { - AZ_ATOM_PROFILE_FUNCTION("RPI", "SimplePointLightFeatureProcessor: Simulate"); + AZ_PROFILE_SCOPE(RPI, "SimplePointLightFeatureProcessor: Simulate"); AZ_UNUSED(packet); if (m_deviceBufferNeedsUpdate) @@ -114,7 +114,7 @@ namespace AZ void SimplePointLightFeatureProcessor::Render(const SimplePointLightFeatureProcessor::RenderPacket& packet) { - AZ_ATOM_PROFILE_FUNCTION("RPI", "SimplePointLightFeatureProcessor: Render"); + AZ_PROFILE_SCOPE(RPI, "SimplePointLightFeatureProcessor: Render"); for (const RPI::ViewPtr& view : packet.m_views) { diff --git a/Gems/Atom/Feature/Common/Code/Source/CoreLights/SimpleSpotLightFeatureProcessor.cpp b/Gems/Atom/Feature/Common/Code/Source/CoreLights/SimpleSpotLightFeatureProcessor.cpp index d4776faec6..277b5026d7 100644 --- a/Gems/Atom/Feature/Common/Code/Source/CoreLights/SimpleSpotLightFeatureProcessor.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/CoreLights/SimpleSpotLightFeatureProcessor.cpp @@ -102,7 +102,7 @@ namespace AZ void SimpleSpotLightFeatureProcessor::Simulate(const FeatureProcessor::SimulatePacket& packet) { - AZ_ATOM_PROFILE_FUNCTION("RPI", "SimpleSpotLightFeatureProcessor: Simulate"); + AZ_PROFILE_SCOPE(RPI, "SimpleSpotLightFeatureProcessor: Simulate"); AZ_UNUSED(packet); if (m_deviceBufferNeedsUpdate) @@ -114,7 +114,7 @@ namespace AZ void SimpleSpotLightFeatureProcessor::Render(const SimpleSpotLightFeatureProcessor::RenderPacket& packet) { - AZ_ATOM_PROFILE_FUNCTION("RPI", "SimpleSpotLightFeatureProcessor: Render"); + AZ_PROFILE_SCOPE(RPI, "SimpleSpotLightFeatureProcessor: Render"); for (const RPI::ViewPtr& view : packet.m_views) { diff --git a/Gems/Atom/Feature/Common/Code/Source/Decals/DecalFeatureProcessor.cpp b/Gems/Atom/Feature/Common/Code/Source/Decals/DecalFeatureProcessor.cpp index 4954ffc01c..4ff718b5ad 100644 --- a/Gems/Atom/Feature/Common/Code/Source/Decals/DecalFeatureProcessor.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/Decals/DecalFeatureProcessor.cpp @@ -107,7 +107,7 @@ namespace AZ void DecalFeatureProcessor::Simulate(const RPI::FeatureProcessor::SimulatePacket& packet) { - AZ_ATOM_PROFILE_FUNCTION("RPI", "DecalFeatureProcessor: Simulate"); + AZ_PROFILE_SCOPE(RPI, "DecalFeatureProcessor: Simulate"); AZ_UNUSED(packet); if (m_deviceBufferNeedsUpdate) @@ -131,7 +131,7 @@ namespace AZ void DecalFeatureProcessor::Render(const RPI::FeatureProcessor::RenderPacket& packet) { - AZ_ATOM_PROFILE_FUNCTION("RPI", "DecalFeatureProcessor: Render"); + AZ_PROFILE_SCOPE(RPI, "DecalFeatureProcessor: Render"); AZStd::array_view> baseMaps = GetImagesFromDecalData<1>(); AZStd::array_view> opacityMaps = GetImagesFromDecalData<2>(); diff --git a/Gems/Atom/Feature/Common/Code/Source/Decals/DecalTextureArrayFeatureProcessor.cpp b/Gems/Atom/Feature/Common/Code/Source/Decals/DecalTextureArrayFeatureProcessor.cpp index 4ecfd7fc09..c0b8f3315a 100644 --- a/Gems/Atom/Feature/Common/Code/Source/Decals/DecalTextureArrayFeatureProcessor.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/Decals/DecalTextureArrayFeatureProcessor.cpp @@ -145,7 +145,7 @@ namespace AZ void DecalTextureArrayFeatureProcessor::Simulate(const RPI::FeatureProcessor::SimulatePacket& packet) { - AZ_PROFILE_FUNCTION(AzRender); + AZ_PROFILE_SCOPE(AzRender, "DecalTextureArrayFeatureProcessor: Simulate"); AZ_UNUSED(packet); if (m_deviceBufferNeedsUpdate) @@ -158,7 +158,7 @@ namespace AZ void DecalTextureArrayFeatureProcessor::Render(const RPI::FeatureProcessor::RenderPacket& packet) { // Note that decals are rendered as part of the forward shading pipeline. We only need to bind the decal buffers/textures in here. - AZ_PROFILE_FUNCTION(AzRender); + AZ_PROFILE_SCOPE(AzRender, "DecalTextureArrayFeatureProcessor: Render"); for (const RPI::ViewPtr& view : packet.m_views) { @@ -294,7 +294,7 @@ namespace AZ void DecalTextureArrayFeatureProcessor::SetDecalMaterial(const DecalHandle handle, const AZ::Data::AssetId material) { - AZ_PROFILE_FUNCTION(AzRender); + AZ_PROFILE_SCOPE(AzRender, "DecalTextureArrayFeatureProcessor: SetDecalMaterial"); if (handle.IsNull()) { AZ_Warning("DecalTextureArrayFeatureProcessor", false, "Invalid handle passed to DecalTextureArrayFeatureProcessor::SetDecalMaterial()."); @@ -364,7 +364,7 @@ namespace AZ void DecalTextureArrayFeatureProcessor::OnAssetReady(const Data::Asset asset) { - AZ_PROFILE_FUNCTION(AzRender); + AZ_PROFILE_SCOPE(AzRender, "DecalTextureArrayFeatureProcessor: OnAssetReady"); const Data::AssetId& assetId = asset->GetId(); const RPI::MaterialAsset* materialAsset = asset.GetAs(); diff --git a/Gems/Atom/Feature/Common/Code/Source/DiffuseGlobalIllumination/DiffuseProbeGridFeatureProcessor.cpp b/Gems/Atom/Feature/Common/Code/Source/DiffuseGlobalIllumination/DiffuseProbeGridFeatureProcessor.cpp index 543851da0f..d79240d4f1 100644 --- a/Gems/Atom/Feature/Common/Code/Source/DiffuseGlobalIllumination/DiffuseProbeGridFeatureProcessor.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/DiffuseGlobalIllumination/DiffuseProbeGridFeatureProcessor.cpp @@ -111,7 +111,7 @@ namespace AZ void DiffuseProbeGridFeatureProcessor::Simulate([[maybe_unused]] const FeatureProcessor::SimulatePacket& packet) { - AZ_PROFILE_FUNCTION(AzRender); + AZ_PROFILE_SCOPE(AzRender, "DiffuseProbeGridFeatureProcessor: Simulate"); // update pipeline states if (m_needUpdatePipelineStates) diff --git a/Gems/Atom/Feature/Common/Code/Source/ImGui/ImGuiPass.cpp b/Gems/Atom/Feature/Common/Code/Source/ImGui/ImGuiPass.cpp index b6ca1191dd..1a68317532 100644 --- a/Gems/Atom/Feature/Common/Code/Source/ImGui/ImGuiPass.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/ImGui/ImGuiPass.cpp @@ -577,8 +577,7 @@ namespace AZ void ImGuiPass::BuildCommandListInternal(const RHI::FrameGraphExecuteContext& context) { - AZ_PROFILE_FUNCTION(AzRender); - AZ_ATOM_PROFILE_FUNCTION("Pass", "ImGuiPass: Execute"); + AZ_PROFILE_SCOPE(AzRender, "ImGuiPass: BuildCommandListInternal"); context.GetCommandList()->SetViewport(m_viewportState); @@ -607,8 +606,7 @@ namespace AZ uint32_t ImGuiPass::UpdateImGuiResources() { - AZ_PROFILE_FUNCTION(AzRender); - AZ_ATOM_PROFILE_FUNCTION("Pass", "ImGuiPass: UpdateImGuiResources"); + AZ_PROFILE_SCOPE(AzRender, "ImGuiPass: UpdateImGuiResources"); auto imguiContextScope = ImguiContextScope(m_imguiContext); diff --git a/Gems/Atom/Feature/Common/Code/Source/ImageBasedLights/ImageBasedLightFeatureProcessor.cpp b/Gems/Atom/Feature/Common/Code/Source/ImageBasedLights/ImageBasedLightFeatureProcessor.cpp index 7c41379a14..9cdad34f17 100644 --- a/Gems/Atom/Feature/Common/Code/Source/ImageBasedLights/ImageBasedLightFeatureProcessor.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/ImageBasedLights/ImageBasedLightFeatureProcessor.cpp @@ -50,7 +50,7 @@ namespace AZ void ImageBasedLightFeatureProcessor::Simulate(const FeatureProcessor::SimulatePacket& packet) { - AZ_ATOM_PROFILE_FUNCTION("RPI", "ImageBasedLightFeatureProcessor: Simulate"); + AZ_PROFILE_SCOPE(RPI, "ImageBasedLightFeatureProcessor: Simulate"); AZ_UNUSED(packet); m_sceneSrg->SetImage(m_specularEnvMapIndex, m_specular); diff --git a/Gems/Atom/Feature/Common/Code/Source/Mesh/MeshFeatureProcessor.cpp b/Gems/Atom/Feature/Common/Code/Source/Mesh/MeshFeatureProcessor.cpp index 8ab324cdf7..fe201f9ca8 100644 --- a/Gems/Atom/Feature/Common/Code/Source/Mesh/MeshFeatureProcessor.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/Mesh/MeshFeatureProcessor.cpp @@ -75,8 +75,7 @@ namespace AZ void MeshFeatureProcessor::Simulate(const FeatureProcessor::SimulatePacket& packet) { - AZ_PROFILE_FUNCTION(AzRender); - AZ_ATOM_PROFILE_FUNCTION("RPI", "MeshFeatureProcessor: Simulate"); + AZ_PROFILE_SCOPE(RPI, "MeshFeatureProcessor: Simulate"); AZ_UNUSED(packet); AZStd::concurrency_check_scope scopeCheck(m_meshDataChecker); @@ -149,7 +148,7 @@ namespace AZ const MeshHandleDescriptor& descriptor, const MaterialAssignmentMap& materials) { - AZ_PROFILE_FUNCTION(AzRender); + AZ_PROFILE_SCOPE(AzRender, "MeshFeatureProcessor: AcquireMesh"); // don't need to check the concurrency during emplace() because the StableDynamicArray won't move the other elements during insertion MeshHandle meshDataHandle = m_meshData.emplace(); @@ -952,7 +951,7 @@ namespace AZ subMeshes.push_back(subMesh); } - rayTracingFeatureProcessor->SetMesh(m_objectId, subMeshes); + rayTracingFeatureProcessor->SetMesh(m_objectId, m_model->GetModelAsset()->GetId(), subMeshes); } void MeshDataInstance::SetSortKey(RHI::DrawItemSortKey sortKey) @@ -984,7 +983,7 @@ namespace AZ void MeshDataInstance::UpdateDrawPackets(bool forceUpdate /*= false*/) { - AZ_PROFILE_FUNCTION(AzRender); + AZ_PROFILE_SCOPE(AzRender, "MeshDataInstance:: UpdateDrawPackets"); for (auto& drawPacketList : m_drawPacketListsByLod) { for (auto& drawPacket : drawPacketList) @@ -999,7 +998,7 @@ namespace AZ void MeshDataInstance::BuildCullable() { - AZ_PROFILE_FUNCTION(AzRender); + AZ_PROFILE_SCOPE(AzRender, "MeshDataInstance: BuildCullable"); AZ_Assert(m_cullableNeedsRebuild, "This function only needs to be called if the cullable to be rebuilt"); AZ_Assert(m_model, "The model has not finished loading yet"); @@ -1076,7 +1075,7 @@ namespace AZ void MeshDataInstance::UpdateCullBounds(const TransformServiceFeatureProcessor* transformService) { - AZ_PROFILE_FUNCTION(AzRender); + AZ_PROFILE_SCOPE(AzRender, "MeshDataInstance: UpdateCullBounds"); AZ_Assert(m_cullBoundsNeedsUpdate, "This function only needs to be called if the culling bounds need to be rebuilt"); AZ_Assert(m_model, "The model has not finished loading yet"); diff --git a/Gems/Atom/Feature/Common/Code/Source/PostProcess/PostProcessFeatureProcessor.cpp b/Gems/Atom/Feature/Common/Code/Source/PostProcess/PostProcessFeatureProcessor.cpp index 35294d7403..d5b3ebd63b 100644 --- a/Gems/Atom/Feature/Common/Code/Source/PostProcess/PostProcessFeatureProcessor.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/PostProcess/PostProcessFeatureProcessor.cpp @@ -49,7 +49,7 @@ namespace AZ void PostProcessFeatureProcessor::Simulate(const FeatureProcessor::SimulatePacket& packet) { - AZ_ATOM_PROFILE_FUNCTION("RPI", "PostProcessFeatureProcessor: Simulate"); + AZ_PROFILE_SCOPE(RPI, "PostProcessFeatureProcessor: Simulate"); AZ_UNUSED(packet); UpdateTime(); diff --git a/Gems/Atom/Feature/Common/Code/Source/PostProcessing/SMAAFeatureProcessor.cpp b/Gems/Atom/Feature/Common/Code/Source/PostProcessing/SMAAFeatureProcessor.cpp index 56d413e84f..a795d88212 100644 --- a/Gems/Atom/Feature/Common/Code/Source/PostProcessing/SMAAFeatureProcessor.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/PostProcessing/SMAAFeatureProcessor.cpp @@ -161,7 +161,7 @@ namespace AZ void SMAAFeatureProcessor::Render([[maybe_unused]] const SMAAFeatureProcessor::RenderPacket& packet) { - AZ_ATOM_PROFILE_FUNCTION("RPI", "SMAAFeatureProcessor: Render"); + AZ_PROFILE_SCOPE(RPI, "SMAAFeatureProcessor: Render"); UpdateConvertToPerceptualPass(); UpdateEdgeDetectionPass(); diff --git a/Gems/Atom/Feature/Common/Code/Source/RayTracing/RayTracingFeatureProcessor.cpp b/Gems/Atom/Feature/Common/Code/Source/RayTracing/RayTracingFeatureProcessor.cpp index ea89f64b73..a17ecaf1aa 100644 --- a/Gems/Atom/Feature/Common/Code/Source/RayTracing/RayTracingFeatureProcessor.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/RayTracing/RayTracingFeatureProcessor.cpp @@ -79,7 +79,7 @@ namespace AZ AZ_Assert(m_rayTracingMaterialSrg, "Failed to create RayTracingMaterialSrg"); } - void RayTracingFeatureProcessor::SetMesh(const ObjectId objectId, const SubMeshVector& subMeshes) + void RayTracingFeatureProcessor::SetMesh(const ObjectId objectId, const AZ::Data::AssetId& assetId, const SubMeshVector& subMeshes) { if (!m_rayTracingEnabled) { @@ -89,10 +89,13 @@ namespace AZ RHI::Ptr device = RHI::RHISystemInterface::Get()->GetDevice(); uint32_t objectIndex = objectId.GetIndex(); + // lock the mutex to protect the mesh and BLAS lists + AZStd::unique_lock lock(m_mutex); + MeshMap::iterator itMesh = m_meshes.find(objectIndex); if (itMesh == m_meshes.end()) { - m_meshes.insert(AZStd::make_pair(objectIndex, Mesh{ subMeshes })); + m_meshes.insert(AZStd::make_pair(objectIndex, Mesh{ assetId, subMeshes })); } else { @@ -102,9 +105,12 @@ namespace AZ m_meshes[objectIndex].m_subMeshes = subMeshes; } - // create the BLAS buffers for each sub-mesh + // create the BLAS buffers for each sub-mesh, or re-use existing BLAS objects if they were already created. + // Note: all sub-meshes must either create new BLAS objects or re-use existing ones, otherwise it's an error (it's the same model in both cases) // Note: the buffer is just reserved here, the BLAS is built in the RayTracingAccelerationStructurePass Mesh& mesh = m_meshes[objectIndex]; + bool blasInstanceFound = false; + for (auto& subMesh : mesh.m_subMeshes) { RHI::RayTracingBlasDescriptor blasDescriptor; @@ -115,11 +121,37 @@ namespace AZ ->IndexBuffer(subMesh.m_indexBufferView) ; - // create the BLAS object - subMesh.m_blas = AZ::RHI::RayTracingBlas::CreateRHIRayTracingBlas(); + // search for an existing BLAS object for this model + RayTracingBlasMap::iterator itBlas = m_blasMap.find(assetId); + if (itBlas != m_blasMap.end()) + { + // re-use existing BLAS + subMesh.m_blas = itBlas->second.m_blas; + itBlas->second.m_count++; - // create the buffers from the descriptor - subMesh.m_blas->CreateBuffers(*device, &blasDescriptor, *m_bufferPools); + // keep track of the fact that we re-used a BLAS + blasInstanceFound = true; + } + else + { + AZ_Assert(blasInstanceFound == false, "Partial set of RayTracingBlas objects found for mesh"); + + // create the BLAS object + subMesh.m_blas = AZ::RHI::RayTracingBlas::CreateRHIRayTracingBlas(); + + // create the buffers from the descriptor + subMesh.m_blas->CreateBuffers(*device, &blasDescriptor, *m_bufferPools); + + // store the BLAS in the side list + RayTracingBlasInstance blasInstance = { subMesh.m_blas, 1 }; + m_blasMap.insert({ assetId, blasInstance }); + } + } + + if (blasInstanceFound) + { + // set the mesh BLAS flag so we don't try to rebuild it in the RayTracingAccelerationStructurePass + mesh.m_blasBuilt = true; } // set initial transform @@ -140,12 +172,26 @@ namespace AZ return; } + // lock the mutex to protect the mesh and BLAS lists + AZStd::unique_lock lock(m_mutex); + MeshMap::iterator itMesh = m_meshes.find(objectId.GetIndex()); if (itMesh != m_meshes.end()) { m_subMeshCount -= aznumeric_cast(itMesh->second.m_subMeshes.size()); m_meshes.erase(itMesh); m_revision++; + + // decrement the count from the BLAS instance, and check to see if we can remove it + RayTracingBlasMap::iterator itBlas = m_blasMap.find(itMesh->second.m_assetId); + if (itBlas != m_blasMap.end()) + { + itBlas->second.m_count--; + if (itBlas->second.m_count == 0) + { + m_blasMap.erase(itBlas); + } + } } m_meshInfoBufferNeedsUpdate = true; diff --git a/Gems/Atom/Feature/Common/Code/Source/RayTracing/RayTracingFeatureProcessor.h b/Gems/Atom/Feature/Common/Code/Source/RayTracing/RayTracingFeatureProcessor.h index 6bd9829c7e..be75f0fac9 100644 --- a/Gems/Atom/Feature/Common/Code/Source/RayTracing/RayTracingFeatureProcessor.h +++ b/Gems/Atom/Feature/Common/Code/Source/RayTracing/RayTracingFeatureProcessor.h @@ -116,6 +116,9 @@ namespace AZ //! Contains data for the top level mesh, including the list of sub-meshes struct Mesh { + // assetId of the model + AZ::Data::AssetId m_assetId = AZ::Data::AssetId{}; + // sub-mesh list SubMeshVector m_subMeshes; @@ -134,7 +137,7 @@ namespace AZ //! Sets ray tracing data for a mesh. //! This will cause an update to the RayTracing acceleration structure on the next frame - void SetMesh(const ObjectId objectId, const SubMeshVector& subMeshes); + void SetMesh(const ObjectId objectId, const AZ::Data::AssetId& assetId, const SubMeshVector& subMeshes); //! Removes ray tracing data for a mesh. //! This will cause an update to the RayTracing acceleration structure on the next frame @@ -220,6 +223,9 @@ namespace AZ // cached TransformServiceFeatureProcessor TransformServiceFeatureProcessor* m_transformServiceFeatureProcessor = nullptr; + // mutex for the mesh and BLAS lists + AZStd::mutex m_mutex; + // structure for data in the m_meshInfoBuffer, shaders that use the buffer must match this type struct MeshInfo { @@ -260,6 +266,16 @@ namespace AZ // flag indicating we need to update the materialInfo buffer bool m_materialInfoBufferNeedsUpdate = false; + + // side list for looking up existing BLAS objects so they can be re-used when the same mesh is added multiple times + struct RayTracingBlasInstance + { + RHI::Ptr m_blas; + uint32_t m_count = 0; + }; + + using RayTracingBlasMap = AZStd::unordered_map; + RayTracingBlasMap m_blasMap; }; } } diff --git a/Gems/Atom/Feature/Common/Code/Source/ReflectionProbe/ReflectionProbeFeatureProcessor.cpp b/Gems/Atom/Feature/Common/Code/Source/ReflectionProbe/ReflectionProbeFeatureProcessor.cpp index 341d1a0274..91c72013dd 100644 --- a/Gems/Atom/Feature/Common/Code/Source/ReflectionProbe/ReflectionProbeFeatureProcessor.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/ReflectionProbe/ReflectionProbeFeatureProcessor.cpp @@ -154,8 +154,7 @@ namespace AZ void ReflectionProbeFeatureProcessor::Simulate([[maybe_unused]] const FeatureProcessor::SimulatePacket& packet) { - AZ_PROFILE_FUNCTION(AzRender); - AZ_ATOM_PROFILE_FUNCTION("ReflectionProbe", "ReflectionProbeFeatureProcessor: Simulate"); + AZ_PROFILE_SCOPE(AzRender, "ReflectionProbeFeatureProcessor: Simulate"); // update pipeline states if (m_needUpdatePipelineStates) @@ -194,7 +193,6 @@ namespace AZ if (m_probeSortRequired) { AZ_PROFILE_SCOPE(AzRender, "Sort reflection probes"); - AZ_ATOM_PROFILE_FUNCTION("ReflectionProbe", "ReflectionProbeFeatureProcessor: Sort reflection probes"); // sort the probes by descending inner volume size, so the smallest volumes are rendered last auto sortFn = [](AZStd::shared_ptr const& probe1, AZStd::shared_ptr const& probe2) -> bool diff --git a/Gems/Atom/Feature/Common/Code/Source/Shadows/ProjectedShadowFeatureProcessor.cpp b/Gems/Atom/Feature/Common/Code/Source/Shadows/ProjectedShadowFeatureProcessor.cpp index 68c31bd859..8fb971f8f5 100644 --- a/Gems/Atom/Feature/Common/Code/Source/Shadows/ProjectedShadowFeatureProcessor.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/Shadows/ProjectedShadowFeatureProcessor.cpp @@ -486,7 +486,7 @@ namespace AZ::Render void ProjectedShadowFeatureProcessor::Simulate(const FeatureProcessor::SimulatePacket& /*packet*/) { - AZ_ATOM_PROFILE_FUNCTION("RPI", "ProjectedShadowFeatureProcessor: Simulate"); + AZ_PROFILE_SCOPE(RPI, "ProjectedShadowFeatureProcessor: Simulate"); if (m_shadowmapPassNeedsUpdate) { @@ -581,7 +581,7 @@ namespace AZ::Render void ProjectedShadowFeatureProcessor::Render(const ProjectedShadowFeatureProcessor::RenderPacket& packet) { - AZ_ATOM_PROFILE_FUNCTION("RPI", "ProjectedShadowFeatureProcessor: Render"); + AZ_PROFILE_SCOPE(RPI, "ProjectedShadowFeatureProcessor: Render"); if (!m_projectedShadowmapsPasses.empty()) { diff --git a/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshFeatureProcessor.cpp b/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshFeatureProcessor.cpp index 0aa72bf2ca..500eadf110 100644 --- a/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshFeatureProcessor.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshFeatureProcessor.cpp @@ -69,8 +69,7 @@ namespace AZ void SkinnedMeshFeatureProcessor::Render(const FeatureProcessor::RenderPacket& packet) { - AZ_PROFILE_FUNCTION(AzRender); - AZ_ATOM_PROFILE_FUNCTION("SkinnedMesh", "SkinnedMeshFeatureProcessor: Render"); + AZ_PROFILE_SCOPE(AzRender, "SkinnedMeshFeatureProcessor: Render"); #if 0 //[GFX_TODO][ATOM-13564] Temporarily disable skinning culling until we figure out how to hook up visibility & lod selection with skinning: //Setup the culling workgroup (it will be re-used for each view) diff --git a/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshInputBuffers.cpp b/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshInputBuffers.cpp index b84d4347a7..4146a669fe 100644 --- a/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshInputBuffers.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/SkinnedMesh/SkinnedMeshInputBuffers.cpp @@ -534,7 +534,7 @@ namespace AZ // lod0 Positions[^ ^] lod0Normals[^ ^] lod1Positions[^ ^] lod1Normals[^ ^] // lod0 subMesh0+1 Positions[^ ^^ ^] lod0 subMesh0+1 Normals[^ ^^ ^] lod1 sm0+1 pos[^ ^^ ^] lod1 sm0+1 norm[^ ^^ ^] - AZ_PROFILE_FUNCTION(AzRender); + AZ_PROFILE_SCOPE(AzRender, "SkinnedMeshInputBuffers: CreateSkinnedMeshInstance"); AZStd::intrusive_ptr instance = aznew SkinnedMeshInstance; // Each model gets a unique, random ID, so if the same source model is used for multiple instances, multiple target models will be created. diff --git a/Gems/Atom/Feature/Common/Code/Source/SkyBox/SkyBoxFeatureProcessor.cpp b/Gems/Atom/Feature/Common/Code/Source/SkyBox/SkyBoxFeatureProcessor.cpp index d0150fc0c2..d4fa3f52be 100644 --- a/Gems/Atom/Feature/Common/Code/Source/SkyBox/SkyBoxFeatureProcessor.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/SkyBox/SkyBoxFeatureProcessor.cpp @@ -105,7 +105,7 @@ namespace AZ void SkyBoxFeatureProcessor::Simulate(const FeatureProcessor::SimulatePacket& packet) { - AZ_ATOM_PROFILE_FUNCTION("RPI", "SkyBoxFeatureProcessor: Simulate"); + AZ_PROFILE_SCOPE(RPI, "SkyBoxFeatureProcessor: Simulate"); AZ_UNUSED(packet); m_sceneSrg->SetConstant(m_skyboxEnableIndex, m_enable); diff --git a/Gems/Atom/RHI/Code/Include/Atom/RHI/CpuProfiler.h b/Gems/Atom/RHI/Code/Include/Atom/RHI/CpuProfiler.h index 3fedc99566..70c5771b57 100644 --- a/Gems/Atom/RHI/Code/Include/Atom/RHI/CpuProfiler.h +++ b/Gems/Atom/RHI/Code/Include/Atom/RHI/CpuProfiler.h @@ -9,6 +9,7 @@ #pragma once #include +#include #include #include #include @@ -21,15 +22,15 @@ namespace AZ //! Structure that is used to cache a timed region into the thread's local storage. struct CachedTimeRegion { - //! Structure that the profiling macro utilizes to create statically initialized instance to create string - //! literals in static memory + //! Structure used internally for caching assumed global string pointers (ideally literals) to the marker group/region + //! NOTE: When used in a separate shared library, the library mustn't be unloaded before the CpuProfiler is shutdown. struct GroupRegionName { GroupRegionName() = delete; GroupRegionName(const char* const group, const char* const region); - - const char* const m_groupName = nullptr; - const char* const m_regionName = nullptr; + + const char* m_groupName = nullptr; + const char* m_regionName = nullptr; struct Hash { @@ -39,31 +40,16 @@ namespace AZ }; CachedTimeRegion() = default; - CachedTimeRegion(const GroupRegionName* groupRegionName); - CachedTimeRegion(const GroupRegionName* groupRegionName, uint16_t stackDepth, uint64_t startTick, uint64_t endTick); + CachedTimeRegion(const GroupRegionName& groupRegionName); + CachedTimeRegion(const GroupRegionName& groupRegionName, uint16_t stackDepth, uint64_t startTick, uint64_t endTick); - //! Pointer to the GroupRegionName static instance. - //! NOTE: When used in a separate shared library, the library mustn't be unloaded before - //! the CpuProfiler is shutdown. - const GroupRegionName* m_groupRegionName = nullptr; + GroupRegionName m_groupRegionName{nullptr, nullptr}; uint16_t m_stackDepth = 0u; AZStd::sys_time_t m_startTick = 0; AZStd::sys_time_t m_endTick = 0; }; - //! Helper class used as a RAII-style mechanism for the macros to begin and end a region. - class TimeRegion : public CachedTimeRegion - { - public: - TimeRegion() = delete; - TimeRegion(const GroupRegionName* groupRegionName); - ~TimeRegion(); - - //! End region - void EndRegion(); - }; - //! Interface class of the CpuProfiler class CpuProfiler { @@ -80,12 +66,6 @@ namespace AZ static CpuProfiler* Get(); - //! Add a new time region - virtual void BeginTimeRegion(TimeRegion& timeRegion) = 0; - - //! Ends a time region - virtual void EndTimeRegion() = 0; - //! Get the last frame's TimeRegionMap virtual const TimeRegionMap& GetTimeRegionMap() const = 0; @@ -101,38 +81,7 @@ namespace AZ virtual void SetProfilerEnabled(bool enabled) = 0; virtual bool IsProfilerEnabled() const = 0 ; - - //! Used by AZ_ATOM_PROFILE_DYNAMIC to create GroupRegionNames with known lifetimes. - virtual const CachedTimeRegion::GroupRegionName& InsertDynamicName(const char* groupName, const AZStd::string& regionName) = 0; }; } // namespace RPI } // namespace AZ - -//! Utility functions for timing a section of code and writing the timing (in cycles) to a new, named time region inside the -//! provided statistics data. - -//! Supply a group and region to the time region -#define AZ_ATOM_PROFILE_TIME_GROUP_REGION(groupName, regionName) \ - static const AZ::RHI::CachedTimeRegion::GroupRegionName AZ_JOIN(groupRegionName, __LINE__)(groupName, regionName); \ - AZ::RHI::TimeRegion AZ_JOIN(timeRegion, __LINE__)(&AZ_JOIN(groupRegionName, __LINE__)); - -//! Supply a region to the time region; "Default" will be used for the group -#define AZ_ATOM_PROFILE_TIME_REGION(regionName) \ - AZ_ATOM_PROFILE_TIME_GROUP_REGION("Default", regionName) - -//! Used to create a time region; "Default" will be used for the group, and __FUNCTION__ macro for the region -#define AZ_ATOM_PROFILE_TIME_FUNCTION() \ - AZ_ATOM_PROFILE_TIME_GROUP_REGION("Default", AZ_FUNCTION_SIGNATURE) - -//! Macro that combines the AZ_TRACE_METHOD with time profiling macro -#define AZ_ATOM_PROFILE_FUNCTION(groupName, regionName) \ - AZ_TRACE_METHOD(); \ - AZ_ATOM_PROFILE_TIME_GROUP_REGION(groupName, regionName) \ - -//! Macro that allows for region names to be submitted at runtime. Use sparingly - this acquires a lock and allocates new objects within a map. -#define AZ_ATOM_PROFILE_DYNAMIC(groupName, regionName) \ - static_assert(AZStd::is_convertible_v, "Runtime group names are not allowed, use a static string literal instead."); \ - const AZ::RHI::CachedTimeRegion::GroupRegionName& AZ_JOIN(groupRegionName, __LINE__) = \ - AZ::RHI::CpuProfiler::Get()->InsertDynamicName(groupName, regionName); \ - AZ::RHI::TimeRegion AZ_JOIN(timeRegion, __LINE__)(&AZ_JOIN(groupRegionName, __LINE__)); diff --git a/Gems/Atom/RHI/Code/Include/Atom/RHI/CpuProfilerImpl.h b/Gems/Atom/RHI/Code/Include/Atom/RHI/CpuProfilerImpl.h index 92b56880b7..29886625ea 100644 --- a/Gems/Atom/RHI/Code/Include/Atom/RHI/CpuProfilerImpl.h +++ b/Gems/Atom/RHI/Code/Include/Atom/RHI/CpuProfilerImpl.h @@ -43,13 +43,13 @@ namespace AZ static constexpr uint32_t TimeRegionStackSize = 2048u; // Adds a region to the stack, gets called each time a region begins - void RegionStackPushBack(TimeRegion& timeRegion); + void RegionStackPushBack(CachedTimeRegion& timeRegion); // Pops a region from the stack, gets called each time a region ends void RegionStackPopBack(); // Add a new cached time region. If the stack is empty, flush all entries to the cached map - void AddCachedRegion(CachedTimeRegion&& timeRegionCached); + void AddCachedRegion(const CachedTimeRegion& timeRegionCached); // Tries to flush the map to the passed parameter, only if the thread's mutex is unlocked void TryFlushCachedMap(CpuProfiler::ThreadTimeRegionMap& cachedRegionMap); @@ -63,7 +63,7 @@ namespace AZ // Use fixed vectors to avoid re-allocating new elements // Keeps track of the regions that added and removed using the macro - AZStd::fixed_vector m_timeRegionStack; + AZStd::fixed_vector m_timeRegionStack; // Keeps track of regions that completed (i.e regions that was pushed and popped from the stack) // Intermediate storage point for the CachedTimeRegions, when the stack is empty, all entries will be @@ -85,7 +85,8 @@ namespace AZ //! forwards the request to profile a region to the appropriate thread. The user is able to request all //! cached regions, which are stored on a per thread frequency. class CpuProfilerImpl final - : public CpuProfiler + : public AZ::Debug::Profiler + , public CpuProfiler , public SystemTickBus::Handler { friend class CpuTimingLocalStorage; @@ -107,16 +108,17 @@ namespace AZ // m_timeRegionMap so that the next frame has up-to-date profiling data. void OnSystemTick() final override; + //! AZ::Debug::Profiler overrides... + void BeginRegion(const AZ::Debug::Budget* budget, const char* eventName) final override; + void EndRegion(const AZ::Debug::Budget* budget) final override; + //! CpuProfiler overrides... - void BeginTimeRegion(TimeRegion& timeRegion) final override; - void EndTimeRegion() final override; const TimeRegionMap& GetTimeRegionMap() const final override; bool BeginContinuousCapture() final override; bool EndContinuousCapture(AZStd::ring_buffer& flushTarget) final override; bool IsContinuousCaptureInProgress() const final override; void SetProfilerEnabled(bool enabled) final override; bool IsProfilerEnabled() const final override; - const CachedTimeRegion::GroupRegionName& InsertDynamicName(const char* groupName, const AZStd::string& regionName) final override; private: static constexpr AZStd::size_t MaxFramesToSave = 2 * 60 * 120; // 2 minutes of 120fps @@ -133,15 +135,6 @@ namespace AZ AZStd::vector, AZ::OSStdAllocator> m_registeredThreads; AZStd::mutex m_threadRegisterMutex; - // Pool for GroupRegionNames that are generated at runtime through AZ_ATOM_PROFILE_DYNAMIC. Each unique - // combination of group name and region name submitted will be stored in this pool to emulate static lifetime. - AZStd::unordered_set m_dynamicGroupRegionNamePool; - - // String pool for storing region names submitted at runtime. Each call to AZ_ATOM_PROFILE_DYNAMIC will either construct - // a string in this pool or use an already-existing entry. - AZStd::unordered_set m_regionNameStringPool; - AZStd::mutex m_dynamicNameMutex; - // Thread local storage, gets lazily allocated when a thread is created static thread_local CpuTimingLocalStorage* ms_threadLocalStorage; diff --git a/Gems/Atom/RHI/Code/Include/Atom/RHI/MemorySubAllocator.h b/Gems/Atom/RHI/Code/Include/Atom/RHI/MemorySubAllocator.h index a10afd880f..bdeb252174 100644 --- a/Gems/Atom/RHI/Code/Include/Atom/RHI/MemorySubAllocator.h +++ b/Gems/Atom/RHI/Code/Include/Atom/RHI/MemorySubAllocator.h @@ -157,7 +157,7 @@ namespace AZ template void MemorySubAllocator::GarbageCollect() { - AZ_ATOM_PROFILE_FUNCTION("RHI", "MemorySubAllocator: GarbageCollect"); + AZ_PROFILE_SCOPE(RHI, "MemorySubAllocator: GarbageCollect"); for (PageContext& pageContext : m_pageContexts) { pageContext.m_allocator.GarbageCollect(); diff --git a/Gems/Atom/RHI/Code/Include/Atom/RHI/ObjectCollector.h b/Gems/Atom/RHI/Code/Include/Atom/RHI/ObjectCollector.h index cd8a85a385..988416326e 100644 --- a/Gems/Atom/RHI/Code/Include/Atom/RHI/ObjectCollector.h +++ b/Gems/Atom/RHI/Code/Include/Atom/RHI/ObjectCollector.h @@ -173,7 +173,7 @@ namespace AZ template void ObjectCollector::Collect(bool forceFlush) { - AZ_ATOM_PROFILE_FUNCTION("DX12", "ObjectCollector: Collect"); + AZ_PROFILE_SCOPE(RHI, "ObjectCollector: Collect"); m_mutex.lock(); if (m_pendingObjects.size()) { diff --git a/Gems/Atom/RHI/Code/Source/RHI/AsyncWorkQueue.cpp b/Gems/Atom/RHI/Code/Source/RHI/AsyncWorkQueue.cpp index 52021fa736..a076bf3e58 100644 --- a/Gems/Atom/RHI/Code/Source/RHI/AsyncWorkQueue.cpp +++ b/Gems/Atom/RHI/Code/Source/RHI/AsyncWorkQueue.cpp @@ -128,7 +128,7 @@ namespace AZ return; } - AZ_PROFILE_FUNCTION(RHI); + AZ_PROFILE_SCOPE(RHI, "AsyncWorkQueue: WaitToFinish"); AZStd::unique_lock lock(m_waitWorkItemMutex); m_waitWorkItemCondition.wait(lock, [&]() {return HasFinishedWork(workHandle); }); diff --git a/Gems/Atom/RHI/Code/Source/RHI/BufferPool.cpp b/Gems/Atom/RHI/Code/Source/RHI/BufferPool.cpp index 9849db254e..5d06d02668 100644 --- a/Gems/Atom/RHI/Code/Source/RHI/BufferPool.cpp +++ b/Gems/Atom/RHI/Code/Source/RHI/BufferPool.cpp @@ -163,7 +163,7 @@ namespace AZ return ResultCode::InvalidArgument; } - AZ_ATOM_PROFILE_FUNCTION("RHI", "BufferPool::OrphanBuffer"); + AZ_PROFILE_SCOPE(RHI, "BufferPool::OrphanBuffer"); return OrphanBufferInternal(buffer); } diff --git a/Gems/Atom/RHI/Code/Source/RHI/CommandQueue.cpp b/Gems/Atom/RHI/Code/Source/RHI/CommandQueue.cpp index 15621c7f2b..0983d4db22 100644 --- a/Gems/Atom/RHI/Code/Source/RHI/CommandQueue.cpp +++ b/Gems/Atom/RHI/Code/Source/RHI/CommandQueue.cpp @@ -22,7 +22,7 @@ namespace AZ ResultCode CommandQueue::Init(Device& device, const CommandQueueDescriptor& descriptor) { - AZ_PROFILE_FUNCTION(RHI); + AZ_PROFILE_SCOPE(RHI, "CommandQueue: Init"); #if defined (AZ_RHI_ENABLE_VALIDATION) if (IsInitialized()) @@ -83,7 +83,7 @@ namespace AZ void CommandQueue::FlushCommands() { - AZ_ATOM_PROFILE_FUNCTION("RHI", "CommandQueue: FlushCommands"); + AZ_PROFILE_SCOPE(RHI, "CommandQueue: FlushCommands"); while (!m_isWorkQueueEmpty && !m_isQuitting) { AZStd::this_thread::yield(); diff --git a/Gems/Atom/RHI/Code/Source/RHI/CpuProfilerImpl.cpp b/Gems/Atom/RHI/Code/Source/RHI/CpuProfilerImpl.cpp index e16b89b5cd..1bc17adb22 100644 --- a/Gems/Atom/RHI/Code/Source/RHI/CpuProfilerImpl.cpp +++ b/Gems/Atom/RHI/Code/Source/RHI/CpuProfilerImpl.cpp @@ -27,38 +27,14 @@ namespace AZ return Interface::Get(); } - // --- TimeRegion --- - - TimeRegion::TimeRegion(const GroupRegionName* groupRegionName) : - CachedTimeRegion(groupRegionName) - { - if (CpuProfiler::Get()) - { - CpuProfiler::Get()->BeginTimeRegion(*this); - } - } - - TimeRegion::~TimeRegion() - { - EndRegion(); - } - - void TimeRegion::EndRegion() - { - if (CpuProfiler::Get()) - { - CpuProfiler::Get()->EndTimeRegion(); - } - } - // --- CachedTimeRegion --- - CachedTimeRegion::CachedTimeRegion(const GroupRegionName* groupRegionName) + CachedTimeRegion::CachedTimeRegion(const GroupRegionName& groupRegionName) { m_groupRegionName = groupRegionName; } - CachedTimeRegion::CachedTimeRegion(const GroupRegionName* groupRegionName, uint16_t stackDepth, uint64_t startTick, uint64_t endTick) + CachedTimeRegion::CachedTimeRegion(const GroupRegionName& groupRegionName, uint16_t stackDepth, uint64_t startTick, uint64_t endTick) { m_groupRegionName = groupRegionName; m_stackDepth = stackDepth; @@ -92,6 +68,7 @@ namespace AZ void CpuProfilerImpl::Init() { + Interface::Register(this); Interface::Register(this); m_initialized = true; SystemTickBus::Handler::BusConnect(); @@ -106,6 +83,7 @@ namespace AZ } // When this call is made, no more thread profiling calls can be performed anymore Interface::Unregister(this); + Interface::Unregister(this); // Wait for the remaining threads that might still be processing its profiling calls AZStd::unique_lock shutdownLock(m_shutdownMutex); @@ -121,7 +99,7 @@ namespace AZ SystemTickBus::Handler::BusDisconnect(); } - void CpuProfilerImpl::BeginTimeRegion(TimeRegion& timeRegion) + void CpuProfilerImpl::BeginRegion(const AZ::Debug::Budget* budget, const char* eventName) { // Try to lock here, the shutdownMutex will only be contested when the CpuProfiler is shutting down. if (m_shutdownMutex.try_lock_shared()) @@ -132,6 +110,7 @@ namespace AZ RegisterThreadStorage(); // Push it to the stack + CachedTimeRegion timeRegion({budget->Name(), eventName}); ms_threadLocalStorage->RegionStackPushBack(timeRegion); } @@ -139,11 +118,12 @@ namespace AZ } } - void CpuProfilerImpl::EndTimeRegion() + void CpuProfilerImpl::EndRegion([[maybe_unused]] const AZ::Debug::Budget* budget) { // Try to lock here, the shutdownMutex will only be contested when the CpuProfiler is shutting down. if (m_shutdownMutex.try_lock_shared()) { + // guard against enabling mid-marker if (m_enabled && ms_threadLocalStorage != nullptr) { ms_threadLocalStorage->RegionStackPopBack(); @@ -232,19 +212,6 @@ namespace AZ return m_enabled; } - const CachedTimeRegion::GroupRegionName& CpuProfilerImpl::InsertDynamicName(const char* groupName, const AZStd::string& regionName) - { - AZStd::scoped_lock lock(m_dynamicNameMutex); - AZ_Warning("CpuProfiler", m_regionNameStringPool.size() < MaxRegionStringPoolSize, - "Stored dynamic region names are accumulating. Consider removing a AZ_ATOM_PROFILE_DYNAMIC invocation."); - auto [regionNameItr, wasRegionInserted] = m_regionNameStringPool.insert(regionName); - - CachedTimeRegion::GroupRegionName newGroupRegionName(groupName, regionNameItr->c_str()); - auto [groupRegionNameItr, wasGroupRegionInserted] = m_dynamicGroupRegionNamePool.insert(newGroupRegionName); - - return *groupRegionNameItr; - } - void CpuProfilerImpl::OnSystemTick() { if (!m_enabled) @@ -307,7 +274,7 @@ namespace AZ m_deleteFlag = true; } - void CpuTimingLocalStorage::RegionStackPushBack(TimeRegion& timeRegion) + void CpuTimingLocalStorage::RegionStackPushBack(CachedTimeRegion& timeRegion) { // If it was (re)enabled, clear the lists first if (m_clearContainers) @@ -323,13 +290,13 @@ namespace AZ timeRegion.m_stackDepth = static_cast(m_stackLevel); AZ_Assert(m_timeRegionStack.size() < TimeRegionStackSize, "Adding too many time regions to the stack. Increase the size of TimeRegionStackSize."); - m_timeRegionStack.push_back(&timeRegion); + m_timeRegionStack.push_back(timeRegion); // Increment the stack m_stackLevel++; // Set the starting time at the end, to avoid recording the minor overhead - timeRegion.m_startTick = AZStd::GetTimeNowTicks(); + m_timeRegionStack.back().m_startTick = AZStd::GetTimeNowTicks(); } void CpuTimingLocalStorage::RegionStackPopBack() @@ -344,23 +311,23 @@ namespace AZ const AZStd::sys_time_t endRegionTime = AZStd::GetTimeNowTicks(); AZ_Assert(!m_timeRegionStack.empty(), "Trying to pop an element in the stack, but it's empty."); - TimeRegion* back = m_timeRegionStack.back(); + CachedTimeRegion back = m_timeRegionStack.back(); m_timeRegionStack.pop_back(); // Set the ending time - back->m_endTick = endRegionTime; + back.m_endTick = endRegionTime; // Decrement the stack m_stackLevel--; // Add an entry to the cached region - AddCachedRegion(CachedTimeRegion(back->m_groupRegionName, back->m_stackDepth, back->m_startTick, back->m_endTick)); + AddCachedRegion(back); } // Gets called when region ends and all data is set - void CpuTimingLocalStorage::AddCachedRegion(CachedTimeRegion&& timeRegionCached) + void CpuTimingLocalStorage::AddCachedRegion(const CachedTimeRegion& timeRegionCached) { - if (m_hitSizeLimitMap[timeRegionCached.m_groupRegionName->m_regionName]) + if (m_hitSizeLimitMap[timeRegionCached.m_groupRegionName.m_regionName]) { return; } @@ -379,12 +346,12 @@ namespace AZ // Add the cached regions to the map for (auto& cachedTimeRegion : m_cachedTimeRegions) { - const AZStd::string regionName = cachedTimeRegion.m_groupRegionName->m_regionName; + const AZStd::string regionName = cachedTimeRegion.m_groupRegionName.m_regionName; AZStd::vector& regionVec = m_cachedTimeRegionMap[regionName]; regionVec.push_back(cachedTimeRegion); if (regionVec.size() >= TimeRegionStackSize) { - m_hitSizeLimitMap[cachedTimeRegion.m_groupRegionName->m_regionName] = true; + m_hitSizeLimitMap.insert_or_assign(AZStd::move(regionName), true); } } @@ -448,8 +415,8 @@ namespace AZ CpuProfilingStatisticsSerializer::CpuProfilingStatisticsSerializerEntry::CpuProfilingStatisticsSerializerEntry( const RHI::CachedTimeRegion& cachedTimeRegion, AZStd::thread_id threadId) { - m_groupName = cachedTimeRegion.m_groupRegionName->m_groupName; - m_regionName = cachedTimeRegion.m_groupRegionName->m_regionName; + m_groupName = cachedTimeRegion.m_groupRegionName.m_groupName; + m_regionName = cachedTimeRegion.m_groupRegionName.m_regionName; m_stackDepth = cachedTimeRegion.m_stackDepth; m_startTick = cachedTimeRegion.m_startTick; m_endTick = cachedTimeRegion.m_endTick; diff --git a/Gems/Atom/RHI/Code/Source/RHI/Device.cpp b/Gems/Atom/RHI/Code/Source/RHI/Device.cpp index 3af09717df..9453ff79ee 100644 --- a/Gems/Atom/RHI/Code/Source/RHI/Device.cpp +++ b/Gems/Atom/RHI/Code/Source/RHI/Device.cpp @@ -128,7 +128,7 @@ namespace AZ { if (ValidateIsInitialized() && ValidateIsInFrame()) { - AZ_ATOM_PROFILE_FUNCTION("RHI", "Device: EndFrame"); + AZ_PROFILE_SCOPE(RHI, "Device: EndFrame"); EndFrameInternal(); m_isInFrame = false; return ResultCode::Success; @@ -150,7 +150,7 @@ namespace AZ { if (ValidateIsInitialized() && ValidateIsNotInFrame()) { - AZ_ATOM_PROFILE_FUNCTION("RHI", "Device: CompileMemoryStatistics"); + AZ_PROFILE_SCOPE(RHI, "Device: CompileMemoryStatistics"); MemoryStatisticsBuilder builder; builder.Begin(memoryStatistics, reportFlags); CompileMemoryStatisticsInternal(builder); diff --git a/Gems/Atom/RHI/Code/Source/RHI/Fence.cpp b/Gems/Atom/RHI/Code/Source/RHI/Fence.cpp index b35260caca..ca0493a52c 100644 --- a/Gems/Atom/RHI/Code/Source/RHI/Fence.cpp +++ b/Gems/Atom/RHI/Code/Source/RHI/Fence.cpp @@ -81,7 +81,7 @@ namespace AZ return ResultCode::InvalidOperation; } - AZ_PROFILE_FUNCTION(RHI); + AZ_PROFILE_SCOPE(RHI, "Fence: WaitOnCpu"); WaitOnCpuInternal(); return ResultCode::Success; } diff --git a/Gems/Atom/RHI/Code/Source/RHI/FrameGraph.cpp b/Gems/Atom/RHI/Code/Source/RHI/FrameGraph.cpp index ff6ecb4df5..b85fa88f34 100644 --- a/Gems/Atom/RHI/Code/Source/RHI/FrameGraph.cpp +++ b/Gems/Atom/RHI/Code/Source/RHI/FrameGraph.cpp @@ -73,7 +73,7 @@ namespace AZ void FrameGraph::Clear() { - AZ_ATOM_PROFILE_FUNCTION("RHI", "FrameGraph: Clear"); + AZ_PROFILE_SCOPE(RHI, "FrameGraph: Clear"); for (Scope* scope : m_scopes) { scope->Deactivate(); @@ -126,7 +126,7 @@ namespace AZ ResultCode FrameGraph::End() { - AZ_ATOM_PROFILE_FUNCTION("RHI", "FrameGraph: End"); + AZ_PROFILE_SCOPE(RHI, "FrameGraph: End"); ResultCode resultCode = ValidateEnd(); if (resultCode != ResultCode::Success) { diff --git a/Gems/Atom/RHI/Code/Source/RHI/FrameGraphCompiler.cpp b/Gems/Atom/RHI/Code/Source/RHI/FrameGraphCompiler.cpp index c4204a4a90..5c5691d975 100644 --- a/Gems/Atom/RHI/Code/Source/RHI/FrameGraphCompiler.cpp +++ b/Gems/Atom/RHI/Code/Source/RHI/FrameGraphCompiler.cpp @@ -121,7 +121,7 @@ namespace AZ */ MessageOutcome FrameGraphCompiler::Compile(const FrameGraphCompileRequest& request) { - AZ_ATOM_PROFILE_FUNCTION("RHI", "FrameGraphCompiler: Compile"); + AZ_PROFILE_SCOPE(RHI, "FrameGraphCompiler: Compile"); MessageOutcome outcome = ValidateCompileRequest(request); if (!outcome) @@ -146,7 +146,7 @@ namespace AZ /// [Phase 4] Compile platform-specific scope data after all attachments and views have been compiled. { - AZ_ATOM_PROFILE_FUNCTION("RHI", "FrameGraphCompiler: Scope Compile"); + AZ_PROFILE_SCOPE(RHI, "FrameGraphCompiler: Scope Compile"); for (Scope* scope : frameGraph.GetScopes()) { @@ -162,7 +162,7 @@ namespace AZ FrameGraph& frameGraph, FrameSchedulerCompileFlags compileFlags) { - AZ_ATOM_PROFILE_FUNCTION("RHI", "FrameGraphCompiler: CompileQueueCentricScopeGraph"); + AZ_PROFILE_SCOPE(RHI, "FrameGraphCompiler: CompileQueueCentricScopeGraph"); const bool disableAsyncQueues = CheckBitsAll(compileFlags, FrameSchedulerCompileFlags::DisableAsyncQueues); if (disableAsyncQueues) @@ -480,7 +480,7 @@ namespace AZ return; } - AZ_ATOM_PROFILE_FUNCTION("RHI", "FrameGraphCompiler: CompileTransientAttachments"); + AZ_PROFILE_SCOPE(RHI, "FrameGraphCompiler: CompileTransientAttachments"); ExtendTransientAttachmentAsyncQueueLifetimes(frameGraph, compileFlags); @@ -769,7 +769,7 @@ namespace AZ void FrameGraphCompiler::CompileResourceViews(const FrameGraphAttachmentDatabase& attachmentDatabase) { - AZ_ATOM_PROFILE_FUNCTION("RHI", "FrameGraphCompiler: CompileResourceViews"); + AZ_PROFILE_SCOPE(RHI, "FrameGraphCompiler: CompileResourceViews"); for (ImageFrameAttachment* imageAttachment : attachmentDatabase.GetImageAttachments()) { diff --git a/Gems/Atom/RHI/Code/Source/RHI/FrameGraphExecuter.cpp b/Gems/Atom/RHI/Code/Source/RHI/FrameGraphExecuter.cpp index 342e537993..429081cd2c 100644 --- a/Gems/Atom/RHI/Code/Source/RHI/FrameGraphExecuter.cpp +++ b/Gems/Atom/RHI/Code/Source/RHI/FrameGraphExecuter.cpp @@ -71,14 +71,13 @@ namespace AZ void FrameGraphExecuter::Begin(const FrameGraph& frameGraph) { - AZ_TRACE_METHOD(); - AZ_ATOM_PROFILE_FUNCTION("RHI", "FrameGraphExecuter: Begin"); + AZ_PROFILE_SCOPE(RHI, "FrameGraphExecuter: Begin"); BeginInternal(frameGraph); } void FrameGraphExecuter::End() { - AZ_ATOM_PROFILE_FUNCTION("RHI", "FrameGraphExecuter: End"); + AZ_PROFILE_SCOPE(RHI, "FrameGraphExecuter: End"); AZ_Assert(m_pendingGroups.empty(), "Pending contexts in queue."); m_groups.clear(); EndInternal(); diff --git a/Gems/Atom/RHI/Code/Source/RHI/FrameScheduler.cpp b/Gems/Atom/RHI/Code/Source/RHI/FrameScheduler.cpp index fe69f56856..90218709fa 100644 --- a/Gems/Atom/RHI/Code/Source/RHI/FrameScheduler.cpp +++ b/Gems/Atom/RHI/Code/Source/RHI/FrameScheduler.cpp @@ -137,7 +137,7 @@ namespace AZ ResultCode FrameScheduler::ImportScopeProducer(ScopeProducer& scopeProducer) { - AZ_PROFILE_FUNCTION(RHI); + AZ_PROFILE_SCOPE(RHI, "FrameScheduler: ImportScopeProducer"); if (!ValidateIsProcessing()) { @@ -171,14 +171,14 @@ namespace AZ MessageOutcome FrameScheduler::Compile(const FrameSchedulerCompileRequest& compileRequest) { - AZ_ATOM_PROFILE_FUNCTION("RHI", "FrameScheduler: Compile"); + AZ_PROFILE_SCOPE(RHI, "FrameScheduler: Compile"); PrepareProducers(); m_compileRequest = compileRequest; { - AZ_ATOM_PROFILE_TIME_GROUP_REGION("RHI", "FrameScheduler: Compile: OnFrameCompile"); + AZ_PROFILE_SCOPE(RHI, "FrameScheduler: Compile: OnFrameCompile"); FrameEventBus::Broadcast(&FrameEventBus::Events::OnFrameCompile); } @@ -193,7 +193,7 @@ namespace AZ if (outcome.IsSuccess()) { { - AZ_ATOM_PROFILE_TIME_GROUP_REGION("RHI", "FrameScheduler: Compile: OnFrameCompileEnd"); + AZ_PROFILE_SCOPE(RHI, "FrameScheduler: Compile: OnFrameCompileEnd"); FrameEventBus::Broadcast(&FrameEventBus::Events::OnFrameCompileEnd, *m_frameGraph); } @@ -216,8 +216,7 @@ namespace AZ void FrameScheduler::PrepareProducers() { - AZ_PROFILE_FUNCTION(RHI); - AZ_ATOM_PROFILE_FUNCTION("RHI", "FrameScheduler: PrepareProducers"); + AZ_PROFILE_SCOPE(RHI, "FrameScheduler: PrepareProducers"); for (ScopeProducer* scopeProducer : m_scopeProducers) { @@ -237,8 +236,7 @@ namespace AZ void FrameScheduler::CompileProducers() { - AZ_PROFILE_FUNCTION(RHI); - AZ_ATOM_PROFILE_FUNCTION("RHI", "FrameScheduler: CompileProducers"); + AZ_PROFILE_SCOPE(RHI, "FrameScheduler: CompileProducers"); for (ScopeProducer* scopeProducer : m_scopeProducers) { @@ -249,8 +247,7 @@ namespace AZ void FrameScheduler::CompileShaderResourceGroups() { - AZ_PROFILE_FUNCTION(RHI); - AZ_ATOM_PROFILE_FUNCTION("RHI", "FrameScheduler: CompileShaderResourceGroups"); + AZ_PROFILE_SCOPE(RHI, "FrameScheduler: CompileShaderResourceGroups"); // Execute all queued resource invalidations, which will mark SRG's for compilation. { @@ -286,7 +283,7 @@ namespace AZ const auto compileGroupsForIntervalLambda = [srgPool, interval]() { - AZ_ATOM_PROFILE_FUNCTION("RHI", "FrameScheduler : compileGroupsForIntervalLambda"); + AZ_PROFILE_SCOPE(RHI, "FrameScheduler : compileGroupsForIntervalLambda"); srgPool->CompileGroupsForInterval(interval); }; @@ -322,8 +319,7 @@ namespace AZ void FrameScheduler::BuildRayTracingShaderTables() { - AZ_PROFILE_FUNCTION(RHI); - AZ_ATOM_PROFILE_FUNCTION("RHI", "FrameScheduler: BuildRayTracingShaderTables"); + AZ_PROFILE_SCOPE(RHI, "FrameScheduler: BuildRayTracingShaderTables"); for (auto rayTracingShaderTable : m_rayTracingShaderTablesToBuild) { @@ -341,8 +337,7 @@ namespace AZ ResultCode FrameScheduler::BeginFrame() { - AZ_PROFILE_FUNCTION(RHI); - AZ_ATOM_PROFILE_FUNCTION("RHI", "FrameScheduler: BeginFrame"); + AZ_PROFILE_SCOPE(RHI, "FrameScheduler: BeginFrame"); if (!ValidateIsInitialized()) { @@ -376,8 +371,7 @@ namespace AZ ResultCode FrameScheduler::EndFrame() { - AZ_PROFILE_FUNCTION(RHI); - AZ_ATOM_PROFILE_FUNCTION("RHI", "FrameScheduler: EndFrame"); + AZ_PROFILE_SCOPE(RHI, "FrameScheduler: EndFrame"); if (Validation::IsEnabled()) { @@ -404,7 +398,7 @@ namespace AZ m_scopeProducerLookup.clear(); { - AZ_ATOM_PROFILE_TIME_GROUP_REGION("RHI", "FrameScheduler: EndFrame: OnFrameEnd"); + AZ_PROFILE_SCOPE(RHI, "FrameScheduler: EndFrame: OnFrameEnd"); FrameEventBus::Event(m_device, &FrameEventBus::Events::OnFrameEnd); } @@ -431,8 +425,7 @@ namespace AZ void FrameScheduler::ExecuteGroupInternal(AZ::Job* parentJob, uint32_t groupIndex) { - AZ_PROFILE_FUNCTION(RHI); - AZ_ATOM_PROFILE_FUNCTION("RHI", "FrameScheduler: ExecuteGroupInternal"); + AZ_PROFILE_SCOPE(RHI, "FrameScheduler: ExecuteGroupInternal"); FrameGraphExecuteGroup* executeGroup = m_frameGraphExecuter->BeginGroup(groupIndex); const uint32_t contextCount = executeGroup->GetContextCount(); @@ -474,8 +467,7 @@ namespace AZ void FrameScheduler::Execute(JobPolicy overrideJobPolicy) { - AZ_PROFILE_FUNCTION(RHI); - AZ_ATOM_PROFILE_FUNCTION("RHI", "FrameScheduler: Execute"); + AZ_PROFILE_SCOPE(RHI, "FrameScheduler: Execute"); const uint32_t groupCount = m_frameGraphExecuter->GetGroupCount(); const JobPolicy platformJobPolicy = m_frameGraphExecuter->GetJobPolicy(); diff --git a/Gems/Atom/RHI/Code/Source/RHI/PipelineStateCache.cpp b/Gems/Atom/RHI/Code/Source/RHI/PipelineStateCache.cpp index 6e98456933..868580f0c7 100644 --- a/Gems/Atom/RHI/Code/Source/RHI/PipelineStateCache.cpp +++ b/Gems/Atom/RHI/Code/Source/RHI/PipelineStateCache.cpp @@ -212,7 +212,7 @@ namespace AZ void PipelineStateCache::Compact() { - AZ_ATOM_PROFILE_FUNCTION("RHI", "PipelineStateCache: Compact"); + AZ_PROFILE_SCOPE(RHI, "PipelineStateCache: Compact"); AZStd::unique_lock lock(m_mutex); // Merge the pending cache into the read-only cache. diff --git a/Gems/Atom/RHI/Code/Source/RHI/RHISystem.cpp b/Gems/Atom/RHI/Code/Source/RHI/RHISystem.cpp index a381209d2b..69f7002435 100644 --- a/Gems/Atom/RHI/Code/Source/RHI/RHISystem.cpp +++ b/Gems/Atom/RHI/Code/Source/RHI/RHISystem.cpp @@ -187,8 +187,7 @@ namespace AZ void RHISystem::FrameUpdate(FrameGraphCallback frameGraphCallback) { - AZ_PROFILE_FUNCTION(RHI); - AZ_ATOM_PROFILE_FUNCTION("RHI", "RHISystem: FrameUpdate"); + AZ_PROFILE_SCOPE(RHI, "RHISystem: FrameUpdate"); { AZ_PROFILE_SCOPE(RHI, "main per-frame work"); @@ -201,7 +200,7 @@ namespace AZ * own RHI scopes to the frame scheduler. This happens prior to the RPI pass graph registration. */ { - AZ_ATOM_PROFILE_TIME_GROUP_REGION("RHI", "RHISystem: FrameUpdate: OnFramePrepare"); + AZ_PROFILE_SCOPE(RHI, "RHISystem: FrameUpdate: OnFramePrepare"); RHISystemNotificationBus::Broadcast(&RHISystemNotificationBus::Events::OnFramePrepare, m_frameScheduler); } diff --git a/Gems/Atom/RHI/DX12/Code/Source/RHI/AsyncUploadQueue.cpp b/Gems/Atom/RHI/DX12/Code/Source/RHI/AsyncUploadQueue.cpp index 29b210e17e..cb9dac3864 100644 --- a/Gems/Atom/RHI/DX12/Code/Source/RHI/AsyncUploadQueue.cpp +++ b/Gems/Atom/RHI/DX12/Code/Source/RHI/AsyncUploadQueue.cpp @@ -196,7 +196,7 @@ namespace AZ AsyncUploadQueue::FramePacket* AsyncUploadQueue::BeginFramePacket() { - AZ_PROFILE_FUNCTION(RHI); + AZ_PROFILE_SCOPE(RHI, "AsyncUploadQueue: BeginFramePacket"); AZ_Assert(!m_recordingFrame, "The previous frame packet isn't ended"); FramePacket* framePacket = &m_framePackets[m_frameIndex]; @@ -212,7 +212,7 @@ namespace AZ void AsyncUploadQueue::EndFramePacket(ID3D12CommandQueue* commandQueue) { - AZ_PROFILE_FUNCTION(RHI); + AZ_PROFILE_SCOPE(RHI, "AsyncUploadQueue: EndFramePacket"); AZ_Assert(m_recordingFrame, "The frame packet wasn't started. You need to call StartFramePacket first."); AssertSuccess(m_commandList->Close()); @@ -229,7 +229,7 @@ namespace AZ // [GFX TODO][ATOM-4205] Stage/Upload 3D streaming images more efficiently. uint64_t AsyncUploadQueue::QueueUpload(const RHI::StreamingImageExpandRequest& request, uint32_t residentMip) { - AZ_PROFILE_FUNCTION(RHI); + AZ_PROFILE_SCOPE(RHI, "AsyncUploadQueue: QueueUpload"); uint64_t fenceValue = m_uploadFence.Increment(); @@ -475,7 +475,7 @@ namespace AZ void AsyncUploadQueue::WaitForUpload(uint64_t fenceValue) { - AZ_PROFILE_FUNCTION(RHI); + AZ_PROFILE_SCOPE(RHI, "AsyncUploadQueue: WaitForUpload"); if (!IsUploadFinished(fenceValue)) { @@ -489,7 +489,7 @@ namespace AZ void AsyncUploadQueue::ProcessCallbacks(uint64_t fenceValue) { - AZ_PROFILE_FUNCTION(RHI); + AZ_PROFILE_SCOPE(RHI, "AsyncUploadQueue: ProcessCallbacks"); AZStd::lock_guard lock(m_callbackMutex); while (m_callbacks.size() > 0 && m_callbacks.front().second <= fenceValue) { diff --git a/Gems/Atom/RHI/DX12/Code/Source/RHI/CommandListBase.cpp b/Gems/Atom/RHI/DX12/Code/Source/RHI/CommandListBase.cpp index c5c71a9f43..efa9518ee1 100644 --- a/Gems/Atom/RHI/DX12/Code/Source/RHI/CommandListBase.cpp +++ b/Gems/Atom/RHI/DX12/Code/Source/RHI/CommandListBase.cpp @@ -33,7 +33,7 @@ namespace AZ void CommandListBase::Reset(ID3D12CommandAllocator* commandAllocator) { - AZ_PROFILE_FUNCTION(RHI); + AZ_PROFILE_SCOPE(RHI, "CommandListBase: Reset"); AZ_Assert(m_queuedBarriers.empty(), "Unflushed barriers in command list."); m_commandList->Reset(commandAllocator, nullptr); @@ -95,7 +95,7 @@ namespace AZ { if (m_queuedBarriers.size()) { - AZ_PROFILE_FUNCTION(RHI); + AZ_PROFILE_SCOPE(RHI, "CommandListBase: FlushBarriers"); m_commandList->ResourceBarrier((UINT)m_queuedBarriers.size(), m_queuedBarriers.data()); m_queuedBarriers.clear(); diff --git a/Gems/Atom/RHI/DX12/Code/Source/RHI/CommandListPool.cpp b/Gems/Atom/RHI/DX12/Code/Source/RHI/CommandListPool.cpp index c31fe8ada8..94262064da 100644 --- a/Gems/Atom/RHI/DX12/Code/Source/RHI/CommandListPool.cpp +++ b/Gems/Atom/RHI/DX12/Code/Source/RHI/CommandListPool.cpp @@ -175,7 +175,7 @@ namespace AZ void CommandListAllocator::Collect() { - AZ_ATOM_PROFILE_FUNCTION("DX12", "CommandListAllocator: Collect"); + AZ_PROFILE_SCOPE(RHI, "CommandListAllocator: Collect(DX12)"); for (uint32_t queueIdx = 0; queueIdx < RHI::HardwareQueueClassCount; ++queueIdx) { m_commandListSubAllocators[queueIdx].ForEach([](Internal::CommandListSubAllocator& commandListSubAllocator) diff --git a/Gems/Atom/RHI/DX12/Code/Source/RHI/CommandQueue.cpp b/Gems/Atom/RHI/DX12/Code/Source/RHI/CommandQueue.cpp index 9d58217f01..0cde6aeb09 100644 --- a/Gems/Atom/RHI/DX12/Code/Source/RHI/CommandQueue.cpp +++ b/Gems/Atom/RHI/DX12/Code/Source/RHI/CommandQueue.cpp @@ -195,7 +195,7 @@ namespace AZ void CommandQueue::UpdateTileMappings(CommandList& commandList) { - AZ_PROFILE_FUNCTION(RHI); + AZ_PROFILE_SCOPE(RHI, "CommandQueue: UpdateTileMappings"); for (const CommandList::TileMapRequest& request : commandList.GetTileMapRequests()) { const uint32_t tileCount = request.m_sourceRegionSize.NumTiles; @@ -229,7 +229,7 @@ namespace AZ void CommandQueue::WaitForIdle() { - AZ_PROFILE_FUNCTION(RHI); + AZ_PROFILE_SCOPE(RHI, "CommandQueue: WaitForIdle"); Fence fence; fence.Init(m_device.get(), RHI::FenceState::Reset); diff --git a/Gems/Atom/RHI/DX12/Code/Source/RHI/CommandQueueContext.cpp b/Gems/Atom/RHI/DX12/Code/Source/RHI/CommandQueueContext.cpp index 96d621df59..eee137026d 100644 --- a/Gems/Atom/RHI/DX12/Code/Source/RHI/CommandQueueContext.cpp +++ b/Gems/Atom/RHI/DX12/Code/Source/RHI/CommandQueueContext.cpp @@ -101,7 +101,7 @@ namespace AZ void CommandQueueContext::WaitForIdle() { - AZ_PROFILE_FUNCTION(RHI); + AZ_PROFILE_SCOPE(RHI, "CommandQueueContext: WaitForIdle"); for (uint32_t hardwareQueueIdx = 0; hardwareQueueIdx < RHI::HardwareQueueClassCount; ++hardwareQueueIdx) { if (m_commandQueues[hardwareQueueIdx]) @@ -113,7 +113,7 @@ namespace AZ void CommandQueueContext::Begin() { - AZ_PROFILE_FUNCTION(RHI); + AZ_PROFILE_SCOPE(RHI, "CommandQueueContext: Begin"); { AZ_PROFILE_SCOPE(RHI, "Clearing Command Queue Timers"); @@ -131,8 +131,7 @@ namespace AZ void CommandQueueContext::End() { - AZ_PROFILE_FUNCTION(RHI); - AZ_ATOM_PROFILE_FUNCTION("DX12", "CommandQueueContext: End"); + AZ_PROFILE_SCOPE(RHI, "CommandQueueContext: End"); QueueGpuSignals(m_frameFences[m_currentFrameIndex]); @@ -146,7 +145,6 @@ namespace AZ { AZ_PROFILE_SCOPE(RHI, "Wait and Reset Fence"); - AZ_ATOM_PROFILE_TIME_GROUP_REGION("DX12", "CommandQueueContext: Wait on Fences"); FenceEvent event("FrameFence"); m_frameFences[m_currentFrameIndex].Wait(event); diff --git a/Gems/Atom/RHI/DX12/Code/Source/RHI/DescriptorContext.cpp b/Gems/Atom/RHI/DX12/Code/Source/RHI/DescriptorContext.cpp index 942b514b97..816b904e16 100644 --- a/Gems/Atom/RHI/DX12/Code/Source/RHI/DescriptorContext.cpp +++ b/Gems/Atom/RHI/DX12/Code/Source/RHI/DescriptorContext.cpp @@ -341,7 +341,7 @@ namespace AZ void DescriptorContext::GarbageCollect() { - AZ_ATOM_PROFILE_FUNCTION("DX12", "DescriptorContext: GarbageCollect"); + AZ_PROFILE_SCOPE(RHI, "DescriptorContext: GarbageCollect(DX12)"); for (const auto& itr : m_platformLimitsDescriptor->m_descriptorHeapLimits) { for (uint32_t shaderVisibleIdx = 0; shaderVisibleIdx < PlatformLimitsDescriptor::NumHeapFlags; ++shaderVisibleIdx) diff --git a/Gems/Atom/RHI/DX12/Code/Source/RHI/FrameGraphCompiler.cpp b/Gems/Atom/RHI/DX12/Code/Source/RHI/FrameGraphCompiler.cpp index 7eaafc3705..722d535cb1 100644 --- a/Gems/Atom/RHI/DX12/Code/Source/RHI/FrameGraphCompiler.cpp +++ b/Gems/Atom/RHI/DX12/Code/Source/RHI/FrameGraphCompiler.cpp @@ -204,7 +204,7 @@ namespace AZ RHI::MessageOutcome FrameGraphCompiler::CompileInternal(const RHI::FrameGraphCompileRequest& request) { - AZ_ATOM_PROFILE_FUNCTION("RHI", "FrameGraphCompiler: CompileInternal(DX12)"); + AZ_PROFILE_SCOPE(RHI, "FrameGraphCompiler: CompileInternal(DX12)"); RHI::FrameGraph& frameGraph = *request.m_frameGraph; @@ -373,7 +373,7 @@ namespace AZ void FrameGraphCompiler::CompileResourceBarriers(Scope* rootScope, const RHI::FrameGraphAttachmentDatabase& attachmentDatabase) { - AZ_ATOM_PROFILE_FUNCTION("RHI", "FrameGraphCompiler: CompileResourceBarriers(DX12)"); + AZ_PROFILE_SCOPE(RHI, "FrameGraphCompiler: CompileResourceBarriers(DX12)"); for (RHI::BufferFrameAttachment* bufferFrameAttachment : attachmentDatabase.GetBufferAttachments()) { @@ -394,7 +394,7 @@ namespace AZ ResourceTransitionLoggerNull logger(bufferFrameAttachment.GetId()); #endif - AZ_ATOM_PROFILE_FUNCTION("RHI", "FrameGraphCompiler: CompileBufferBarriers(DX12)"); + AZ_PROFILE_SCOPE(RHI, "FrameGraphCompiler: CompileBufferBarriers(DX12)"); Buffer& buffer = static_cast(*bufferFrameAttachment.GetBuffer()); RHI::BufferScopeAttachment* scopeAttachment = bufferFrameAttachment.GetFirstScopeAttachment(); @@ -469,7 +469,7 @@ namespace AZ ResourceTransitionLoggerNull logger(imageFrameAttachment.GetId()); #endif - AZ_ATOM_PROFILE_FUNCTION("RHI", "FrameGraphCompiler: CompileImageBarriers (DX12)"); + AZ_PROFILE_SCOPE(RHI, "FrameGraphCompiler: CompileImageBarriers (DX12)"); Image& image = static_cast(*imageFrameAttachment.GetImage()); RHI::ImageScopeAttachment* scopeAttachment = imageFrameAttachment.GetFirstScopeAttachment(); diff --git a/Gems/Atom/RHI/DX12/Code/Source/RHI/StagingMemoryAllocator.cpp b/Gems/Atom/RHI/DX12/Code/Source/RHI/StagingMemoryAllocator.cpp index 1a32f06fa2..337b25a793 100644 --- a/Gems/Atom/RHI/DX12/Code/Source/RHI/StagingMemoryAllocator.cpp +++ b/Gems/Atom/RHI/DX12/Code/Source/RHI/StagingMemoryAllocator.cpp @@ -55,7 +55,7 @@ namespace AZ void StagingMemoryAllocator::GarbageCollect() { - AZ_ATOM_PROFILE_FUNCTION("DX12", "StagingMemoryAllocator: GarbageCollect"); + AZ_PROFILE_SCOPE(RHI, "StagingMemoryAllocator: GarbageCollect(DX12)"); m_mediumBlockAllocators.ForEach([](MemoryLinearSubAllocator& subAllocator) { subAllocator.GarbageCollect(); diff --git a/Gems/Atom/RHI/Metal/Code/Source/RHI/CommandQueueContext.cpp b/Gems/Atom/RHI/Metal/Code/Source/RHI/CommandQueueContext.cpp index 7f0c535383..fb11700e04 100644 --- a/Gems/Atom/RHI/Metal/Code/Source/RHI/CommandQueueContext.cpp +++ b/Gems/Atom/RHI/Metal/Code/Source/RHI/CommandQueueContext.cpp @@ -79,8 +79,8 @@ namespace AZ void CommandQueueContext::End() { - AZ_PROFILE_FUNCTION(RHI); - + AZ_PROFILE_SCOPE(RHI, "CommandQueueContext: End"); + QueueGpuSignals(m_frameFences[m_currentFrameIndex]); for (uint32_t hardwareQueueIdx = 0; hardwareQueueIdx < RHI::HardwareQueueClassCount; ++hardwareQueueIdx) { @@ -92,7 +92,6 @@ namespace AZ { AZ_PROFILE_SCOPE(RHI, "Wait and Reset Fence"); - AZ_ATOM_PROFILE_TIME_GROUP_REGION("RHI", "CommandQueueContext: Wait on Fences"); //Synchronize the CPU with the GPU by waiting on the fence until signalled by the GPU. CPU can only go upto //RHI::Limits::Device::FrameCountMax frames ahead of the GPU diff --git a/Gems/Atom/RHI/Metal/Code/Source/RHI/FrameGraphCompiler.cpp b/Gems/Atom/RHI/Metal/Code/Source/RHI/FrameGraphCompiler.cpp index 520e417f6b..a347ed9b12 100644 --- a/Gems/Atom/RHI/Metal/Code/Source/RHI/FrameGraphCompiler.cpp +++ b/Gems/Atom/RHI/Metal/Code/Source/RHI/FrameGraphCompiler.cpp @@ -33,7 +33,7 @@ namespace AZ RHI::MessageOutcome FrameGraphCompiler::CompileInternal(const RHI::FrameGraphCompileRequest& request) { - AZ_ATOM_PROFILE_FUNCTION("RHI", "FrameGraphCompiler: CompileInternal(Metal)"); + AZ_PROFILE_SCOPE(RHI, "FrameGraphCompiler: CompileInternal(Metal)"); RHI::FrameGraph& frameGraph = *request.m_frameGraph; if (!RHI::CheckBitsAny(request.m_compileFlags, RHI::FrameSchedulerCompileFlags::DisableAsyncQueues)) { diff --git a/Gems/Atom/RHI/Metal/Code/Source/RHI/SwapChain.cpp b/Gems/Atom/RHI/Metal/Code/Source/RHI/SwapChain.cpp index 90ba04c3db..567f89b6e0 100644 --- a/Gems/Atom/RHI/Metal/Code/Source/RHI/SwapChain.cpp +++ b/Gems/Atom/RHI/Metal/Code/Source/RHI/SwapChain.cpp @@ -192,7 +192,7 @@ namespace AZ id SwapChain::RequestDrawable(bool isFrameCaptureEnabled) { - AZ_ATOM_PROFILE_FUNCTION("RHI", "SwapChain::RequestDrawable"); + AZ_PROFILE_SCOPE(RHI, "SwapChain::RequestDrawable"); m_metalView.metalLayer.framebufferOnly = !isFrameCaptureEnabled; const uint32_t currentImageIndex = GetCurrentImageIndex(); if(m_drawables[currentImageIndex]) diff --git a/Gems/Atom/RHI/Vulkan/Code/Source/RHI/AsyncUploadQueue.cpp b/Gems/Atom/RHI/Vulkan/Code/Source/RHI/AsyncUploadQueue.cpp index 7a8499ab9d..2c3958cb65 100644 --- a/Gems/Atom/RHI/Vulkan/Code/Source/RHI/AsyncUploadQueue.cpp +++ b/Gems/Atom/RHI/Vulkan/Code/Source/RHI/AsyncUploadQueue.cpp @@ -451,7 +451,7 @@ namespace AZ AsyncUploadQueue::FramePacket* AsyncUploadQueue::BeginFramePacket(Queue* queue) { - AZ_PROFILE_FUNCTION(RHI); + AZ_PROFILE_SCOPE(RHI, "AsyncUploadQueue: BeginFramePacket"); AZ_Assert(!m_recordingFrame, "The previous frame packet isn't ended."); auto& device = static_cast(GetDevice()); @@ -471,7 +471,7 @@ namespace AZ void AsyncUploadQueue::EndFramePacket(Queue* queue, Semaphore* semaphoreToSignal /*=nullptr*/) { - AZ_PROFILE_FUNCTION(RHI); + AZ_PROFILE_SCOPE(RHI, "AsyncUploadQueue: EndFramePacket"); AZ_Assert(m_recordingFrame, "The frame packet wasn't started. You need to call StartFramePacket first."); m_commandList->EndCommandBuffer(); @@ -636,7 +636,7 @@ namespace AZ void AsyncUploadQueue::ProcessCallback(const RHI::AsyncWorkHandle& handle) { - AZ_PROFILE_FUNCTION(RHI); + AZ_PROFILE_SCOPE(RHI, "AsyncUploadQueue: ProcessCallback"); AZStd::unique_lock lock(m_callbackListMutex); auto findIter = m_callbackList.find(handle); if (findIter != m_callbackList.end()) diff --git a/Gems/Atom/RHI/Vulkan/Code/Source/RHI/CommandQueueContext.cpp b/Gems/Atom/RHI/Vulkan/Code/Source/RHI/CommandQueueContext.cpp index 39f0ac9d58..c0d34e2bf9 100644 --- a/Gems/Atom/RHI/Vulkan/Code/Source/RHI/CommandQueueContext.cpp +++ b/Gems/Atom/RHI/Vulkan/Code/Source/RHI/CommandQueueContext.cpp @@ -42,7 +42,7 @@ namespace AZ void CommandQueueContext::End() { - AZ_PROFILE_FUNCTION(RHI); + AZ_PROFILE_SCOPE(RHI, "CommandQueueContext: End"); for (auto& commandQueue : m_commandQueues) { @@ -55,7 +55,6 @@ namespace AZ { AZ_PROFILE_SCOPE(RHI, "Wait on Fences"); - AZ_ATOM_PROFILE_FUNCTION("RHI", "CommandQueueContext: Wait on Fences"); FencesPerQueue& nextFences = m_frameFences[m_currentFrameIndex]; for (auto& fence : nextFences) @@ -79,7 +78,7 @@ namespace AZ void CommandQueueContext::WaitForIdle() { - AZ_PROFILE_FUNCTION(RHI); + AZ_PROFILE_SCOPE(RHI, "CommandQueueContext: WaitForIdle"); for (auto& commandQueue : m_commandQueues) { commandQueue->WaitForIdle(); diff --git a/Gems/Atom/RHI/Vulkan/Code/Source/RHI/FrameGraphCompiler.cpp b/Gems/Atom/RHI/Vulkan/Code/Source/RHI/FrameGraphCompiler.cpp index 372ebc273d..2a30e36e04 100644 --- a/Gems/Atom/RHI/Vulkan/Code/Source/RHI/FrameGraphCompiler.cpp +++ b/Gems/Atom/RHI/Vulkan/Code/Source/RHI/FrameGraphCompiler.cpp @@ -45,7 +45,7 @@ namespace AZ RHI::MessageOutcome FrameGraphCompiler::CompileInternal(const RHI::FrameGraphCompileRequest& request) { - AZ_ATOM_PROFILE_FUNCTION("RHI", "FrameGraphCompiler: CompileInternal(Vulkan)"); + AZ_PROFILE_SCOPE(RHI, "FrameGraphCompiler: CompileInternal(Vulkan)"); AZ_Assert(request.m_frameGraph, "FrameGraph is null."); RHI::FrameGraph& frameGraph = *request.m_frameGraph; @@ -89,7 +89,7 @@ namespace AZ void FrameGraphCompiler::CompileResourceBarriers(const RHI::FrameGraphAttachmentDatabase& attachmentDatabase) { - AZ_ATOM_PROFILE_FUNCTION("RHI", "FrameGraphCompiler: CompileResourceBarriers(Vulkan)"); + AZ_PROFILE_SCOPE(RHI, "FrameGraphCompiler: CompileResourceBarriers(Vulkan)"); for (RHI::BufferFrameAttachment* bufferFrameAttachment : attachmentDatabase.GetBufferAttachments()) { diff --git a/Gems/Atom/RPI/Code/Source/RPI.Public/Culling.cpp b/Gems/Atom/RPI/Code/Source/RPI.Public/Culling.cpp index bedf4fe989..f67343faa1 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Public/Culling.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Public/Culling.cpp @@ -299,7 +299,7 @@ namespace AZ //work function void Process() override { - AZ_PROFILE_FUNCTION(RPI); + AZ_PROFILE_SCOPE(RPI, "AddObjectsToViewJob: Process"); const View::UsageFlags viewFlags = m_jobData->m_view->GetUsageFlags(); const RHI::DrawListMask drawListMask = m_jobData->m_view->GetDrawListMask(); @@ -645,7 +645,7 @@ namespace AZ uint32_t AddLodDataToView(const Vector3& pos, const Cullable::LodData& lodData, RPI::View& view) { #ifdef AZ_CULL_PROFILE_DETAILED - AZ_PROFILE_FUNCTION(RPI); + AZ_PROFILE_SCOPE(RPI, "AddLodDataToView"); #endif const Matrix4x4& viewToClip = view.GetViewToClipMatrix(); @@ -725,7 +725,7 @@ namespace AZ void CullingScene::BeginCulling(const AZStd::vector& views) { - AZ_ATOM_PROFILE_FUNCTION("RPI", "CullingScene: BeginCulling"); + AZ_PROFILE_SCOPE(RPI, "CullingScene: BeginCulling"); m_cullDataConcurrencyCheck.soft_lock(); m_debugCtx.ResetCullStats(); diff --git a/Gems/Atom/RPI/Code/Source/RPI.Public/GpuQuery/GpuQuerySystem.cpp b/Gems/Atom/RPI/Code/Source/RPI.Public/GpuQuery/GpuQuerySystem.cpp index 257bb689ee..8077038ea7 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Public/GpuQuery/GpuQuerySystem.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Public/GpuQuery/GpuQuerySystem.cpp @@ -75,7 +75,7 @@ namespace AZ void GpuQuerySystem::Update() { - AZ_ATOM_PROFILE_FUNCTION("RPI", "GpuQuerySystem: Update"); + AZ_PROFILE_SCOPE(RPI, "GpuQuerySystem: Update"); for (auto& queryPool : m_queryPoolArray) { if (queryPool) diff --git a/Gems/Atom/RPI/Code/Source/RPI.Public/Image/ImageSystem.cpp b/Gems/Atom/RPI/Code/Source/RPI.Public/Image/ImageSystem.cpp index d4a55d1c29..71e2eaa2f5 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Public/Image/ImageSystem.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Public/Image/ImageSystem.cpp @@ -34,6 +34,8 @@ #include #include +AZ_DECLARE_BUDGET(RPI); + namespace AZ { namespace RPI @@ -171,7 +173,7 @@ namespace AZ void ImageSystem::Update() { - AZ_ATOM_PROFILE_FUNCTION("RPI", "ImageSystem: Update"); + AZ_PROFILE_SCOPE(RPI, "ImageSystem: Update"); AZStd::lock_guard lock(m_activeStreamingPoolMutex); for (StreamingImagePool* imagePool : m_activeStreamingPools) diff --git a/Gems/Atom/RPI/Code/Source/RPI.Public/Model/Model.cpp b/Gems/Atom/RPI/Code/Source/RPI.Public/Model/Model.cpp index 6fd313e27f..50da470ec4 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Public/Model/Model.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Public/Model/Model.cpp @@ -42,7 +42,7 @@ namespace AZ Data::Instance Model::CreateInternal(const Data::Asset& modelAsset) { - AZ_PROFILE_FUNCTION(RPI); + AZ_PROFILE_SCOPE(RPI, "Model: CreateInternal"); Data::Instance model = aznew Model(); const RHI::ResultCode resultCode = model->Init(modelAsset); @@ -56,7 +56,7 @@ namespace AZ RHI::ResultCode Model::Init(const Data::Asset& modelAsset) { - AZ_PROFILE_FUNCTION(RPI); + AZ_PROFILE_SCOPE(RPI, "Model: Init"); m_lods.resize(modelAsset->GetLodAssets().size()); @@ -128,7 +128,7 @@ namespace AZ bool Model::LocalRayIntersection(const AZ::Vector3& rayStart, const AZ::Vector3& rayDir, float& distanceNormalized, AZ::Vector3& normal) const { - AZ_PROFILE_FUNCTION(RPI); + AZ_PROFILE_SCOPE(RPI, "Model: LocalRayIntersection"); if (!GetModelAsset()) { @@ -171,7 +171,7 @@ namespace AZ float& distanceNormalized, AZ::Vector3& normal) const { - AZ_PROFILE_FUNCTION(RPI); + AZ_PROFILE_SCOPE(RPI, "Model: RayIntersection"); const AZ::Vector3 clampedScale = nonUniformScale.GetMax(AZ::Vector3(AZ::MinTransformScale)); const AZ::Transform inverseTM = modelTransform.GetInverse(); diff --git a/Gems/Atom/RPI/Code/Source/RPI.Public/Model/ModelLodUtils.cpp b/Gems/Atom/RPI/Code/Source/RPI.Public/Model/ModelLodUtils.cpp index ef9521d23c..a1651defbc 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Public/Model/ModelLodUtils.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Public/Model/ModelLodUtils.cpp @@ -27,7 +27,7 @@ namespace AZ ModelLodIndex SelectLod(const View* view, const Vector3& position, const Model& model, ModelLodIndex lodOverride) { - AZ_PROFILE_FUNCTION(RPI); + AZ_PROFILE_SCOPE(RPI, "ModelLodUtils: SelectLod"); ModelLodIndex lodIndex; if (model.GetLodCount() == 1) { diff --git a/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/PassSystem.cpp b/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/PassSystem.cpp index a8feeb1047..34d72338dc 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/PassSystem.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/PassSystem.cpp @@ -169,7 +169,7 @@ namespace AZ void PassSystem::RemovePasses() { m_state = PassSystemState::RemovingPasses; - AZ_ATOM_PROFILE_FUNCTION("RPI", "PassSystem: RemovePasses"); + AZ_PROFILE_SCOPE(RPI, "PassSystem: RemovePasses"); if (!m_removePassList.empty()) { @@ -189,8 +189,7 @@ namespace AZ void PassSystem::BuildPasses() { m_state = PassSystemState::BuildingPasses; - AZ_PROFILE_FUNCTION(RPI); - AZ_ATOM_PROFILE_FUNCTION("RPI", "PassSystem: BuildPassAttachments"); + AZ_PROFILE_SCOPE(RPI, "PassSystem: BuildPasses"); m_passHierarchyChanged = m_passHierarchyChanged || !m_buildPassList.empty(); @@ -239,8 +238,7 @@ namespace AZ void PassSystem::InitializePasses() { m_state = PassSystemState::InitializingPasses; - AZ_PROFILE_FUNCTION(RPI); - AZ_ATOM_PROFILE_FUNCTION("RPI", "PassSystem: BuildPassAttachments"); + AZ_PROFILE_SCOPE(RPI, "PassSystem: InitializePasses"); m_passHierarchyChanged = m_passHierarchyChanged || !m_initializePassList.empty(); @@ -277,7 +275,6 @@ namespace AZ void PassSystem::Validate() { m_state = PassSystemState::ValidatingPasses; - AZ_ATOM_PROFILE_FUNCTION("RPI", "PassSystem: Validate"); if (PassValidation::IsEnabled()) { @@ -286,7 +283,7 @@ namespace AZ return; } - AZ_PROFILE_FUNCTION(RPI); + AZ_PROFILE_SCOPE(RPI, "PassSystem: Validate"); PassValidationResults validationResults; m_rootPass->Validate(validationResults); @@ -298,7 +295,7 @@ namespace AZ void PassSystem::ProcessQueuedChanges() { - AZ_ATOM_PROFILE_FUNCTION("RPI", "PassSystem: ProcessQueuedChanges"); + AZ_PROFILE_SCOPE(RPI, "PassSystem: ProcessQueuedChanges"); RemovePasses(); BuildPasses(); InitializePasses(); @@ -307,8 +304,7 @@ namespace AZ void PassSystem::FrameUpdate(RHI::FrameGraphBuilder& frameGraphBuilder) { - AZ_PROFILE_FUNCTION(RPI); - AZ_ATOM_PROFILE_FUNCTION("RPI", "PassSystem: FrameUpdate"); + AZ_PROFILE_SCOPE(RPI, "PassSystem: FrameUpdate"); ResetFrameStatistics(); ProcessQueuedChanges(); @@ -317,14 +313,14 @@ namespace AZ Pass::FramePrepareParams params{ &frameGraphBuilder }; { - AZ_ATOM_PROFILE_TIME_GROUP_REGION("RPI", "Pass: FrameBegin"); + AZ_PROFILE_SCOPE(RPI, "Pass: FrameBegin"); m_rootPass->FrameBegin(params); } } void PassSystem::FrameEnd() { - AZ_ATOM_PROFILE_FUNCTION("RHI", "PassSystem: FrameEnd"); + AZ_PROFILE_SCOPE(RHI, "PassSystem: FrameEnd"); m_state = PassSystemState::FrameEnd; diff --git a/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/RasterPass.cpp b/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/RasterPass.cpp index 4c923d4a1a..d9f98c11d3 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/RasterPass.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/RasterPass.cpp @@ -216,7 +216,7 @@ namespace AZ void RasterPass::CompileResources(const RHI::FrameGraphCompileContext& context) { - AZ_PROFILE_FUNCTION(RPI); + AZ_PROFILE_SCOPE(RPI, "RasterPass: CompileResources"); if (m_shaderResourceGroup == nullptr) { diff --git a/Gems/Atom/RPI/Code/Source/RPI.Public/RPISystem.cpp b/Gems/Atom/RPI/Code/Source/RPI.Public/RPISystem.cpp index 500bf21628..5df1c655d6 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Public/RPISystem.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Public/RPISystem.cpp @@ -233,7 +233,7 @@ namespace AZ void RPISystem::OnSystemTick() { - AZ_ATOM_PROFILE_FUNCTION("RPI", "RPISystem: OnSystemTick"); + AZ_PROFILE_SCOPE(RPI, "RPISystem: OnSystemTick"); // Image system update is using system tick but not game tick so it can stream images in background even game is pausing m_imageSystem.Update(); @@ -245,7 +245,7 @@ namespace AZ { return; } - AZ_ATOM_PROFILE_FUNCTION("RPI", "RPISystem: SimulationTick"); + AZ_PROFILE_SCOPE(RPI, "RPISystem: SimulationTick"); AssetInitBus::Broadcast(&AssetInitBus::Events::PostLoadInit); @@ -273,8 +273,7 @@ namespace AZ return; } - AZ_PROFILE_FUNCTION(RPI); - AZ_ATOM_PROFILE_FUNCTION("RPI", "RPISystem: RenderTick"); + AZ_PROFILE_SCOPE(RPI, "RPISystem: RenderTick"); // Query system update is to increment the frame count m_querySystem.Update(); @@ -301,7 +300,7 @@ namespace AZ }); { - AZ_ATOM_PROFILE_TIME_GROUP_REGION("RPI", "RPISystem: FrameEnd"); + AZ_PROFILE_SCOPE(RPI, "RPISystem: FrameEnd"); m_dynamicDraw.FrameEnd(); m_passSystem.FrameEnd(); diff --git a/Gems/Atom/RPI/Code/Source/RPI.Public/RenderPipeline.cpp b/Gems/Atom/RPI/Code/Source/RPI.Public/RenderPipeline.cpp index 7f0ab9c8aa..409a084e4f 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Public/RenderPipeline.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Public/RenderPipeline.cpp @@ -397,7 +397,7 @@ namespace AZ void RenderPipeline::OnStartFrame() { - AZ_PROFILE_FUNCTION(RPI); + AZ_PROFILE_SCOPE(RPI, "RenderPipeline: OnStartFrame"); m_lastRenderStartTime = m_lastRenderRequestTime; diff --git a/Gems/Atom/RPI/Code/Source/RPI.Public/Scene.cpp b/Gems/Atom/RPI/Code/Source/RPI.Public/Scene.cpp index 9fa49f7f2a..778548f14c 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Public/Scene.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Public/Scene.cpp @@ -350,7 +350,7 @@ namespace AZ void Scene::Simulate([[maybe_unused]] const TickTimeInfo& tickInfo, RHI::JobPolicy jobPolicy) { - AZ_ATOM_PROFILE_FUNCTION("RPI", "Scene: Simulate"); + AZ_PROFILE_SCOPE(RPI, "Scene: Simulate"); m_simulationTime = tickInfo.m_currentGameTime; @@ -389,7 +389,7 @@ namespace AZ { if (completionJob) { - AZ_ATOM_PROFILE_FUNCTION("RPI", "Scene: WaitAndCleanCompletionJob"); + AZ_PROFILE_SCOPE(RPI, "Scene: WaitAndCleanCompletionJob"); //[GFX TODO]: the completion job should start earlier and wait for completion here completionJob->StartAndWaitForCompletion(); delete completionJob; @@ -422,11 +422,10 @@ namespace AZ void Scene::PrepareRender([[maybe_unused]]const TickTimeInfo& tickInfo, RHI::JobPolicy jobPolicy) { - AZ_ATOM_PROFILE_FUNCTION("RPI", "Scene: PrepareRender"); + AZ_PROFILE_SCOPE(RPI, "Scene: PrepareRender"); { AZ_PROFILE_SCOPE(RPI, "WaitForSimulationCompletion"); - AZ_ATOM_PROFILE_TIME_GROUP_REGION("RPI", "WaitForSimulationCompletion"); WaitAndCleanCompletionJob(m_simulationCompletion); } @@ -435,7 +434,7 @@ namespace AZ // Get active pipelines which need to be rendered and notify them of an impending frame. AZStd::vector activePipelines; { - AZ_ATOM_PROFILE_TIME_GROUP_REGION("RPI", "Scene: OnPrepareFrame"); + AZ_PROFILE_SCOPE(RPI, "Scene: OnPrepareFrame"); for (auto& pipeline : m_pipelines) { pipeline->OnPrepareFrame(); @@ -449,7 +448,7 @@ namespace AZ // Get active pipelines which need to be rendered and notify them frame started for (const auto& pipeline : activePipelines) { - AZ_ATOM_PROFILE_TIME_GROUP_REGION("RPI", "Scene: OnStartFrame"); + AZ_PROFILE_SCOPE(RPI, "Scene: OnStartFrame"); pipeline->OnStartFrame(); } @@ -468,7 +467,7 @@ namespace AZ { - AZ_ATOM_PROFILE_TIME_GROUP_REGION("RPI", "Setup Views"); + AZ_PROFILE_SCOPE(RPI, "Setup Views"); // Collect persistent views from all pipelines to be rendered AZStd::map persistentViews; @@ -506,8 +505,7 @@ namespace AZ } { - AZ_PROFILE_SCOPE(RPI, "CollectDrawPackets"); - AZ_ATOM_PROFILE_TIME_GROUP_REGION("RPI", "CollectDrawPackets"); + AZ_PROFILE_SCOPE(RPI, "CollectDrawPackets"); AZ::JobCompletion* collectDrawPacketsCompletion = aznew AZ::JobCompletion(); // Launch FeatureProcessor::Render() jobs @@ -550,14 +548,13 @@ namespace AZ // Add dynamic draw data for all the views if (m_dynamicDrawSystem) { - AZ_ATOM_PROFILE_TIME_GROUP_REGION("RPI", "DynamicDraw SubmitDrawData"); + AZ_PROFILE_SCOPE(RPI, "DynamicDraw SubmitDrawData"); m_dynamicDrawSystem->SubmitDrawData(this, m_renderPacket.m_views); } } { AZ_PROFILE_BEGIN(RPI, "FinalizeDrawLists"); - AZ_ATOM_PROFILE_TIME_GROUP_REGION("RPI", "FinalizeDrawLists"); if (jobPolicy == RHI::JobPolicy::Serial) { for (auto& view : m_renderPacket.m_views) @@ -586,14 +583,14 @@ namespace AZ } { - AZ_ATOM_PROFILE_TIME_GROUP_REGION("RPI", "Scene OnEndPrepareRender"); + AZ_PROFILE_SCOPE(RPI, "Scene OnEndPrepareRender"); SceneNotificationBus::Event(GetId(), &SceneNotification::OnEndPrepareRender); } } void Scene::OnFrameEnd() { - AZ_ATOM_PROFILE_FUNCTION("RPI", "Scene: OnFrameEnd"); + AZ_PROFILE_SCOPE(RPI, "Scene: OnFrameEnd"); bool didRender = false; for (auto& pipeline : m_pipelines) { @@ -730,7 +727,7 @@ namespace AZ void Scene::RebuildPipelineStatesLookup() { - AZ_ATOM_PROFILE_FUNCTION("RPI", "Scene: RebuildPipelineStatesLookup"); + AZ_PROFILE_SCOPE(RPI, "Scene: RebuildPipelineStatesLookup"); m_pipelineStatesLookup.clear(); AZStd::queue parents; diff --git a/Gems/Atom/RPI/Code/Source/RPI.Public/Shader/Metrics/ShaderMetricsSystem.cpp b/Gems/Atom/RPI/Code/Source/RPI.Public/Shader/Metrics/ShaderMetricsSystem.cpp index 9e633077e7..528d32e217 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Public/Shader/Metrics/ShaderMetricsSystem.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Public/Shader/Metrics/ShaderMetricsSystem.cpp @@ -113,7 +113,7 @@ namespace AZ return; } - AZ_PROFILE_FUNCTION(RPI); + AZ_PROFILE_SCOPE(RPI, "ShaderMetricsSystem: RequestShaderVariant"); AZStd::lock_guard lock(m_metricsMutex); diff --git a/Gems/Atom/RPI/Code/Source/RPI.Public/View.cpp b/Gems/Atom/RPI/Code/Source/RPI.Public/View.cpp index bdb3ea8899..c2356cea45 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Public/View.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Public/View.cpp @@ -239,7 +239,7 @@ namespace AZ void View::FinalizeDrawLists() { - AZ_PROFILE_FUNCTION(RPI); + AZ_PROFILE_SCOPE(RPI, "View: FinalizeDrawLists"); m_drawListContext.FinalizeLists(); if (m_passesByDrawList) { diff --git a/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiCpuProfiler.h b/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiCpuProfiler.h index ea4dd20250..a649fcf0f7 100644 --- a/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiCpuProfiler.h +++ b/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiCpuProfiler.h @@ -174,7 +174,7 @@ namespace AZ AZStd::unordered_map> m_savedData; // Region color cache - AZStd::unordered_map m_regionColorMap; + AZStd::unordered_map m_regionColorMap; // Tracks the frame boundaries AZStd::vector m_frameEndTicks = { INT64_MIN }; diff --git a/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiCpuProfiler.inl b/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiCpuProfiler.inl index 61f68197b6..a573e3019a 100644 --- a/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiCpuProfiler.inl +++ b/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiCpuProfiler.inl @@ -124,7 +124,7 @@ namespace AZ m_cpuTimingStatisticsWhenPause = currentCpuTimingStatistics; CollectFrameData(); - CullFrameData(currentCpuTimingStatistics); + CullFrameData(currentCpuTimingStatistics); // Only listen to system ticks when the profiler is active if (!SystemTickBus::Handler::BusIsConnected()) @@ -148,7 +148,7 @@ namespace AZ } } ImGui::End(); - + if (m_captureToFile) { AZStd::sys_time_t timeNow = AZStd::GetTimeNowSecond(); @@ -325,7 +325,7 @@ namespace AZ { const bool ascending = sortSpecs->Specs->SortDirection == ImGuiSortDirection_Ascending; const ImS16 columnToSort = sortSpecs->Specs->ColumnIndex; - + switch (columnToSort) { case (0): // Sort by group name @@ -343,7 +343,7 @@ namespace AZ case (4): // Sort by invocations AZStd::sort(m_tableData.begin(), m_tableData.end(), TableRow::TableRowCompareFunctor(&TableRow::m_invocationsLastFrame, ascending)); break; - case (5): // Sort by total time + case (5): // Sort by total time AZStd::sort(m_tableData.begin(), m_tableData.end(), TableRow::TableRowCompareFunctor(&TableRow::m_lastFrameTotalTicks, ascending)); break; } @@ -401,7 +401,7 @@ namespace AZ } DrawTable(); - } + } } inline void ImGuiCpuProfiler::DrawFilePicker() @@ -460,17 +460,17 @@ namespace AZ const auto [groupRegionNameItr, wasGroupRegionNameInserted] = m_deserializedGroupRegionNamePool.emplace(groupNameItr->c_str(), regionNameItr->c_str()); - const RHI::CachedTimeRegion newRegion(&(*groupRegionNameItr), entry.m_stackDepth, entry.m_startTick, entry.m_endTick); + const RHI::CachedTimeRegion newRegion(*groupRegionNameItr, entry.m_stackDepth, entry.m_startTick, entry.m_endTick); m_savedData[entry.m_threadId].push_back(newRegion); - // Since we don't serialize the frame boundaries, we need to use the RPI's OnSystemTick event as a heuristic. + // Since we don't serialize the frame boundaries, we need to use the RPI's OnSystemTick event as a heuristic. const static Name frameBoundaryName = Name("RPISystem: OnSystemTick"); if (entry.m_regionName == frameBoundaryName) { m_frameEndTicks.push_back(entry.m_endTick); - } + } - // Update running statistics + // Update running statistics if (!m_groupRegionMap[*groupNameItr].contains(*regionNameItr)) { m_groupRegionMap[*groupNameItr][*regionNameItr].m_groupName = *groupNameItr; @@ -487,7 +487,7 @@ namespace AZ // Invariant: each vector in m_savedData must be sorted so that we can efficiently cull region data. for (auto& [threadId, singleThreadData] : m_savedData) { - AZStd::sort(singleThreadData.begin(), singleThreadData.end(), + AZStd::sort(singleThreadData.begin(), singleThreadData.end(), [](const TimeRegion& lhs, const TimeRegion& rhs) { return lhs.m_startTick < rhs.m_startTick; @@ -669,7 +669,7 @@ namespace AZ // Iterate through the entire TimeRegionMap and copy the data since it will get deleted on the next frame for (const auto& [threadId, singleThreadRegionMap] : timeRegionMap) { - const size_t threadIdHashed = AZStd::hash{}(threadId); + const size_t threadIdHashed = AZStd::hash{}(threadId); // The profiler can sometime return threads without any profiling events when dropping threads, FIXME(ATOM-15949) if (singleThreadRegionMap.size() == 0) { @@ -686,7 +686,7 @@ namespace AZ newVisualizerData.push_back(region); // Copies // Also update the statistical view's data - const AZStd::string& groupName = region.m_groupRegionName->m_groupName; + const AZStd::string& groupName = region.m_groupRegionName.m_groupName; if (!m_groupRegionMap[groupName].contains(regionName)) { @@ -765,7 +765,7 @@ namespace AZ inline void ImGuiCpuProfiler::DrawBlock(const TimeRegion& block, u64 targetRow) { // Don't draw anything if the user is searching for regions and this block doesn't pass the filter - if (!m_visualizerHighlightFilter.PassFilter(block.m_groupRegionName->m_regionName)) + if (!m_visualizerHighlightFilter.PassFilter(block.m_groupRegionName.m_regionName)) { return; } @@ -798,7 +798,7 @@ namespace AZ if (regionPixelWidth > maxCharWidth) // We can draw at least one character { const AZStd::string label = - AZStd::string::format("%s/ %s", block.m_groupRegionName->m_groupName, block.m_groupRegionName->m_regionName); + AZStd::string::format("%s/ %s", block.m_groupRegionName.m_groupName, block.m_groupRegionName.m_regionName); const float textWidth = ImGui::CalcTextSize(label.c_str()).x; if (regionPixelWidth < textWidth) // Not enough space in the block to draw the whole name, draw clipped text. @@ -809,7 +809,7 @@ namespace AZ // so we must adjust for the scale manually. const float scaleFactor = ImGui::GetIO().FontGlobalScale; const float fontSize = ImGui::GetFont()->FontSize * scaleFactor; - + ImGui::GetFont()->RenderText(drawList, fontSize, startPoint, IM_COL32_WHITE, clipRect, label.c_str(), 0); } else // We have enough space to draw the entire label, draw and center text. @@ -828,7 +828,7 @@ namespace AZ if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { m_enableVisualizer = false; - const auto newFilter = AZStd::string(block.m_groupRegionName->m_regionName); + const auto newFilter = AZStd::string(block.m_groupRegionName.m_regionName); m_timedRegionFilter = ImGuiTextFilter(newFilter.c_str()); m_timedRegionFilter.Build(); } @@ -836,7 +836,7 @@ namespace AZ drawList->AddRect(startPoint, endPoint, ImGui::GetColorU32({ 1, 1, 1, 1 }), 0.0, 0, 1.5); ImGui::BeginTooltip(); - ImGui::Text("%s::%s", block.m_groupRegionName->m_groupName, block.m_groupRegionName->m_regionName); + ImGui::Text("%s::%s", block.m_groupRegionName.m_groupName, block.m_groupRegionName.m_regionName); ImGui::Text("Execution time: %.3f ms", CpuProfilerImGuiHelper::TicksToMs(block.m_endTick - block.m_startTick)); ImGui::Text("Ticks %lld => %lld", block.m_startTick, block.m_endTick); ImGui::EndTooltip(); @@ -846,10 +846,10 @@ namespace AZ inline ImU32 ImGuiCpuProfiler::GetBlockColor(const TimeRegion& block) { // Use the GroupRegionName pointer a key into the cache, equal regions will have equal pointers - const GroupRegionName* key = block.m_groupRegionName; - if (m_regionColorMap.contains(key)) // Cache hit + const GroupRegionName& key = block.m_groupRegionName; + if (auto iter = m_regionColorMap.find(key); iter != m_regionColorMap.end()) // Cache hit { - return ImGui::GetColorU32(m_regionColorMap[key]); + return ImGui::GetColorU32(iter->second); } // Cache miss, generate a new random color diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/CMakeLists.txt b/Gems/AtomLyIntegration/CommonFeatures/Code/CMakeLists.txt index bf28ed3f14..e68681315e 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/CMakeLists.txt +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/CMakeLists.txt @@ -109,6 +109,7 @@ if(PAL_TRAIT_BUILD_HOST_TOOLS) RUNTIME_DEPENDENCIES Gem::Atom_RPI.Editor Gem::Atom_Feature_Common.Editor + Legacy::EditorCommon ) # The AtomLyIntegration_CommonFeatures.Editor module is used for Builders and Tools diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Include/AtomLyIntegration/CommonFeatures/Material/MaterialComponentBus.h b/Gems/AtomLyIntegration/CommonFeatures/Code/Include/AtomLyIntegration/CommonFeatures/Material/MaterialComponentBus.h index 3b65750b5f..ace16ba6ca 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Include/AtomLyIntegration/CommonFeatures/Material/MaterialComponentBus.h +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Include/AtomLyIntegration/CommonFeatures/Material/MaterialComponentBus.h @@ -33,6 +33,16 @@ namespace AZ virtual const MaterialAssignmentMap& GetMaterialOverrides() const = 0; //! Clear all material overrides virtual void ClearAllMaterialOverrides() = 0; + //! Clear non-lod material overrides + virtual void ClearModelMaterialOverrides() = 0; + //! Clear lod material overrides + virtual void ClearLodMaterialOverrides() = 0; + //! Clear residual materials that don't correspond to the associated model + virtual void ClearIncompatibleMaterialOverrides() = 0; + //! Clear materials that reference missing assets + virtual void ClearInvalidMaterialOverrides() = 0; + //! Repair materials that reference missing assets by assigning the default asset + virtual void RepairInvalidMaterialOverrides() = 0; //! Set default material override virtual void SetDefaultMaterialOverride(const AZ::Data::AssetId& materialAssetId) = 0; //! Get default material override diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponent.cpp b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponent.cpp index c243522257..b7bf191bc9 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponent.cpp +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponent.cpp @@ -32,9 +32,6 @@ namespace AZ const char* EditorMaterialComponent::GenerateMaterialsButtonText = "Generate/Manage Source Materials..."; const char* EditorMaterialComponent::GenerateMaterialsToolTipText = "Generate editable source material files from materials provided by the model."; - const char* EditorMaterialComponent::ResetMaterialsButtonText = "Reset Materials"; - const char* EditorMaterialComponent::ResetMaterialsToolTipText = "Clear all settings, materials, and properties then rebuild material slots from the associated model."; - // Update serialized data to the new format and data types bool EditorMaterialComponent::ConvertVersion(AZ::SerializeContext& context, AZ::SerializeContext::DataElementNode& classElement) { @@ -178,43 +175,74 @@ namespace AZ menu->addSeparator(); - action = menu->addAction(ResetMaterialsButtonText, [this]() { ResetMaterialSlots(); }); - action->setToolTip(ResetMaterialsToolTipText); + action = menu->addAction("Clear All Materials", [this]() { + AzToolsFramework::ScopedUndoBatch undoBatch("Clearing all materials."); + SetDirty(); - menu->addSeparator(); + MaterialComponentRequestBus::Event(GetEntityId(), &MaterialComponentRequestBus::Events::ClearAllMaterialOverrides); + + m_materialSlotsByLodEnabled = false; + + UpdateMaterialSlots(); + }); + action->setToolTip("Clear all materials and properties then rebuild material slots from the associated model."); action = menu->addAction("Clear Model Materials", [this]() { AzToolsFramework::ScopedUndoBatch undoBatch("Clearing model materials."); SetDirty(); - for (auto& materialSlotPair : GetMaterialSlots()) - { - EditorMaterialComponentSlot* materialSlot = materialSlotPair.second; - if (materialSlot->m_id.IsSlotIdOnly()) - { - materialSlot->Clear(); - } - } - }); + MaterialComponentRequestBus::Event(GetEntityId(), &MaterialComponentRequestBus::Events::ClearModelMaterialOverrides); + + UpdateMaterialSlots(); + }); + action->setToolTip("Clear model materials and properties then rebuild material slots from the associated model."); + action = menu->addAction("Clear LOD Materials", [this]() { AzToolsFramework::ScopedUndoBatch undoBatch("Clearing LOD materials."); SetDirty(); - for (auto& materialSlotPair : GetMaterialSlots()) - { - EditorMaterialComponentSlot* materialSlot = materialSlotPair.second; - if (materialSlot->m_id.IsLodAndSlotId()) - { - materialSlot->Clear(); - } - } - }); - action->setEnabled(m_materialSlotsByLodEnabled); + MaterialComponentRequestBus::Event(GetEntityId(), &MaterialComponentRequestBus::Events::ClearLodMaterialOverrides); + + m_materialSlotsByLodEnabled = false; + + UpdateMaterialSlots(); + }); + action->setToolTip("Clear LOD materials and properties then rebuild material slots from the associated model."); + + action = menu->addAction("Clear Incompatible Materials", [this]() { + AzToolsFramework::ScopedUndoBatch undoBatch("Clearing incompatible materials."); + SetDirty(); + + MaterialComponentRequestBus::Event(GetEntityId(), &MaterialComponentRequestBus::Events::ClearIncompatibleMaterialOverrides); + + UpdateMaterialSlots(); + }); + action->setToolTip("Clear residual materials that don't correspond to the associated model."); + + action = menu->addAction("Clear Invalid Materials", [this]() { + AzToolsFramework::ScopedUndoBatch undoBatch("Clearing invalid materials."); + SetDirty(); + + MaterialComponentRequestBus::Event(GetEntityId(), &MaterialComponentRequestBus::Events::ClearInvalidMaterialOverrides); + + UpdateMaterialSlots(); + }); + action->setToolTip("Clear materials that reference missing assets."); + + action = menu->addAction("Repair Invalid Materials", [this]() { + AzToolsFramework::ScopedUndoBatch undoBatch("Repairing invalid materials."); + SetDirty(); + + MaterialComponentRequestBus::Event(GetEntityId(), &MaterialComponentRequestBus::Events::RepairInvalidMaterialOverrides); + + UpdateMaterialSlots(); + }); + action->setToolTip("Repair materials that reference missing assets by assigning the default asset."); } void EditorMaterialComponent::SetPrimaryAsset(const AZ::Data::AssetId& assetId) { - m_controller.SetDefaultMaterialOverride(assetId); + MaterialComponentRequestBus::Event(GetEntityId(), &MaterialComponentRequestBus::Events::SetDefaultMaterialOverride, assetId); MaterialComponentNotificationBus::Event(GetEntityId(), &MaterialComponentNotifications::OnMaterialsEdited); @@ -249,14 +277,18 @@ namespace AZ m_materialSlots = {}; m_materialSlotsByLod = {}; - const MaterialComponentConfig& config = m_controller.GetConfiguration(); + // Get current material assignments + MaterialAssignmentMap currentMaterials; + MaterialComponentRequestBus::EventResult( + currentMaterials, GetEntityId(), &MaterialComponentRequestBus::Events::GetMaterialOverrides); // Get the known material assignment slots from the associated model or other source - MaterialAssignmentMap materialsFromSource; - MaterialReceiverRequestBus::EventResult(materialsFromSource, GetEntityId(), &MaterialReceiverRequestBus::Events::GetMaterialAssignments); + MaterialAssignmentMap originalMaterials; + MaterialComponentRequestBus::EventResult( + originalMaterials, GetEntityId(), &MaterialComponentRequestBus::Events::GetOriginalMaterialAssignments); // Generate the table of editable materials using the source data to define number of groups, elements, and initial values - for (const auto& materialPair : materialsFromSource) + for (const auto& materialPair : originalMaterials) { // Setup the material slot entry EditorMaterialComponentSlot slot; @@ -264,7 +296,7 @@ namespace AZ slot.m_id = materialPair.first; // if material is present in controller configuration, assign its data - const MaterialAssignment& materialFromController = GetMaterialAssignmentFromMap(config.m_materials, slot.m_id); + const MaterialAssignment& materialFromController = GetMaterialAssignmentFromMap(currentMaterials, slot.m_id); slot.m_materialAsset = materialFromController.m_materialAsset; if (slot.m_id.IsDefault()) @@ -289,7 +321,7 @@ namespace AZ } } - // Sort all of the slots by label to ensure stable index values (materialsFromSource is an unordered map) + // Sort all of the slots by label to ensure stable index values (originalMaterials is an unordered map) AZStd::sort(m_materialSlots.begin(), m_materialSlots.end(), [](const auto& a, const auto& b) { return a.GetLabel() < b.GetLabel(); }); @@ -305,49 +337,36 @@ namespace AZ &AzToolsFramework::ToolsApplicationEvents::InvalidatePropertyDisplay, AzToolsFramework::Refresh_EntireTree); } - AZ::u32 EditorMaterialComponent::ResetMaterialSlots() - { - AzToolsFramework::ScopedUndoBatch undoBatch("Resetting materials."); - SetDirty(); - - m_controller.SetMaterialOverrides(MaterialAssignmentMap()); - UpdateMaterialSlots(); - - m_materialSlotsByLodEnabled = false; - - MaterialComponentNotificationBus::Event(GetEntityId(), &MaterialComponentNotifications::OnMaterialsEdited); - - AzToolsFramework::ToolsApplicationEvents::Bus::Broadcast( - &AzToolsFramework::ToolsApplicationEvents::InvalidatePropertyDisplay, AzToolsFramework::Refresh_EntireTree); - - return AZ::Edit::PropertyRefreshLevels::EntireTree; - } - AZ::u32 EditorMaterialComponent::OpenMaterialExporter() { AzToolsFramework::ScopedUndoBatch undoBatch("Generating materials."); SetDirty(); - // First generating a unique set of all material asset IDs that will be used for source data generation - AZStd::unordered_map assetIdMap; + MaterialAssignmentMap originalMaterials; + MaterialComponentRequestBus::EventResult( + originalMaterials, GetEntityId(), &MaterialComponentRequestBus::Events::GetOriginalMaterialAssignments); - auto materialSlots = GetMaterialSlots(); - for (auto& materialSlotPair : materialSlots) + // Generate a unique set of all material asset IDs that will be used for source data generation + AZStd::unordered_map assetIdToSlotNameMap; + for (const auto& materialPair : originalMaterials) { - Data::AssetId defaultMaterialAssetId = materialSlotPair.second->GetDefaultAssetId(); - if (defaultMaterialAssetId.IsValid()) + const Data::AssetId originalAssetId = materialPair.second.m_materialAsset.GetId(); + if (originalAssetId.IsValid()) { - assetIdMap[defaultMaterialAssetId] = materialSlotPair.second->GetLabel(); + MaterialComponentRequestBus::EventResult( + assetIdToSlotNameMap[originalAssetId], GetEntityId(), &MaterialComponentRequestBus::Events::GetMaterialSlotLabel, + materialPair.first); } } // Convert the unique set of asset IDs into export items that can be configured in the dialog // The order should not matter because the table in the dialog can sort itself for a specific row EditorMaterialComponentExporter::ExportItemsContainer exportItems; - for (auto assetIdInfo : assetIdMap) + exportItems.reserve(assetIdToSlotNameMap.size()); + + for (const auto& [assetId, slotName] : assetIdToSlotNameMap) { - EditorMaterialComponentExporter::ExportItem exportItem{ assetIdInfo.first, assetIdInfo.second }; - exportItems.push_back(exportItem); + exportItems.emplace_back(assetId, slotName); } // Display the export dialog so that the user can configure how they want different materials to be exported @@ -363,16 +382,17 @@ namespace AZ const auto& assetIdOutcome = AZ::RPI::AssetUtils::MakeAssetId(exportItem.GetExportPath(), 0); if (assetIdOutcome) { - for (auto& materialSlotPair : materialSlots) + for (const auto& materialPair : originalMaterials) { - EditorMaterialComponentSlot* editorMaterialSlot = materialSlotPair.second; - - if (editorMaterialSlot) + // We need to check whether replaced material corresponds to this slot's default material. + const Data::AssetId originalAssetId = materialPair.second.m_materialAsset.GetId(); + if (originalAssetId == exportItem.GetOriginalAssetId()) { - // We need to check whether replaced material corresponds to this slot's default material. - if (editorMaterialSlot->GetDefaultAssetId() == exportItem.GetOriginalAssetId()) + if (m_materialSlotsByLodEnabled || !materialPair.first.IsLodAndSlotId()) { - editorMaterialSlot->SetAsset(assetIdOutcome.GetValue()); + MaterialComponentRequestBus::Event( + GetEntityId(), &MaterialComponentRequestBus::Events::SetMaterialOverride, materialPair.first, + assetIdOutcome.GetValue()); } } } @@ -380,12 +400,9 @@ namespace AZ } } - MaterialComponentNotificationBus::Event(GetEntityId(), &MaterialComponentNotifications::OnMaterialsEdited); + UpdateMaterialSlots(); - AzToolsFramework::ToolsApplicationEvents::Bus::Broadcast( - &AzToolsFramework::ToolsApplicationEvents::InvalidatePropertyDisplay, AzToolsFramework::Refresh_AttributesAndValues); - - return AZ::Edit::PropertyRefreshLevels::AttributesAndValues; + return AZ::Edit::PropertyRefreshLevels::EntireTree; } AZ::u32 EditorMaterialComponent::OnLodsToggled() @@ -395,15 +412,10 @@ namespace AZ if (!m_materialSlotsByLodEnabled) { - MaterialComponentConfig config = m_controller.GetConfiguration(); - AZStd::erase_if(config.m_materials, [](const auto& item) { - const auto& [key, value] = item; - return key.m_lodIndex != MaterialAssignmentId::NonLodIndex; - }); - m_controller.SetMaterialOverrides(config.m_materials); + MaterialComponentRequestBus::Event(GetEntityId(), &MaterialComponentRequestBus::Events::ClearLodMaterialOverrides); } - MaterialComponentNotificationBus::Event(GetEntityId(), &MaterialComponentNotifications::OnMaterialsEdited); + UpdateMaterialSlots(); return AZ::Edit::PropertyRefreshLevels::EntireTree; } @@ -440,41 +452,5 @@ namespace AZ { return AZStd::string::format("LOD %d", lodIndex); } - - template - void EditorMaterialComponent::BuildMaterialSlotMap(ComponentType& component, ContainerType& materialSlots) - { - materialSlots[DefaultMaterialAssignmentId] = &component.m_defaultMaterialSlot; - - for (auto& slot : component.m_materialSlots) - { - materialSlots[slot.m_id] = &slot; - } - - if (component.m_materialSlotsByLodEnabled) - { - for (auto& slotsForLod : component.m_materialSlotsByLod) - { - for (auto& slot : slotsForLod) - { - materialSlots[slot.m_id] = &slot; - } - } - } - } - - AZStd::unordered_map EditorMaterialComponent::GetMaterialSlots() - { - AZStd::unordered_map materialSlots; - BuildMaterialSlotMap(*this, materialSlots); - return AZStd::move(materialSlots); - } - - AZStd::unordered_map EditorMaterialComponent::GetMaterialSlots() const - { - AZStd::unordered_map materialSlots; - BuildMaterialSlotMap(*this, materialSlots); - return AZStd::move(materialSlots); - } } // namespace Render } // namespace AZ diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponent.h b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponent.h index 6a218e23b7..f8895d994f 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponent.h +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponent.h @@ -58,9 +58,6 @@ namespace AZ // controller configuration then those values will be assigned to the editor component slots. void UpdateMaterialSlots(); - // Clears all values related to the material component and regenerates the editor slots - AZ::u32 ResetMaterialSlots(); - // Opens the source material export dialog and updates editor material slots based on // selected actions AZ::u32 OpenMaterialExporter(); @@ -82,10 +79,6 @@ namespace AZ // Evaluate if materials can be edited bool IsEditingAllowed() const; - template - static void BuildMaterialSlotMap(ComponentType& component, ContainerType& materialSlots); - AZStd::unordered_map GetMaterialSlots(); - AZStd::unordered_map GetMaterialSlots() const; AZStd::string GetLabelForLod(int lodIndex) const; AZStd::string m_message; diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponentExporter.cpp b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponentExporter.cpp index f55da28fa6..14cc28b91e 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponentExporter.cpp +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponentExporter.cpp @@ -55,6 +55,10 @@ namespace AZ bool OpenExportDialog(ExportItemsContainer& exportItems) { + // Sort material entries so they are ordered by name in the table + AZStd::sort(exportItems.begin(), exportItems.end(), + [](const auto& a, const auto& b) { return a.GetMaterialSlotName() < b.GetMaterialSlotName(); }); + QWidget* activeWindow = nullptr; AzToolsFramework::EditorWindowRequestBus::BroadcastResult(activeWindow, &AzToolsFramework::EditorWindowRequests::GetAppMainWindow); diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponentSlot.cpp b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponentSlot.cpp index 37a9ee7b93..363221d3fc 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponentSlot.cpp +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponentSlot.cpp @@ -140,7 +140,7 @@ namespace AZ void EditorMaterialComponentSlot::SetAsset(const Data::AssetId& assetId) { - m_materialAsset.Create(assetId); + m_materialAsset = AZ::Data::Asset(assetId, AZ::AzTypeInfo::Uuid()); MaterialComponentRequestBus::Event( m_entityId, &MaterialComponentRequestBus::Events::SetMaterialOverride, m_id, m_materialAsset.GetId()); OnDataChanged(); @@ -164,7 +164,7 @@ namespace AZ void EditorMaterialComponentSlot::ClearToDefaultAsset() { - m_materialAsset.Create(GetDefaultAssetId()); + m_materialAsset = AZ::Data::Asset(GetDefaultAssetId(), AZ::AzTypeInfo::Uuid()); MaterialComponentRequestBus::Event( m_entityId, &MaterialComponentRequestBus::Events::SetMaterialOverride, m_id, m_materialAsset.GetId()); ClearOverrides(); @@ -204,7 +204,8 @@ namespace AZ const auto& assetIdOutcome = AZ::RPI::AssetUtils::MakeAssetId(exportItem.GetExportPath(), 0); if (assetIdOutcome) { - m_materialAsset.Create(assetIdOutcome.GetValue()); + m_materialAsset = AZ::Data::Asset( + assetIdOutcome.GetValue(), AZ::AzTypeInfo::Uuid()); changed = true; } } diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/MaterialComponentController.cpp b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/MaterialComponentController.cpp index 14f81c5021..1d9f1e81dd 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/MaterialComponentController.cpp +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/MaterialComponentController.cpp @@ -43,6 +43,11 @@ namespace AZ ->Event("SetDefaultMaterialOverride", &MaterialComponentRequestBus::Events::SetDefaultMaterialOverride) ->Event("GetDefaultMaterialOverride", &MaterialComponentRequestBus::Events::GetDefaultMaterialOverride) ->Event("ClearDefaultMaterialOverride", &MaterialComponentRequestBus::Events::ClearDefaultMaterialOverride) + ->Event("ClearModelMaterialOverrides", &MaterialComponentRequestBus::Events::ClearModelMaterialOverrides) + ->Event("ClearLodMaterialOverrides", &MaterialComponentRequestBus::Events::ClearLodMaterialOverrides) + ->Event("ClearIncompatibleMaterialOverrides", &MaterialComponentRequestBus::Events::ClearIncompatibleMaterialOverrides) + ->Event("ClearInvalidMaterialOverrides", &MaterialComponentRequestBus::Events::ClearInvalidMaterialOverrides) + ->Event("RepairInvalidMaterialOverrides", &MaterialComponentRequestBus::Events::RepairInvalidMaterialOverrides) ->Event("SetMaterialOverride", &MaterialComponentRequestBus::Events::SetMaterialOverride) ->Event("GetMaterialOverride", &MaterialComponentRequestBus::Events::GetMaterialOverride) ->Event("ClearMaterialOverride", &MaterialComponentRequestBus::Events::ClearMaterialOverride) @@ -275,10 +280,10 @@ namespace AZ MaterialAssignmentMap MaterialComponentController::GetOriginalMaterialAssignments() const { - MaterialAssignmentMap materialAssignmentMap; + MaterialAssignmentMap originalMaterials; MaterialReceiverRequestBus::EventResult( - materialAssignmentMap, m_entityId, &MaterialReceiverRequestBus::Events::GetMaterialAssignments); - return materialAssignmentMap; + originalMaterials, m_entityId, &MaterialReceiverRequestBus::Events::GetMaterialAssignments); + return originalMaterials; } MaterialAssignmentId MaterialComponentController::FindMaterialAssignmentId( @@ -348,6 +353,67 @@ namespace AZ } } + void MaterialComponentController::ClearModelMaterialOverrides() + { + AZStd::erase_if(m_configuration.m_materials, [](const auto& materialPair) { + return materialPair.first.IsSlotIdOnly(); + }); + QueueMaterialUpdateNotification(); + } + + void MaterialComponentController::ClearLodMaterialOverrides() + { + AZStd::erase_if(m_configuration.m_materials, [](const auto& materialPair) { + return materialPair.first.IsLodAndSlotId(); + }); + QueueMaterialUpdateNotification(); + } + + void MaterialComponentController::ClearIncompatibleMaterialOverrides() + { + const MaterialAssignmentMap& originalMaterials = GetOriginalMaterialAssignments(); + AZStd::erase_if(m_configuration.m_materials, [&originalMaterials](const auto& materialPair) { + return originalMaterials.find(materialPair.first) == originalMaterials.end(); + }); + QueueMaterialUpdateNotification(); + } + + void MaterialComponentController::ClearInvalidMaterialOverrides() + { + AZStd::erase_if(m_configuration.m_materials, [](const auto& materialPair) { + if (materialPair.second.m_materialAsset.GetId().IsValid()) + { + AZ::Data::AssetInfo assetInfo; + AZ::Data::AssetCatalogRequestBus::BroadcastResult( + assetInfo, &AZ::Data::AssetCatalogRequestBus::Events::GetAssetInfoById, + materialPair.second.m_materialAsset.GetId()); + return !assetInfo.m_assetId.IsValid(); + } + return false; + }); + QueueMaterialUpdateNotification(); + } + + void MaterialComponentController::RepairInvalidMaterialOverrides() + { + for (auto& materialPair : m_configuration.m_materials) + { + if (materialPair.second.m_materialAsset.GetId().IsValid()) + { + AZ::Data::AssetInfo assetInfo; + AZ::Data::AssetCatalogRequestBus::BroadcastResult( + assetInfo, &AZ::Data::AssetCatalogRequestBus::Events::GetAssetInfoById, + materialPair.second.m_materialAsset.GetId()); + if (!assetInfo.m_assetId.IsValid()) + { + materialPair.second.m_materialAsset = AZ::Data::Asset( + GetDefaultMaterialAssetId(materialPair.first), AZ::AzTypeInfo::Uuid()); + } + } + } + LoadMaterials(); + } + void MaterialComponentController::SetDefaultMaterialOverride(const AZ::Data::AssetId& materialAssetId) { SetMaterialOverride(DefaultMaterialAssignmentId, materialAssetId); @@ -363,9 +429,11 @@ namespace AZ ClearMaterialOverride(DefaultMaterialAssignmentId); } - void MaterialComponentController::SetMaterialOverride(const MaterialAssignmentId& materialAssignmentId, const AZ::Data::AssetId& materialAssetId) + void MaterialComponentController::SetMaterialOverride( + const MaterialAssignmentId& materialAssignmentId, const AZ::Data::AssetId& materialAssetId) { - m_configuration.m_materials[materialAssignmentId].m_materialAsset.Create(materialAssetId); + m_configuration.m_materials[materialAssignmentId].m_materialAsset = + AZ::Data::Asset(materialAssetId, AZ::AzTypeInfo::Uuid()); LoadMaterials(); } @@ -616,7 +684,6 @@ namespace AZ void MaterialComponentController::ClearAllPropertyOverrides() { - bool cleared = false; for (auto& materialPair : m_configuration.m_materials) { if (!materialPair.second.m_propertyOverrides.empty()) @@ -625,13 +692,8 @@ namespace AZ materialPair.second.RebuildInstance(); MaterialComponentNotificationBus::Event(m_entityId, &MaterialComponentNotifications::OnMaterialInstanceCreated, materialPair.second); QueueMaterialUpdateNotification(); - cleared = true; } } - - if (cleared) - { - } } void MaterialComponentController::SetPropertyOverrides( diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/MaterialComponentController.h b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/MaterialComponentController.h index c9dd330412..2bf15ca3b8 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/MaterialComponentController.h +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/MaterialComponentController.h @@ -52,6 +52,11 @@ namespace AZ void SetMaterialOverrides(const MaterialAssignmentMap& materials) override; const MaterialAssignmentMap& GetMaterialOverrides() const override; void ClearAllMaterialOverrides() override; + void ClearModelMaterialOverrides() override; + void ClearLodMaterialOverrides() override; + void ClearIncompatibleMaterialOverrides() override; + void ClearInvalidMaterialOverrides() override; + void RepairInvalidMaterialOverrides() override; void SetDefaultMaterialOverride(const AZ::Data::AssetId& materialAssetId) override; const AZ::Data::AssetId GetDefaultMaterialOverride() const override; void ClearDefaultMaterialOverride() override; diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/SurfaceData/SurfaceDataMeshComponent.cpp b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/SurfaceData/SurfaceDataMeshComponent.cpp index cbb30e6cd8..d8cae4564d 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/SurfaceData/SurfaceDataMeshComponent.cpp +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/SurfaceData/SurfaceDataMeshComponent.cpp @@ -231,7 +231,7 @@ namespace SurfaceData void SurfaceDataMeshComponent::UpdateMeshData() { - AZ_PROFILE_FUNCTION(Entity); + AZ_PROFILE_SCOPE(Entity, "SurfaceDataMeshComponent: UpdateMeshData"); bool meshValidBeforeUpdate = false; bool meshValidAfterUpdate = false; diff --git a/Gems/Camera/Code/CMakeLists.txt b/Gems/Camera/Code/CMakeLists.txt index 3fbeec0908..138f0fda85 100644 --- a/Gems/Camera/Code/CMakeLists.txt +++ b/Gems/Camera/Code/CMakeLists.txt @@ -61,6 +61,8 @@ if (PAL_TRAIT_BUILD_HOST_TOOLS) Legacy::EditorCommon AZ::AzToolsFramework Gem::Camera.Static + RUNTIME_DEPENDENCIES + Legacy::EditorCommon ) # tools and builders use the above module. diff --git a/Gems/Multiplayer/Code/CMakeLists.txt b/Gems/Multiplayer/Code/CMakeLists.txt index e8b38c8799..49b404fb1c 100644 --- a/Gems/Multiplayer/Code/CMakeLists.txt +++ b/Gems/Multiplayer/Code/CMakeLists.txt @@ -25,6 +25,9 @@ ly_add_target( AZ::AzCore AZ::AzFramework AZ::AzNetworking + PRIVATE + Gem::EMotionFXStaticLib + Gem::PhysX.Static AUTOGEN_RULES *.AutoPackets.xml,AutoPackets_Header.jinja,$path/$fileprefix.AutoPackets.h *.AutoPackets.xml,AutoPackets_Inline.jinja,$path/$fileprefix.AutoPackets.inl diff --git a/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetBindComponent.h b/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetBindComponent.h index 855d109145..743315e8bc 100644 --- a/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetBindComponent.h +++ b/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetBindComponent.h @@ -35,7 +35,7 @@ namespace Multiplayer using EntityMigrationStartEvent = AZ::Event; using EntityMigrationEndEvent = AZ::Event<>; using EntityServerMigrationEvent = AZ::Event; - using EntityPreRenderEvent = AZ::Event; + using EntityPreRenderEvent = AZ::Event; using EntityCorrectionEvent = AZ::Event<>; //! @class NetBindComponent @@ -118,7 +118,7 @@ namespace Multiplayer void NotifyMigrationStart(ClientInputId migratedInputId); void NotifyMigrationEnd(); void NotifyServerMigration(HostId hostId, AzNetworking::ConnectionId connectionId); - void NotifyPreRender(float deltaTime, float blendFactor); + void NotifyPreRender(float deltaTime); void NotifyCorrection(); void AddEntityStopEventHandler(EntityStopEvent::Handler& eventHandler); diff --git a/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkCharacterComponent.h b/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkCharacterComponent.h new file mode 100644 index 0000000000..478c925299 --- /dev/null +++ b/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkCharacterComponent.h @@ -0,0 +1,82 @@ +/* + * 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 + +#include +#include +#include + +namespace Physics +{ + class Character; +} + +namespace Multiplayer +{ + //! NetworkCharacterComponent + //! Provides multiplayer support for game-play player characters. + class NetworkCharacterComponent + : public NetworkCharacterComponentBase + , private PhysX::CharacterGameplayRequestBus::Handler + { + friend class NetworkCharacterComponentController; + + public: + AZ_MULTIPLAYER_COMPONENT(Multiplayer::NetworkCharacterComponent, s_networkCharacterComponentConcreteUuid, Multiplayer::NetworkCharacterComponentBase) + + static void Reflect(AZ::ReflectContext* context); + + NetworkCharacterComponent(); + + static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible) + { + incompatible.push_back(AZ_CRC_CE("NetworkRigidBodyService")); + } + + // AZ::Component + void OnInit() override {} + void OnActivate(Multiplayer::EntityIsMigrating entityIsMigrating) override; + void OnDeactivate(Multiplayer::EntityIsMigrating entityIsMigrating) override; + + private: + void OnTranslationChangedEvent(const AZ::Vector3& translation); + void OnSyncRewind(); + + // CharacterGameplayRequestBus + bool IsOnGround() const override; + float GetGravityMultiplier() const override { return {}; } + void SetGravityMultiplier([[maybe_unused]] float gravityMultiplier) override {} + AZ::Vector3 GetFallingVelocity() const override { return {}; } + void SetFallingVelocity([[maybe_unused]] const AZ::Vector3& fallingVelocity) override {} + + Physics::Character* m_physicsCharacter = nullptr; + Multiplayer::EntitySyncRewindEvent::Handler m_syncRewindHandler = Multiplayer::EntitySyncRewindEvent::Handler([this]() { OnSyncRewind(); }); + AZ::Event::Handler m_translationEventHandler; + }; + + //! NetworkCharacterComponentController + //! This is the network controller for NetworkCharacterComponent. + //! Class provides the ability to move characters in physical space while keeping the network in-sync. + class NetworkCharacterComponentController + : public NetworkCharacterComponentControllerBase + { + public: + NetworkCharacterComponentController(NetworkCharacterComponent& parent); + + // NetworkCharacterComponentControllerBase + void OnActivate(Multiplayer::EntityIsMigrating entityIsMigrating) override; + void OnDeactivate(Multiplayer::EntityIsMigrating entityIsMigrating) override; + + //! TryMoveWithVelocity + //! Will move this character entity kinematically through physical world while also ensuring the network stays in-sync. + //! Velocity will be applied over delta-time to determine the movement amount. + //! Returns this entity's world-space position after the move. + AZ::Vector3 TryMoveWithVelocity(const AZ::Vector3& velocity, float deltaTime); + }; +} diff --git a/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkHitVolumesComponent.h b/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkHitVolumesComponent.h new file mode 100644 index 0000000000..215359f209 --- /dev/null +++ b/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkHitVolumesComponent.h @@ -0,0 +1,90 @@ +/* + * 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 + +#include +#include +#include +#include + +namespace Physics +{ + class CharacterRequests; + class CharacterHitDetectionConfiguration; +} + +namespace Multiplayer +{ + class NetworkHitVolumesComponent + : public NetworkHitVolumesComponentBase + , private EMotionFX::Integration::ActorComponentNotificationBus::Handler + { + public: + struct AnimatedHitVolume final + { + AnimatedHitVolume + ( + AzNetworking::ConnectionId connectionId, + Physics::CharacterRequests* character, + const char* hitVolumeName, + const Physics::ColliderConfiguration* colliderConfig, + const Physics::ShapeConfiguration* shapeConfig, + const uint32_t jointIndex + ); + + ~AnimatedHitVolume() = default; + + void UpdateTransform(const AZ::Transform& transform); + void SyncToCurrentTransform(); + + Multiplayer::RewindableObject m_transform; + AZStd::shared_ptr m_physicsShape; + + // Cached so we don't have to do subsequent lookups by name + const Physics::ColliderConfiguration* m_colliderConfig = nullptr; + const Physics::ShapeConfiguration* m_shapeConfig = nullptr; + AZ::Transform m_colliderOffSetTransform; + const AZ::u32 m_jointIndex = 0; + }; + + AZ_MULTIPLAYER_COMPONENT(Multiplayer::NetworkHitVolumesComponent, s_networkHitVolumesComponentConcreteUuid, Multiplayer::NetworkHitVolumesComponentBase); + + static void Reflect(AZ::ReflectContext* context); + + NetworkHitVolumesComponent(); + + void OnInit() override; + void OnActivate(Multiplayer::EntityIsMigrating entityIsMigrating) override; + void OnDeactivate(Multiplayer::EntityIsMigrating entityIsMigrating) override; + + private: + void OnPreRender(float deltaTime); + void OnTransformUpdate(const AZ::Transform& transform); + void OnSyncRewind(); + + void CreateHitVolumes(); + void DestroyHitVolumes(); + + //! ActorComponentNotificationBus::Handler + //! @{ + void OnActorInstanceCreated(EMotionFX::ActorInstance* actorInstance) override; + void OnActorInstanceDestroyed(EMotionFX::ActorInstance* actorInstance) override; + //! @} + + Physics::CharacterRequests* m_physicsCharacter = nullptr; + EMotionFX::Integration::ActorComponentRequests* m_actorComponent = nullptr; + const Physics::CharacterColliderConfiguration* m_hitDetectionConfig = nullptr; + + AZStd::vector m_animatedHitVolumes; + + Multiplayer::EntitySyncRewindEvent::Handler m_syncRewindHandler; + Multiplayer::EntityPreRenderEvent::Handler m_preRenderHandler; + AZ::TransformChangedEvent::Handler m_transformChangedHandler; + }; +} diff --git a/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkRigidBodyComponent.h b/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkRigidBodyComponent.h new file mode 100644 index 0000000000..19379fc959 --- /dev/null +++ b/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkRigidBodyComponent.h @@ -0,0 +1,68 @@ +/* + * 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 + +#include +#include +#include + +namespace Physics +{ + class RigidBodyRequests; +} + +namespace Multiplayer +{ + //! Bus for requests to the network rigid body component. + class NetworkRigidBodyRequests : public AZ::ComponentBus + { + }; + using NetworkRigidBodyRequestBus = AZ::EBus; + + class NetworkRigidBodyComponent final + : public NetworkRigidBodyComponentBase + , private NetworkRigidBodyRequestBus::Handler + { + friend class NetworkRigidBodyComponentController; + + public: + AZ_MULTIPLAYER_COMPONENT( + Multiplayer::NetworkRigidBodyComponent, s_networkRigidBodyComponentConcreteUuid, Multiplayer::NetworkRigidBodyComponentBase); + + static void Reflect(AZ::ReflectContext* context); + static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided); + static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required); + static void GetDependentServices(AZ::ComponentDescriptor::DependencyArrayType& dependent); + + NetworkRigidBodyComponent(); + + void OnInit() override; + void OnActivate(Multiplayer::EntityIsMigrating entityIsMigrating) override; + void OnDeactivate(Multiplayer::EntityIsMigrating entityIsMigrating) override; + + private: + void OnTransformUpdate(const AZ::Transform& worldTm); + void OnSyncRewind(); + + Multiplayer::EntitySyncRewindEvent::Handler m_syncRewindHandler; + AZ::TransformChangedEvent::Handler m_transformChangedHandler; + Physics::RigidBodyRequests* m_physicsRigidBodyComponent = nullptr; + Multiplayer::RewindableObject m_transform; + }; + + class NetworkRigidBodyComponentController + : public NetworkRigidBodyComponentControllerBase + { + public: + NetworkRigidBodyComponentController(NetworkRigidBodyComponent& parent); + + void OnActivate(Multiplayer::EntityIsMigrating entityIsMigrating) override; + void OnDeactivate(Multiplayer::EntityIsMigrating entityIsMigrating) override; + + void HandleSendApplyImpulse(AzNetworking::IConnection* invokingConnection, const AZ::Vector3& impulse, const AZ::Vector3& worldPoint) override; + }; +} // namespace Multiplayer diff --git a/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkTransformComponent.h b/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkTransformComponent.h index 969b59a672..d90e7cddfe 100644 --- a/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkTransformComponent.h +++ b/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkTransformComponent.h @@ -29,26 +29,9 @@ namespace Multiplayer void OnDeactivate(Multiplayer::EntityIsMigrating entityIsMigrating) override; private: - void OnPreRender(float deltaTime, float blendFactor); + void OnPreRender(float deltaTime); void OnCorrection(); - void OnRotationChangedEvent(const AZ::Quaternion& rotation); - void OnTranslationChangedEvent(const AZ::Vector3& translation); - void OnScaleChangedEvent(float scale); - void OnResetCountChangedEvent(); - void OnParentIdChangedEvent(NetEntityId newParent); - - void UpdateTargetHostFrameId(); - - AZ::Transform m_previousTransform = AZ::Transform::CreateIdentity(); - AZ::Transform m_targetTransform = AZ::Transform::CreateIdentity(); - - AZ::Event::Handler m_rotationEventHandler; - AZ::Event::Handler m_translationEventHandler; - AZ::Event::Handler m_scaleEventHandler; - AZ::Event::Handler m_resetCountEventHandler; - AZ::Event::Handler m_parentIdChangedEventHandler; - EntityPreRenderEvent::Handler m_entityPreRenderEventHandler; EntityCorrectionEvent::Handler m_entityCorrectionEventHandler; diff --git a/Gems/Multiplayer/Code/Include/Multiplayer/IMultiplayer.h b/Gems/Multiplayer/Code/Include/Multiplayer/IMultiplayer.h index 4f714068db..e32b6188eb 100644 --- a/Gems/Multiplayer/Code/Include/Multiplayer/IMultiplayer.h +++ b/Gems/Multiplayer/Code/Include/Multiplayer/IMultiplayer.h @@ -193,15 +193,13 @@ namespace Multiplayer m_previousHostFrameId = time->GetHostFrameId(); m_previousHostTimeMs = time->GetHostTimeMs(); m_previousRewindConnectionId = time->GetRewindingConnectionId(); - time->AlterTime(frameId, timeMs, connectionId); m_previousBlendFactor = time->GetHostBlendFactor(); - time->AlterBlendFactor(blendFactor); + time->AlterTime(frameId, timeMs, blendFactor, connectionId); } inline ~ScopedAlterTime() { INetworkTime* time = GetNetworkTime(); - time->AlterTime(m_previousHostFrameId, m_previousHostTimeMs, m_previousRewindConnectionId); - time->AlterBlendFactor(m_previousBlendFactor); + time->AlterTime(m_previousHostFrameId, m_previousHostTimeMs, m_previousBlendFactor, m_previousRewindConnectionId); } private: HostFrameId m_previousHostFrameId = InvalidHostFrameId; diff --git a/Gems/Multiplayer/Code/Include/Multiplayer/NetworkTime/INetworkTime.h b/Gems/Multiplayer/Code/Include/Multiplayer/NetworkTime/INetworkTime.h index ae47aa8373..c12cbb660b 100644 --- a/Gems/Multiplayer/Code/Include/Multiplayer/NetworkTime/INetworkTime.h +++ b/Gems/Multiplayer/Code/Include/Multiplayer/NetworkTime/INetworkTime.h @@ -52,12 +52,6 @@ namespace Multiplayer //! @return the ConnectionId of the connection requesting the rewind operation virtual AzNetworking::ConnectionId GetRewindingConnectionId() const = 0; - //! Get the controlling connection that may be currently altering global game time. - //! Note this abstraction is required at a relatively high level to allow for 'don't rewind the shooter' semantics - //! @param rewindConnectionId if this parameter matches the current rewindConnectionId, it will return the unaltered hostFrameId - //! @return the HostFrameId taking into account the provided rewinding connectionId - virtual HostFrameId GetHostFrameIdForRewindingConnection(AzNetworking::ConnectionId rewindConnectionId) const = 0; - //! Forcibly sets the current network time to the provided frameId and game time in milliseconds. //! @param frameId the new HostFrameId to use //! @param timeMs the new HostTimeMs to use @@ -66,12 +60,9 @@ namespace Multiplayer //! Alters the current HostFrameId and binds that alteration to the provided ConnectionId. //! @param frameId the new HostFrameId to use //! @param timeMs the new HostTimeMs to use + //! @param blendFactor the factor used to blend between values at the current and previous HostFrameId //! @param rewindConnectionId the rewinding ConnectionId - virtual void AlterTime(HostFrameId frameId, AZ::TimeMs timeMs, AzNetworking::ConnectionId rewindConnectionId) = 0; - - //! Alters the current Host blend factor. Used to drive interpolation in rewound states. - //! @param blendFactor the blend factor to use - virtual void AlterBlendFactor(float blendFactor) = 0; + virtual void AlterTime(HostFrameId frameId, AZ::TimeMs timeMs, float blendFactor, AzNetworking::ConnectionId rewindConnectionId) = 0; //! Syncs all entities contained within a volume to the current rewind state. //! @param rewindVolume the volume to rewind entities within (needed for physics entities) diff --git a/Gems/Multiplayer/Code/Include/Multiplayer/NetworkTime/RewindableObject.h b/Gems/Multiplayer/Code/Include/Multiplayer/NetworkTime/RewindableObject.h index a8af564c15..152f6f47a7 100644 --- a/Gems/Multiplayer/Code/Include/Multiplayer/NetworkTime/RewindableObject.h +++ b/Gems/Multiplayer/Code/Include/Multiplayer/NetworkTime/RewindableObject.h @@ -60,7 +60,7 @@ namespace Multiplayer //! @return value in const base type form const BASE_TYPE& Get() const; - //! Const base type retriever for one host frame behind Get(). Only intended for use in SyncRewind contexts. + //! Const base type retriever for one host frame behind Get() when contextually appropriate, otherwise identical to Get(). //! @return value in const base type form const BASE_TYPE& GetPrevious() const; @@ -86,9 +86,13 @@ namespace Multiplayer private: //! Returns what the appropriate current time is for this rewindable property. - //! @return the appropriate current time is for this rewindable property + //! @return the appropriate current time for this rewindable property HostFrameId GetCurrentTimeForProperty() const; + //! Returns what the appropriate previous time is for this rewindable property. + //! @return the appropriate previous time for this rewindable property + HostFrameId GetPreviousTimeForProperty() const; + //! Updates the latest value for this object instance, if frameTime represents a current or future time. //! Any attempts to set old values on the object will fail //! @param value the new value to set in the object history diff --git a/Gems/Multiplayer/Code/Include/Multiplayer/NetworkTime/RewindableObject.inl b/Gems/Multiplayer/Code/Include/Multiplayer/NetworkTime/RewindableObject.inl index b0c9bc0c46..9183a1e9da 100644 --- a/Gems/Multiplayer/Code/Include/Multiplayer/NetworkTime/RewindableObject.inl +++ b/Gems/Multiplayer/Code/Include/Multiplayer/NetworkTime/RewindableObject.inl @@ -69,7 +69,7 @@ namespace Multiplayer template inline const BASE_TYPE& RewindableObject::GetPrevious() const { - return GetValueForTime(GetCurrentTimeForProperty() - HostFrameId(1)); + return GetValueForTime(GetPreviousTimeForProperty()); } template @@ -118,7 +118,22 @@ namespace Multiplayer inline HostFrameId RewindableObject::GetCurrentTimeForProperty() const { INetworkTime* networkTime = Multiplayer::GetNetworkTime(); - return networkTime->GetHostFrameIdForRewindingConnection(m_owningConnectionId); + if (networkTime->IsTimeRewound() && (m_owningConnectionId == networkTime->GetRewindingConnectionId())) + { + return networkTime->GetUnalteredHostFrameId(); + } + return networkTime->GetHostFrameId(); + } + + template + inline HostFrameId RewindableObject::GetPreviousTimeForProperty() const + { + INetworkTime* networkTime = Multiplayer::GetNetworkTime(); + if (networkTime->IsTimeRewound() && (m_owningConnectionId == networkTime->GetRewindingConnectionId())) + { + return networkTime->GetUnalteredHostFrameId(); + } + return networkTime->GetHostFrameId() - HostFrameId(1); } template diff --git a/Gems/Multiplayer/Code/Source/AutoGen/AutoComponentTypes_Source.jinja b/Gems/Multiplayer/Code/Source/AutoGen/AutoComponentTypes_Source.jinja index 7bb4bdcc2c..c551c8bc20 100644 --- a/Gems/Multiplayer/Code/Source/AutoGen/AutoComponentTypes_Source.jinja +++ b/Gems/Multiplayer/Code/Source/AutoGen/AutoComponentTypes_Source.jinja @@ -14,7 +14,7 @@ {% set Namespace = dataFiles[0].attrib['Namespace'] %} {% for Component in dataFiles %} {% if Component.attrib['Namespace'] != Namespace %} -#error "mismatched component namespaces detected in declared multiplayer components, expected {{ Namespace }} but found {{ Component.attrib['Namespace'] }}" +#error "mismatched component namespaces detected in declared multiplayer components, expected {{ Namespace }} but {{ Component.attrib['Name'] }} is using {{ Component.attrib['Namespace'] }} namespace." {% endif %} {% endfor %} namespace {{ Namespace }} diff --git a/Gems/Multiplayer/Code/Source/AutoGen/AutoComponent_Header.jinja b/Gems/Multiplayer/Code/Source/AutoGen/AutoComponent_Header.jinja index 0f62da11f3..41f0fa1b02 100644 --- a/Gems/Multiplayer/Code/Source/AutoGen/AutoComponent_Header.jinja +++ b/Gems/Multiplayer/Code/Source/AutoGen/AutoComponent_Header.jinja @@ -7,21 +7,21 @@ {% macro DeclareNetworkPropertyGetter(Property) %} {% set PropertyName = UpperFirst(Property.attrib['Name']) %} {% if Property.attrib['Container'] == 'Array' %} -{% if Property.attrib['IsRewindable']|booleanTrue %} +{% if Property.attrib['IsRewindable']|booleanTrue %} const RewindableArray<{{ Property.attrib['Type'] }}, {{ Property.attrib['Count'] }}>& Get{{ PropertyName }}Array() const; -{% else %} +{% else %} const AZStd::array<{{ Property.attrib['Type'] }}, {{ Property.attrib['Count'] }}>& Get{{ PropertyName }}Array() const; -{% endif %} +{% endif %} const {{ Property.attrib['Type'] }}& Get{{ PropertyName }}(int32_t index) const; {% if Property.attrib['GenerateEventBindings']|booleanTrue %} void {{ PropertyName }}AddEvent(AZ::Event::Handler& handler); {% endif %} {% elif Property.attrib['Container'] == 'Vector' %} -{% if Property.attrib['IsRewindable']|booleanTrue %} +{% if Property.attrib['IsRewindable']|booleanTrue %} const RewindableFixedVector<{{ Property.attrib['Type'] }}, {{ Property.attrib['Count'] }}>& Get{{ PropertyName }}Vector() const; -{% else %} +{% else %} const AZStd::fixed_vector<{{ Property.attrib['Type'] }}, {{ Property.attrib['Count'] }}>& Get{{ PropertyName }}Vector() const; -{% endif %} +{% endif %} const {{ Property.attrib['Type'] }}& Get{{ PropertyName }}(int32_t index) const; const {{ Property.attrib['Type'] }}& {{ PropertyName }}GetBack() const; uint32_t {{ PropertyName }}GetSize() const; @@ -31,6 +31,9 @@ void {{ PropertyName }}SizeChangedAddEvent(AZ::Event::Handler& handler {% endif %} {% else %} const {{ Property.attrib['Type'] }}& Get{{ PropertyName }}() const; +{% if Property.attrib['IsRewindable']|booleanTrue %} +const {{ Property.attrib['Type'] }}& Get{{ PropertyName }}Previous() const; +{% endif %} {% if Property.attrib['GenerateEventBindings']|booleanTrue %} void {{ PropertyName }}AddEvent(AZ::Event<{{ Property.attrib['Type'] }}>::Handler& handler); {% endif %} diff --git a/Gems/Multiplayer/Code/Source/AutoGen/AutoComponent_Source.jinja b/Gems/Multiplayer/Code/Source/AutoGen/AutoComponent_Source.jinja index e57e8dad2f..eb4b81ea96 100644 --- a/Gems/Multiplayer/Code/Source/AutoGen/AutoComponent_Source.jinja +++ b/Gems/Multiplayer/Code/Source/AutoGen/AutoComponent_Source.jinja @@ -3,11 +3,11 @@ {% macro LowerFirst(text) %}{{ text[0] | lower}}{{ text[1:] }}{% endmacro %} {% macro DefineNetworkPropertyGet(ClassName, Property, Prefix = '') %} {% if Property.attrib['Container'] == 'Array' %} -{% if Property.attrib['IsRewindable']|booleanTrue %} +{% if Property.attrib['IsRewindable']|booleanTrue %} const RewindableArray<{{ Property.attrib['Type'] }}, {{ Property.attrib['Count'] }}>& {{ ClassName }}::Get{{ UpperFirst(Property.attrib['Name']) }}Array() const -{% else %} +{% else %} const AZStd::array<{{ Property.attrib['Type'] }}, {{ Property.attrib['Count'] }}>& {{ ClassName }}::Get{{ UpperFirst(Property.attrib['Name']) }}Array() const -{% endif %} +{% endif %} { return {{ Prefix }}m_{{ LowerFirst(Property.attrib['Name']) }}; } @@ -25,11 +25,11 @@ void {{ ClassName }}::{{ UpperFirst(Property.attrib['Name']) }}AddEvent(AZ::Even {% endif %} {% elif Property.attrib['Container'] == 'Vector' %} -{% if Property.attrib['IsRewindable']|booleanTrue %} +{% if Property.attrib['IsRewindable']|booleanTrue %} const RewindableFixedVector<{{ Property.attrib['Type'] }}, {{ Property.attrib['Count'] }}>& {{ ClassName }}::Get{{ UpperFirst(Property.attrib['Name']) }}Vector() const -{% else %} +{% else %} const AZStd::fixed_vector<{{ Property.attrib['Type'] }}, {{ Property.attrib['Count'] }}>& {{ ClassName }}::Get{{ UpperFirst(Property.attrib['Name']) }}Vector() const -{% endif %} +{% endif %} { return {{ Prefix }}m_{{ LowerFirst(Property.attrib['Name']) }}; } @@ -68,7 +68,12 @@ const {{ Property.attrib['Type'] }}& {{ ClassName }}::Get{{ UpperFirst(Property. { return {{ Prefix }}m_{{ LowerFirst(Property.attrib['Name']) }}; } - +{% if Property.attrib['IsRewindable']|booleanTrue %} +const {{ Property.attrib['Type'] }}& {{ ClassName }}::Get{{ UpperFirst(Property.attrib['Name']) }}Previous() const +{ + return {{ Prefix }}m_{{ LowerFirst(Property.attrib['Name']) }}.GetPrevious(); +} +{% endif %} {% if Property.attrib['GenerateEventBindings']|booleanTrue %} void {{ ClassName }}::{{ UpperFirst(Property.attrib['Name']) }}AddEvent(AZ::Event<{{ Property.attrib['Type'] }}>::Handler& handler) { diff --git a/Gems/Multiplayer/Code/Source/AutoGen/Multiplayer.AutoPackets.xml b/Gems/Multiplayer/Code/Source/AutoGen/Multiplayer.AutoPackets.xml index 203750d761..c553bc5351 100644 --- a/Gems/Multiplayer/Code/Source/AutoGen/Multiplayer.AutoPackets.xml +++ b/Gems/Multiplayer/Code/Source/AutoGen/Multiplayer.AutoPackets.xml @@ -32,11 +32,11 @@ - + - + diff --git a/Gems/Multiplayer/Code/Source/AutoGen/NetworkCharacterComponent.AutoComponent.xml b/Gems/Multiplayer/Code/Source/AutoGen/NetworkCharacterComponent.AutoComponent.xml new file mode 100644 index 0000000000..83e15800e0 --- /dev/null +++ b/Gems/Multiplayer/Code/Source/AutoGen/NetworkCharacterComponent.AutoComponent.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/Gems/Multiplayer/Code/Source/AutoGen/NetworkHitVolumesComponent.AutoComponent.xml b/Gems/Multiplayer/Code/Source/AutoGen/NetworkHitVolumesComponent.AutoComponent.xml new file mode 100644 index 0000000000..d3c31a5bc0 --- /dev/null +++ b/Gems/Multiplayer/Code/Source/AutoGen/NetworkHitVolumesComponent.AutoComponent.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/Gems/Multiplayer/Code/Source/AutoGen/NetworkRigidBodyComponent.AutoComponent.xml b/Gems/Multiplayer/Code/Source/AutoGen/NetworkRigidBodyComponent.AutoComponent.xml new file mode 100644 index 0000000000..b6cdfca9c2 --- /dev/null +++ b/Gems/Multiplayer/Code/Source/AutoGen/NetworkRigidBodyComponent.AutoComponent.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + diff --git a/Gems/Multiplayer/Code/Source/AutoGen/NetworkTransformComponent.AutoComponent.xml b/Gems/Multiplayer/Code/Source/AutoGen/NetworkTransformComponent.AutoComponent.xml index cec005cc26..8ab2e61e5e 100644 --- a/Gems/Multiplayer/Code/Source/AutoGen/NetworkTransformComponent.AutoComponent.xml +++ b/Gems/Multiplayer/Code/Source/AutoGen/NetworkTransformComponent.AutoComponent.xml @@ -12,9 +12,9 @@ - + - + diff --git a/Gems/Multiplayer/Code/Source/Components/LocalPredictionPlayerInputComponent.cpp b/Gems/Multiplayer/Code/Source/Components/LocalPredictionPlayerInputComponent.cpp index 418bea79dd..c5f9b265f9 100644 --- a/Gems/Multiplayer/Code/Source/Components/LocalPredictionPlayerInputComponent.cpp +++ b/Gems/Multiplayer/Code/Source/Components/LocalPredictionPlayerInputComponent.cpp @@ -185,12 +185,9 @@ namespace Multiplayer // Discard move input events, client may be speed hacking if (m_clientBankedTime < sv_MaxBankTimeWindowSec) { - // Client blends from previous frame to target so here we subtract blend factor to get to that state - const float blendFactor = AZStd::min(AZStd::max(0.f, input.GetHostBlendFactor()), 1.0f); - const AZ::TimeMs blendMs = AZ::TimeMs(static_cast(static_cast(cl_InputRateMs)) * (1.0f - blendFactor)); m_clientBankedTime = AZStd::min(m_clientBankedTime + clientInputRateSec, (double)sv_MaxBankTimeWindowSec); // clamp to boundary { - ScopedAlterTime scopedTime(input.GetHostFrameId(), input.GetHostTimeMs() - blendMs, input.GetHostBlendFactor(), invokingConnection->GetConnectionId()); + ScopedAlterTime scopedTime(input.GetHostFrameId(), input.GetHostTimeMs(), input.GetHostBlendFactor(), invokingConnection->GetConnectionId()); GetNetBindComponent()->ProcessInput(input, static_cast(clientInputRateSec)); } @@ -436,10 +433,13 @@ namespace Multiplayer NetworkInputArray inputArray(GetEntityHandle()); NetworkInput& input = inputArray[0]; + const float blendFactor = AZStd::min(AZStd::max(0.f, multiplayer->GetCurrentBlendFactor()), 1.0f); + const AZ::TimeMs blendMs = AZ::TimeMs(static_cast(static_cast(cl_InputRateMs)) * (1.0f - blendFactor)); input.SetClientInputId(m_clientInputId); input.SetHostFrameId(networkTime->GetHostFrameId()); - input.SetHostTimeMs(multiplayer->GetCurrentHostTimeMs()); + // Account for the client blending from previous frame to current + input.SetHostTimeMs(multiplayer->GetCurrentHostTimeMs() - blendMs); input.SetHostBlendFactor(multiplayer->GetCurrentBlendFactor()); // Allow components to form the input for this frame diff --git a/Gems/Multiplayer/Code/Source/Components/NetBindComponent.cpp b/Gems/Multiplayer/Code/Source/Components/NetBindComponent.cpp index 43577525c0..634cac74b2 100644 --- a/Gems/Multiplayer/Code/Source/Components/NetBindComponent.cpp +++ b/Gems/Multiplayer/Code/Source/Components/NetBindComponent.cpp @@ -405,9 +405,9 @@ namespace Multiplayer m_entityServerMigrationEvent.Signal(m_netEntityHandle, hostId, connectionId); } - void NetBindComponent::NotifyPreRender(float deltaTime, float blendFactor) + void NetBindComponent::NotifyPreRender(float deltaTime) { - m_entityPreRenderEvent.Signal(deltaTime, blendFactor); + m_entityPreRenderEvent.Signal(deltaTime); } void NetBindComponent::NotifyCorrection() diff --git a/Gems/Multiplayer/Code/Source/Components/NetworkCharacterComponent.cpp b/Gems/Multiplayer/Code/Source/Components/NetworkCharacterComponent.cpp new file mode 100644 index 0000000000..b14fc8761f --- /dev/null +++ b/Gems/Multiplayer/Code/Source/Components/NetworkCharacterComponent.cpp @@ -0,0 +1,209 @@ +/* + * 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Multiplayer +{ + + bool CollisionLayerBasedControllerFilter(const physx::PxController& controllerA, const physx::PxController& controllerB) + { + PHYSX_SCENE_READ_LOCK(controllerA.getActor()->getScene()); + physx::PxRigidDynamic* actorA = controllerA.getActor(); + physx::PxRigidDynamic* actorB = controllerB.getActor(); + + if (actorA && actorA->getNbShapes() > 0 && actorB && actorB->getNbShapes() > 0) + { + physx::PxShape* shapeA = nullptr; + actorA->getShapes(&shapeA, 1, 0); + physx::PxFilterData filterDataA = shapeA->getSimulationFilterData(); + physx::PxShape* shapeB = nullptr; + actorB->getShapes(&shapeB, 1, 0); + physx::PxFilterData filterDataB = shapeB->getSimulationFilterData(); + return PhysX::Utils::Collision::ShouldCollide(filterDataA, filterDataB); + } + + return true; + } + + physx::PxQueryHitType::Enum CollisionLayerBasedObjectPreFilter( + const physx::PxFilterData& filterData, + const physx::PxShape* shape, + const physx::PxRigidActor* actor, + [[maybe_unused]] physx::PxHitFlags& queryFlags) + { + // non-kinematic dynamic bodies should not impede the movement of the character + if (actor->getConcreteType() == physx::PxConcreteType::eRIGID_DYNAMIC) + { + const physx::PxRigidDynamic* rigidDynamic = static_cast(actor); + + bool isKinematic = (rigidDynamic->getRigidBodyFlags() & physx::PxRigidBodyFlag::eKINEMATIC); + if (isKinematic) + { + const PhysX::ActorData* actorData = PhysX::Utils::GetUserData(rigidDynamic); + if (actorData) + { + const AZ::EntityId entityId = actorData->GetEntityId(); + + if (Multiplayer::NetworkRigidBodyRequestBus::FindFirstHandler(entityId) != nullptr) + { + // Network rigid bodies are kinematic on the client but dynamic on the server, + // hence filtering treats these actors as dynamic to support client prediction and avoid desyncs + isKinematic = false; + } + } + } + + if (!isKinematic) + { + return physx::PxQueryHitType::eNONE; + } + } + + // all other cases should be determined by collision filters + if (PhysX::Utils::Collision::ShouldCollide(filterData, shape->getSimulationFilterData())) + { + return physx::PxQueryHitType::eBLOCK; + } + + return physx::PxQueryHitType::eNONE; + } + + void NetworkCharacterComponent::NetworkCharacterComponent::Reflect(AZ::ReflectContext* context) + { + AZ::SerializeContext* serializeContext = azrtti_cast(context); + if (serializeContext) + { + serializeContext->Class() + ->Version(1); + } + NetworkCharacterComponentBase::Reflect(context); + } + + NetworkCharacterComponent::NetworkCharacterComponent() + : m_translationEventHandler([this](const AZ::Vector3& translation) { OnTranslationChangedEvent(translation); }) + { + } + + void NetworkCharacterComponent::OnActivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating) + { + Physics::CharacterRequests* characterRequests = Physics::CharacterRequestBus::FindFirstHandler(GetEntityId()); + m_physicsCharacter = (characterRequests != nullptr) ? characterRequests->GetCharacter() : nullptr; + GetNetBindComponent()->AddEntitySyncRewindEventHandler(m_syncRewindHandler); + + if (m_physicsCharacter) + { + auto controller = static_cast(m_physicsCharacter); + controller->SetFilterFlags(physx::PxQueryFlag::eSTATIC | physx::PxQueryFlag::eDYNAMIC | physx::PxQueryFlag::ePREFILTER); + if (auto callbackManager = controller->GetCallbackManager()) + { + callbackManager->SetControllerFilter(CollisionLayerBasedControllerFilter); + callbackManager->SetObjectPreFilter(CollisionLayerBasedObjectPreFilter); + } + } + + if (!HasController()) + { + GetNetworkTransformComponent()->TranslationAddEvent(m_translationEventHandler); + } + } + + void NetworkCharacterComponent::OnDeactivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating) + { + ; + } + + void NetworkCharacterComponent::OnTranslationChangedEvent([[maybe_unused]] const AZ::Vector3& translation) + { + OnSyncRewind(); + } + + void NetworkCharacterComponent::OnSyncRewind() + { + if (m_physicsCharacter == nullptr) + { + return; + } + + const AZ::Vector3 currPosition = m_physicsCharacter->GetBasePosition(); + if (!currPosition.IsClose(GetNetworkTransformComponent()->GetTranslation())) + { + uint32_t frameId = static_cast(Multiplayer::GetNetworkTime()->GetHostFrameId()); + m_physicsCharacter->SetFrameId(frameId); + //m_physicsCharacter->SetBasePosition(GetNetworkTransformComponent()->GetTranslation()); + } + } + + bool NetworkCharacterComponent::IsOnGround() const + { + auto pxController = static_cast(m_physicsCharacter->GetNativePointer()); + if (!pxController) + { + return true; + } + + physx::PxControllerState state; + pxController->getState(state); + return state.touchedActor != nullptr || (state.collisionFlags & physx::PxControllerCollisionFlag::eCOLLISION_DOWN) != 0; + } + + NetworkCharacterComponentController::NetworkCharacterComponentController(NetworkCharacterComponent& parent) + : NetworkCharacterComponentControllerBase(parent) + { + ; + } + + void NetworkCharacterComponentController::OnActivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating) + { + ; + } + + void NetworkCharacterComponentController::OnDeactivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating) + { + ; + } + + AZ::Vector3 NetworkCharacterComponentController::TryMoveWithVelocity(const AZ::Vector3& velocity, [[maybe_unused]] float deltaTime) + { + // Ensure any entities that we might interact with are properly synchronized to their rewind state + if (IsAuthority()) + { + const AZ::Aabb entityStartBounds = AZ::Interface::Get()->GetEntityLocalBoundsUnion(GetEntity()->GetId()); + const AZ::Aabb entityFinalBounds = entityStartBounds.GetTranslated(velocity); + AZ::Aabb entitySweptBounds = entityStartBounds; + entitySweptBounds.AddAabb(entityFinalBounds); + Multiplayer::GetNetworkTime()->SyncEntitiesToRewindState(entitySweptBounds); + } + + if ((GetParent().m_physicsCharacter == nullptr) || (velocity.GetLengthSq() <= 0.0f)) + { + return GetEntity()->GetTransform()->GetWorldTranslation(); + } + GetParent().m_physicsCharacter->AddVelocity(velocity); + GetParent().m_physicsCharacter->ApplyRequestedVelocity(deltaTime); + GetEntity()->GetTransform()->SetWorldTranslation(GetParent().m_physicsCharacter->GetBasePosition()); + AZLOG + ( + NET_Movement, + "Moved to position %f x %f x %f", + GetParent().m_physicsCharacter->GetBasePosition().GetX(), + GetParent().m_physicsCharacter->GetBasePosition().GetY(), + GetParent().m_physicsCharacter->GetBasePosition().GetZ() + ); + return GetEntity()->GetTransform()->GetWorldTranslation(); + } +} diff --git a/Gems/Multiplayer/Code/Source/Components/NetworkHitVolumesComponent.cpp b/Gems/Multiplayer/Code/Source/Components/NetworkHitVolumesComponent.cpp new file mode 100644 index 0000000000..c14bb643e1 --- /dev/null +++ b/Gems/Multiplayer/Code/Source/Components/NetworkHitVolumesComponent.cpp @@ -0,0 +1,221 @@ +/* + * 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 +#include +#include +#include +#include +#include +#include +#include + +namespace Multiplayer +{ + AZ_CVAR(bool, bg_DrawArticulatedHitVolumes, false, nullptr, AZ::ConsoleFunctorFlags::Null, "Enables debug draw of articulated hit volumes"); + AZ_CVAR(float, bg_DrawDebugHitVolumeLifetime, 0.0f, nullptr, AZ::ConsoleFunctorFlags::Null, "The lifetime for hit volume draw-debug shapes"); + + AZ_CVAR(float, bg_RewindPositionTolerance, 0.0001f, nullptr, AZ::ConsoleFunctorFlags::Null, "Don't sync the physx entity if the square of delta position is less than this value"); + AZ_CVAR(float, bg_RewindOrientationTolerance, 0.001f, nullptr, AZ::ConsoleFunctorFlags::Null, "Don't sync the physx entity if the square of delta orientation is less than this value"); + + NetworkHitVolumesComponent::AnimatedHitVolume::AnimatedHitVolume + ( + AzNetworking::ConnectionId connectionId, + Physics::CharacterRequests* character, + const char* hitVolumeName, + const Physics::ColliderConfiguration* colliderConfig, + const Physics::ShapeConfiguration* shapeConfig, + const uint32_t jointIndex + ) + : m_colliderConfig(colliderConfig) + , m_shapeConfig(shapeConfig) + , m_jointIndex(jointIndex) + { + m_transform.SetOwningConnectionId(connectionId); + + m_colliderOffSetTransform = AZ::Transform::CreateFromQuaternionAndTranslation(m_colliderConfig->m_rotation, m_colliderConfig->m_position); + + if (m_colliderConfig->m_isExclusive) + { + Physics::SystemRequestBus::BroadcastResult(m_physicsShape, &Physics::SystemRequests::CreateShape, *m_colliderConfig, *m_shapeConfig); + } + else + { + Physics::ColliderConfiguration colliderConfiguration = *m_colliderConfig; + colliderConfiguration.m_isExclusive = true; + colliderConfiguration.m_isSimulated = false; + colliderConfiguration.m_isInSceneQueries = true; + Physics::SystemRequestBus::BroadcastResult(m_physicsShape, &Physics::SystemRequests::CreateShape, colliderConfiguration, *m_shapeConfig); + } + + if (m_physicsShape) + { + m_physicsShape->SetName(hitVolumeName); + character->GetCharacter()->AttachShape(m_physicsShape); + } + } + + void NetworkHitVolumesComponent::AnimatedHitVolume::UpdateTransform(const AZ::Transform& transform) + { + m_transform = transform; + m_physicsShape->SetLocalPose(transform.GetTranslation(), transform.GetRotation()); + } + + void NetworkHitVolumesComponent::AnimatedHitVolume::SyncToCurrentTransform() + { + AZ::Transform rewoundTransform; + const AZ::Transform& targetTransform = m_transform.Get(); + const float blendFactor = Multiplayer::GetNetworkTime()->GetHostBlendFactor(); + if (blendFactor < 1.f) + { + // If a blend factor was supplied, interpolate the transform appropriately + const AZ::Transform& previousTransform = m_transform.GetPrevious(); + rewoundTransform.SetRotation(previousTransform.GetRotation().Slerp(targetTransform.GetRotation(), blendFactor)); + rewoundTransform.SetTranslation(previousTransform.GetTranslation().Lerp(targetTransform.GetTranslation(), blendFactor)); + rewoundTransform.SetUniformScale(AZ::Lerp(previousTransform.GetUniformScale(), targetTransform.GetUniformScale(), blendFactor)); + } + else + { + rewoundTransform = m_transform.Get(); + } + + const AZ::Transform physicsTransform = AZ::Transform::CreateFromQuaternionAndTranslation(m_physicsShape->GetLocalPose().second, m_physicsShape->GetLocalPose().first); + + // Don't call SetLocalPose unless the transforms are actually different + const AZ::Vector3 positionDelta = physicsTransform.GetTranslation() - rewoundTransform.GetTranslation(); + const AZ::Quaternion orientationDelta = physicsTransform.GetRotation() - rewoundTransform.GetRotation(); + + if ((positionDelta.GetLengthSq() >= bg_RewindPositionTolerance) || (orientationDelta.GetLengthSq() >= bg_RewindOrientationTolerance)) + { + m_physicsShape->SetLocalPose(rewoundTransform.GetTranslation(), rewoundTransform.GetRotation()); + } + } + + void NetworkHitVolumesComponent::NetworkHitVolumesComponent::Reflect(AZ::ReflectContext* context) + { + AZ::SerializeContext* serializeContext = azrtti_cast(context); + if (serializeContext) + { + serializeContext->Class() + ->Version(1); + } + NetworkHitVolumesComponentBase::Reflect(context); + } + + NetworkHitVolumesComponent::NetworkHitVolumesComponent() + : m_syncRewindHandler([this]() { OnSyncRewind(); }) + , m_preRenderHandler([this](float deltaTime) { OnPreRender(deltaTime); }) + , m_transformChangedHandler([this](const AZ::Transform&, const AZ::Transform& worldTm) { OnTransformUpdate(worldTm); }) + { + ; + } + + void NetworkHitVolumesComponent::OnInit() + { + ; + } + + void NetworkHitVolumesComponent::OnActivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating) + { + EMotionFX::Integration::ActorComponentNotificationBus::Handler::BusConnect(GetEntityId()); + GetNetBindComponent()->AddEntitySyncRewindEventHandler(m_syncRewindHandler); + m_physicsCharacter = Physics::CharacterRequestBus::FindFirstHandler(GetEntityId()); + GetTransformComponent()->BindTransformChangedEventHandler(m_transformChangedHandler); + OnTransformUpdate(GetTransformComponent()->GetWorldTM()); + } + + void NetworkHitVolumesComponent::OnDeactivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating) + { + DestroyHitVolumes(); + EMotionFX::Integration::ActorComponentNotificationBus::Handler::BusDisconnect(); + } + + void NetworkHitVolumesComponent::OnPreRender([[maybe_unused]] float deltaTime) + { + if (m_animatedHitVolumes.size() <= 0) + { + CreateHitVolumes(); + } + + AZ::Vector3 position, scale; + AZ::Quaternion rotation; + for (AnimatedHitVolume& hitVolume : m_animatedHitVolumes) + { + m_actorComponent->GetJointTransformComponents(hitVolume.m_jointIndex, EMotionFX::Integration::Space::ModelSpace, position, rotation, scale); + hitVolume.UpdateTransform(AZ::Transform::CreateFromQuaternionAndTranslation(rotation, position) * hitVolume.m_colliderOffSetTransform); + } + } + + void NetworkHitVolumesComponent::OnTransformUpdate([[maybe_unused]] const AZ::Transform& transform) + { + OnSyncRewind(); + } + + void NetworkHitVolumesComponent::OnSyncRewind() + { + if (m_physicsCharacter && m_physicsCharacter->GetCharacter()) + { + uint32_t frameId = static_cast(Multiplayer::GetNetworkTime()->GetHostFrameId()); + m_physicsCharacter->GetCharacter()->SetFrameId(frameId); + } + + for (AnimatedHitVolume& hitVolume : m_animatedHitVolumes) + { + hitVolume.SyncToCurrentTransform(); + } + } + + void NetworkHitVolumesComponent::CreateHitVolumes() + { + if (m_physicsCharacter == nullptr || m_actorComponent == nullptr) + { + return; + } + + const Physics::AnimationConfiguration* physicsConfig = m_actorComponent->GetPhysicsConfig(); + if (physicsConfig == nullptr) + { + return; + } + + m_hitDetectionConfig = &physicsConfig->m_hitDetectionConfig; + const AzNetworking::ConnectionId owningConnectionId = GetNetBindComponent()->GetOwningConnectionId(); + + m_animatedHitVolumes.reserve(m_hitDetectionConfig->m_nodes.size()); + for (const Physics::CharacterColliderNodeConfiguration& nodeConfig : m_hitDetectionConfig->m_nodes) + { + const AZStd::size_t jointIndex = m_actorComponent->GetJointIndexByName(nodeConfig.m_name.c_str()); + if (jointIndex == EMotionFX::Integration::ActorComponentRequests::s_invalidJointIndex) + { + continue; + } + + for (const AzPhysics::ShapeColliderPair& coliderPair : nodeConfig.m_shapes) + { + const Physics::ColliderConfiguration* colliderConfig = coliderPair.first.get(); + Physics::ShapeConfiguration* shapeConfig = coliderPair.second.get(); + m_animatedHitVolumes.emplace_back(owningConnectionId, m_physicsCharacter, nodeConfig.m_name.c_str(), colliderConfig, shapeConfig, aznumeric_cast(jointIndex)); + } + } + } + + void NetworkHitVolumesComponent::DestroyHitVolumes() + { + m_animatedHitVolumes.clear(); + } + + void NetworkHitVolumesComponent::OnActorInstanceCreated([[maybe_unused]] EMotionFX::ActorInstance* actorInstance) + { + m_actorComponent = EMotionFX::Integration::ActorComponentRequestBus::FindFirstHandler(GetEntity()->GetId()); + } + + void NetworkHitVolumesComponent::OnActorInstanceDestroyed([[maybe_unused]] EMotionFX::ActorInstance* actorInstance) + { + m_actorComponent = nullptr; + } +} diff --git a/Gems/Multiplayer/Code/Source/Components/NetworkRigidBodyComponent.cpp b/Gems/Multiplayer/Code/Source/Components/NetworkRigidBodyComponent.cpp new file mode 100644 index 0000000000..bcf855d834 --- /dev/null +++ b/Gems/Multiplayer/Code/Source/Components/NetworkRigidBodyComponent.cpp @@ -0,0 +1,147 @@ +/* + * 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 +#include +#include +#include + +namespace Multiplayer +{ + AZ_CVAR_EXTERNED(float, bg_RewindPositionTolerance); + AZ_CVAR_EXTERNED(float, bg_RewindOrientationTolerance); + + void NetworkRigidBodyComponent::NetworkRigidBodyComponent::Reflect(AZ::ReflectContext* context) + { + AZ::SerializeContext* serializeContext = azrtti_cast(context); + if (serializeContext) + { + serializeContext->Class()->Version(1); + } + NetworkRigidBodyComponentBase::Reflect(context); + } + + void NetworkRigidBodyComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided) + { + provided.push_back(AZ_CRC_CE("NetworkRigidBodyService")); + } + + void NetworkRigidBodyComponent::GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required) + { + required.push_back(AZ_CRC_CE("PhysXRigidBodyService")); + } + + void NetworkRigidBodyComponent::GetDependentServices(AZ::ComponentDescriptor::DependencyArrayType& dependent) + { + dependent.push_back(AZ_CRC_CE("TransformService")); + dependent.push_back(AZ_CRC_CE("PhysXRigidBodyService")); + } + + NetworkRigidBodyComponent::NetworkRigidBodyComponent() + : m_syncRewindHandler([this](){ OnSyncRewind(); }) + , m_transformChangedHandler([this]([[maybe_unused]] const AZ::Transform& localTm, const AZ::Transform& worldTm){ OnTransformUpdate(worldTm); }) + { + } + + void NetworkRigidBodyComponent::OnInit() + { + } + + void NetworkRigidBodyComponent::OnActivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating) + { + NetworkRigidBodyRequestBus::Handler::BusConnect(GetEntityId()); + + GetNetBindComponent()->AddEntitySyncRewindEventHandler(m_syncRewindHandler); + GetEntity()->FindComponent()->BindTransformChangedEventHandler(m_transformChangedHandler); + + m_physicsRigidBodyComponent = + Physics::RigidBodyRequestBus::FindFirstHandler(GetEntity()->GetId()); + AZ_Assert(m_physicsRigidBodyComponent, "PhysX Rigid Body Component is required on entity %s", GetEntity()->GetName().c_str()); + + if (!HasController()) + { + m_physicsRigidBodyComponent->SetKinematic(true); + } + } + + void NetworkRigidBodyComponent::OnDeactivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating) + { + NetworkRigidBodyRequestBus::Handler::BusDisconnect(); + } + + void NetworkRigidBodyComponent::OnTransformUpdate(const AZ::Transform& worldTm) + { + m_transform = worldTm; + + if (!HasController()) + { + m_physicsRigidBodyComponent->SetKinematicTarget(worldTm); + } + } + + void NetworkRigidBodyComponent::OnSyncRewind() + { + uint32_t frameId = static_cast(Multiplayer::GetNetworkTime()->GetHostFrameId()); + + AzPhysics::RigidBody* rigidBody = m_physicsRigidBodyComponent->GetRigidBody(); + rigidBody->SetFrameId(frameId); + + AZ::Transform rewoundTransform; + const AZ::Transform& targetTransform = m_transform.Get(); + const float blendFactor = Multiplayer::GetNetworkTime()->GetHostBlendFactor(); + if (blendFactor < 1.f) + { + // If a blend factor was supplied, interpolate the transform appropriately + const AZ::Transform& previousTransform = m_transform.GetPrevious(); + rewoundTransform.SetRotation(previousTransform.GetRotation().Slerp(targetTransform.GetRotation(), blendFactor)); + rewoundTransform.SetTranslation(previousTransform.GetTranslation().Lerp(targetTransform.GetTranslation(), blendFactor)); + rewoundTransform.SetUniformScale(AZ::Lerp(previousTransform.GetUniformScale(), targetTransform.GetUniformScale(), blendFactor)); + } + else + { + rewoundTransform = m_transform.Get(); + } + const AZ::Transform& physicsTransform = rigidBody->GetTransform(); + + // Don't call SetLocalPose unless the transforms are actually different + const AZ::Vector3 positionDelta = physicsTransform.GetTranslation() - rewoundTransform.GetTranslation(); + const AZ::Quaternion orientationDelta = physicsTransform.GetRotation() - rewoundTransform.GetRotation(); + + if ((positionDelta.GetLengthSq() >= bg_RewindPositionTolerance) || + (orientationDelta.GetLengthSq() >= bg_RewindOrientationTolerance)) + { + rigidBody->SetTransform(rewoundTransform); + } + } + + NetworkRigidBodyComponentController::NetworkRigidBodyComponentController(NetworkRigidBodyComponent& parent) + : NetworkRigidBodyComponentControllerBase(parent) + { + ; + } + + void NetworkRigidBodyComponentController::OnActivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating) + { + ; + } + + void NetworkRigidBodyComponentController::OnDeactivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating) + { + ; + } + + void NetworkRigidBodyComponentController::HandleSendApplyImpulse + ( + [[maybe_unused]] AzNetworking::IConnection* invokingConnection, + const AZ::Vector3& impulse, + const AZ::Vector3& worldPoint + ) + { + AzPhysics::RigidBody* rigidBody = GetParent().m_physicsRigidBodyComponent->GetRigidBody(); + rigidBody->ApplyLinearImpulseAtWorldPoint(impulse, worldPoint); + } +} // namespace Multiplayer diff --git a/Gems/Multiplayer/Code/Source/Components/NetworkTransformComponent.cpp b/Gems/Multiplayer/Code/Source/Components/NetworkTransformComponent.cpp index 8461f950fd..9ef585a0a0 100644 --- a/Gems/Multiplayer/Code/Source/Components/NetworkTransformComponent.cpp +++ b/Gems/Multiplayer/Code/Source/Components/NetworkTransformComponent.cpp @@ -26,12 +26,7 @@ namespace Multiplayer } NetworkTransformComponent::NetworkTransformComponent() - : m_rotationEventHandler([this](const AZ::Quaternion& rotation) { OnRotationChangedEvent(rotation); }) - , m_translationEventHandler([this](const AZ::Vector3& translation) { OnTranslationChangedEvent(translation); }) - , m_scaleEventHandler([this](float scale) { OnScaleChangedEvent(scale); }) - , m_resetCountEventHandler([this](const uint8_t&) { OnResetCountChangedEvent(); }) - , m_parentIdChangedEventHandler([this](NetEntityId newParent) { OnParentIdChangedEvent(newParent); }) - , m_entityPreRenderEventHandler([this](float deltaTime, float blendFactor) { OnPreRender(deltaTime, blendFactor); }) + : m_entityPreRenderEventHandler([this](float deltaTime) { OnPreRender(deltaTime); }) , m_entityCorrectionEventHandler([this]() { OnCorrection(); }) { ; @@ -44,19 +39,8 @@ namespace Multiplayer void NetworkTransformComponent::OnActivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating) { - RotationAddEvent(m_rotationEventHandler); - TranslationAddEvent(m_translationEventHandler); - ScaleAddEvent(m_scaleEventHandler); - ResetCountAddEvent(m_resetCountEventHandler); - ParentEntityIdAddEvent(m_parentIdChangedEventHandler); - if (GetNetBindComponent()) - { - GetNetBindComponent()->AddEntityPreRenderEventHandler(m_entityPreRenderEventHandler); - GetNetBindComponent()->AddEntityCorrectionEventHandler(m_entityCorrectionEventHandler); - } - - // When coming into relevance, reset all blending factors so we don't interpolate to our start position - OnResetCountChangedEvent(); + GetNetBindComponent()->AddEntityPreRenderEventHandler(m_entityPreRenderEventHandler); + GetNetBindComponent()->AddEntityCorrectionEventHandler(m_entityCorrectionEventHandler); } void NetworkTransformComponent::OnDeactivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating) @@ -64,90 +48,31 @@ namespace Multiplayer ; } - void NetworkTransformComponent::OnRotationChangedEvent(const AZ::Quaternion& rotation) - { - m_previousTransform.SetRotation(m_targetTransform.GetRotation()); - m_targetTransform.SetRotation(rotation); - UpdateTargetHostFrameId(); - } - - void NetworkTransformComponent::OnTranslationChangedEvent(const AZ::Vector3& translation) - { - m_previousTransform.SetTranslation(m_targetTransform.GetTranslation()); - m_targetTransform.SetTranslation(translation); - UpdateTargetHostFrameId(); - } - - void NetworkTransformComponent::OnScaleChangedEvent(float scale) - { - m_previousTransform.SetUniformScale(m_targetTransform.GetUniformScale()); - m_targetTransform.SetUniformScale(scale); - UpdateTargetHostFrameId(); - } - - void NetworkTransformComponent::OnResetCountChangedEvent() - { - OnParentIdChangedEvent(GetParentEntityId()); - - m_targetTransform.SetRotation(GetRotation()); - m_targetTransform.SetTranslation(GetTranslation()); - m_targetTransform.SetUniformScale(GetScale()); - m_previousTransform = m_targetTransform; - } - - void NetworkTransformComponent::OnParentIdChangedEvent([[maybe_unused]] NetEntityId newParent) - { - if (newParent == InvalidNetEntityId) - { - if (AzFramework::TransformComponent* transformComponent = GetEntity()->FindComponent()) - { - if (transformComponent->GetParentId() != AZ::EntityId()) - { - transformComponent->SetParent(AZ::EntityId()); - } - } - } - else - { - const ConstNetworkEntityHandle rootHandle = GetNetworkEntityManager()->GetEntity(newParent); - if (rootHandle.Exists()) - { - const AZ::EntityId parentEntityId = rootHandle.GetEntity()->GetId(); - if (AzFramework::TransformComponent* transformComponent = GetEntity()->FindComponent()) - { - if (transformComponent->GetParentId() != parentEntityId) - { - transformComponent->SetParent(parentEntityId); - } - } - } - } - } - - void NetworkTransformComponent::UpdateTargetHostFrameId() - { - const HostFrameId currentHostFrameId = Multiplayer::GetNetworkTime()->GetHostFrameId(); - if (currentHostFrameId > m_targetHostFrameId) - { - m_targetHostFrameId = currentHostFrameId; - } - } - - void NetworkTransformComponent::OnPreRender([[maybe_unused]] float deltaTime, float blendFactor) + void NetworkTransformComponent::OnPreRender([[maybe_unused]] float deltaTime) { if (!HasController()) { AZ::Transform blendTransform; - if (Multiplayer::GetNetworkTime() && Multiplayer::GetNetworkTime()->GetHostFrameId() > m_targetHostFrameId) + blendTransform.SetRotation(GetRotation()); + blendTransform.SetTranslation(GetTranslation()); + blendTransform.SetUniformScale(GetScale()); + + const float blendFactor = GetMultiplayer()->GetCurrentBlendFactor(); + if (!AZ::IsClose(blendFactor, 1.0f)) { - m_previousTransform = m_targetTransform; - blendTransform = m_targetTransform; - } - else - { - blendTransform.SetRotation(m_previousTransform.GetRotation().Slerp(m_targetTransform.GetRotation(), blendFactor)); - blendTransform.SetTranslation(m_previousTransform.GetTranslation().Lerp(m_targetTransform.GetTranslation(), blendFactor)); - blendTransform.SetUniformScale(AZ::Lerp(m_previousTransform.GetUniformScale(), m_targetTransform.GetUniformScale(), blendFactor)); + AZ::Transform blendTransformPrevious; + blendTransformPrevious.SetRotation(GetRotationPrevious()); + blendTransformPrevious.SetTranslation(GetTranslationPrevious()); + blendTransformPrevious.SetUniformScale(GetScalePrevious()); + + if (!blendTransform.IsClose(blendTransformPrevious)) + { + blendTransform.SetRotation(blendTransformPrevious.GetRotation().Slerp(blendTransform.GetRotation(), blendFactor)); + blendTransform.SetTranslation( + blendTransformPrevious.GetTranslation().Lerp(blendTransform.GetTranslation(), blendFactor)); + blendTransform.SetUniformScale( + AZ::Lerp(blendTransformPrevious.GetUniformScale(), blendTransform.GetUniformScale(), blendFactor)); + } } if (!GetTransformComponent()->GetWorldTM().IsClose(blendTransform)) @@ -160,12 +85,15 @@ namespace Multiplayer void NetworkTransformComponent::OnCorrection() { // Snap to latest - OnResetCountChangedEvent(); + AZ::Transform targetTransform; + targetTransform.SetRotation(GetRotation()); + targetTransform.SetTranslation(GetTranslation()); + targetTransform.SetUniformScale(GetScale()); // Hard set the entities transform - if (!GetTransformComponent()->GetWorldTM().IsClose(m_targetTransform)) + if (!GetTransformComponent()->GetWorldTM().IsClose(targetTransform)) { - GetTransformComponent()->SetWorldTM(m_targetTransform); + GetTransformComponent()->SetWorldTM(targetTransform); } } diff --git a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp index 24f865194f..e66eb8d3a3 100644 --- a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp +++ b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp @@ -922,7 +922,7 @@ namespace Multiplayer for (NetBindComponent* netBindComponent : gatheredEntities) { - netBindComponent->NotifyPreRender(deltaTime, m_renderBlendFactor); + netBindComponent->NotifyPreRender(deltaTime); } } else @@ -934,7 +934,7 @@ namespace Multiplayer NetBindComponent* netBindComponent = entity->FindComponent(); if (netBindComponent != nullptr) { - netBindComponent->NotifyPreRender(deltaTime, m_renderBlendFactor); + netBindComponent->NotifyPreRender(deltaTime); } } } diff --git a/Gems/Multiplayer/Code/Source/NetworkTime/NetworkTime.cpp b/Gems/Multiplayer/Code/Source/NetworkTime/NetworkTime.cpp index cd9c1737d6..b37f485ff4 100644 --- a/Gems/Multiplayer/Code/Source/NetworkTime/NetworkTime.cpp +++ b/Gems/Multiplayer/Code/Source/NetworkTime/NetworkTime.cpp @@ -65,11 +65,6 @@ namespace Multiplayer return m_rewindingConnectionId; } - HostFrameId NetworkTime::GetHostFrameIdForRewindingConnection(AzNetworking::ConnectionId rewindConnectionId) const - { - return (IsTimeRewound() && (rewindConnectionId == m_rewindingConnectionId)) ? m_unalteredFrameId : m_hostFrameId; - } - void NetworkTime::ForceSetTime(HostFrameId frameId, AZ::TimeMs timeMs) { AZ_Assert(!IsTimeRewound(), "Forcibly setting network time is unsupported under a rewound time scope"); @@ -79,16 +74,12 @@ namespace Multiplayer m_rewindingConnectionId = AzNetworking::InvalidConnectionId; } - void NetworkTime::AlterTime(HostFrameId frameId, AZ::TimeMs timeMs, AzNetworking::ConnectionId rewindConnectionId) + void NetworkTime::AlterTime(HostFrameId frameId, AZ::TimeMs timeMs, float blendFactor, AzNetworking::ConnectionId rewindConnectionId) { m_hostFrameId = frameId; m_hostTimeMs = timeMs; - m_rewindingConnectionId = rewindConnectionId; - } - - void NetworkTime::AlterBlendFactor(float blendFactor) - { m_hostBlendFactor = blendFactor; + m_rewindingConnectionId = rewindConnectionId; } void NetworkTime::SyncEntitiesToRewindState(const AZ::Aabb& rewindVolume) @@ -121,8 +112,15 @@ namespace Multiplayer if (networkTransform != nullptr) { - // We're not presently factoring in interpolated position here - const AZ::Vector3 rewindCenter = networkTransform->GetTranslation(); // Get the rewound position + // Get the rewound position for target host frame ID plus the one preceding it for potential lerp + AZ::Vector3 rewindCenter = networkTransform->GetTranslation(); + const AZ::Vector3 rewindCenterPrevious = networkTransform->GetTranslationPrevious(); + const float blendFactor = GetNetworkTime()->GetHostBlendFactor(); + if (!AZ::IsClose(blendFactor, 1.0f) && !rewindCenter.IsClose(rewindCenterPrevious)) + { + // If we have a blend factor, lerp the translation for accuracy + rewindCenter = rewindCenterPrevious.Lerp(rewindCenter, blendFactor); + } const AZ::Vector3 rewindOffset = rewindCenter - currentCenter; // Compute offset between rewound and current positions const AZ::Aabb rewoundAabb = currentBounds.GetTranslated(rewindOffset); // Apply offset to the entity aabb diff --git a/Gems/Multiplayer/Code/Source/NetworkTime/NetworkTime.h b/Gems/Multiplayer/Code/Source/NetworkTime/NetworkTime.h index 0278ddd2b0..2bcf019623 100644 --- a/Gems/Multiplayer/Code/Source/NetworkTime/NetworkTime.h +++ b/Gems/Multiplayer/Code/Source/NetworkTime/NetworkTime.h @@ -32,10 +32,8 @@ namespace Multiplayer AZ::TimeMs GetHostTimeMs() const override; float GetHostBlendFactor() const override; AzNetworking::ConnectionId GetRewindingConnectionId() const override; - HostFrameId GetHostFrameIdForRewindingConnection(AzNetworking::ConnectionId rewindConnectionId) const override; void ForceSetTime(HostFrameId frameId, AZ::TimeMs timeMs) override; - void AlterTime(HostFrameId frameId, AZ::TimeMs timeMs, AzNetworking::ConnectionId rewindConnectionId) override; - void AlterBlendFactor(float blendFactor) override; + void AlterTime(HostFrameId frameId, AZ::TimeMs timeMs, float blendFactor, AzNetworking::ConnectionId rewindConnectionId) override; void SyncEntitiesToRewindState(const AZ::Aabb& rewindVolume) override; void ClearRewoundEntities() override; //! @} diff --git a/Gems/Multiplayer/Code/Tests/RewindableObjectTests.cpp b/Gems/Multiplayer/Code/Tests/RewindableObjectTests.cpp index b8d60a94e8..04de971f0d 100644 --- a/Gems/Multiplayer/Code/Tests/RewindableObjectTests.cpp +++ b/Gems/Multiplayer/Code/Tests/RewindableObjectTests.cpp @@ -57,6 +57,35 @@ namespace UnitTest } } + TEST_F(RewindableObjectTests, CurrentPreviousTests) + { + Multiplayer::RewindableObject test(0); + + for (uint32_t i = 0; i < RewindableBufferFrames; ++i) + { + test = i; + EXPECT_EQ(i, test); + Multiplayer::GetNetworkTime()->IncrementHostFrameId(); + } + + { + // Test that Get/GetPrevious return different value when not on the owning connection + Multiplayer::ScopedAlterTime time(static_cast(RewindableBufferFrames - 1), AZ::TimeMs{ 0 }, 1.f, AzNetworking::InvalidConnectionId); + EXPECT_EQ(RewindableBufferFrames - 1, test.Get()); + EXPECT_EQ(RewindableBufferFrames - 2, test.GetPrevious()); + } + + // Test that Get/GetPrevious return the unaltered frame on the owning conection + Multiplayer::GetNetworkTime()->AlterTime(static_cast(RewindableBufferFrames - 1), AZ::TimeMs{ 0 }, 1.f, AzNetworking::ConnectionId(0)); + { + Multiplayer::ScopedAlterTime time(static_cast(RewindableBufferFrames - 1), AZ::TimeMs{ 0 }, 1.f, AzNetworking::ConnectionId(0)); + test.SetOwningConnectionId(AzNetworking::ConnectionId(0)); + EXPECT_EQ(RewindableBufferFrames - 1, test.Get()); + EXPECT_EQ(RewindableBufferFrames - 1, test.GetPrevious()); + } + Multiplayer::GetNetworkTime()->AlterTime(static_cast(RewindableBufferFrames), AZ::TimeMs(0), 1.f, AzNetworking::InvalidConnectionId); + } + TEST_F(RewindableObjectTests, OverflowTests) { Multiplayer::RewindableObject test(0); diff --git a/Gems/Multiplayer/Code/multiplayer_files.cmake b/Gems/Multiplayer/Code/multiplayer_files.cmake index 6475155f40..61bb66e6bd 100644 --- a/Gems/Multiplayer/Code/multiplayer_files.cmake +++ b/Gems/Multiplayer/Code/multiplayer_files.cmake @@ -21,6 +21,9 @@ set(FILES Include/Multiplayer/Components/NetworkHierarchyChildComponent.h Include/Multiplayer/Components/NetworkHierarchyRootComponent.h Include/Multiplayer/Components/NetworkHierarchyBus.h + Include/Multiplayer/Components/NetworkCharacterComponent.h + Include/Multiplayer/Components/NetworkHitVolumesComponent.h + Include/Multiplayer/Components/NetworkRigidBodyComponent.h Include/Multiplayer/Components/NetworkTransformComponent.h Include/Multiplayer/ConnectionData/IConnectionData.h Include/Multiplayer/EntityDomains/IEntityDomain.h @@ -56,6 +59,9 @@ set(FILES Source/AutoGen/LocalPredictionPlayerInputComponent.AutoComponent.xml Source/AutoGen/Multiplayer.AutoPackets.xml Source/AutoGen/MultiplayerEditor.AutoPackets.xml + Source/AutoGen/NetworkCharacterComponent.AutoComponent.xml + Source/AutoGen/NetworkHitVolumesComponent.AutoComponent.xml + Source/AutoGen/NetworkRigidBodyComponent.AutoComponent.xml Source/AutoGen/NetworkTransformComponent.AutoComponent.xml Source/AutoGen/NetworkHierarchyChildComponent.AutoComponent.xml Source/AutoGen/NetworkHierarchyRootComponent.AutoComponent.xml @@ -66,6 +72,9 @@ set(FILES Source/Components/NetBindComponent.cpp Source/Components/NetworkHierarchyChildComponent.cpp Source/Components/NetworkHierarchyRootComponent.cpp + Source/Components/NetworkCharacterComponent.cpp + Source/Components/NetworkHitVolumesComponent.cpp + Source/Components/NetworkRigidBodyComponent.cpp Source/Components/NetworkTransformComponent.cpp Source/ConnectionData/ClientToServerConnectionData.cpp Source/ConnectionData/ClientToServerConnectionData.h diff --git a/cmake/3rdParty.cmake b/cmake/3rdParty.cmake index f70ed1aa02..4b7f929279 100644 --- a/cmake/3rdParty.cmake +++ b/cmake/3rdParty.cmake @@ -6,6 +6,17 @@ # # +define_property(TARGET PROPERTY LY_SYSTEM_LIBRARY + BRIEF_DOCS "Defines a 3rdParty library as a system library" + FULL_DOCS [[ + Property which is set on third party targets that should be considered + as provided by the system. Such targets are excluded from the runtime + dependencies considerations, and are not distributed as part of the + O3DE SDK package. Instead, users of the SDK are expected to install + such a third party library themselves. + ]] +) + # Do not overcomplicate searching for the 3rdParty path, if it is not easy to find, # the user should define it. @@ -79,9 +90,10 @@ endfunction() # "fileA\nMy/Output/Subfolder/lib" # "fileB\nMy/Output/Subfolder/bin" # +# \arg:SYSTEM If specified, the library is considered a system library, and is not copied to the build output directory function(ly_add_external_target) - set(options) + set(options SYSTEM) set(oneValueArgs NAME VERSION 3RDPARTY_DIRECTORY PACKAGE 3RDPARTY_ROOT_DIRECTORY OUTPUT_SUBDIRECTORY) set(multiValueArgs HEADER_CHECK COMPILE_DEFINITIONS INCLUDE_DIRECTORIES BUILD_DEPENDENCIES RUNTIME_DEPENDENCIES) @@ -300,6 +312,10 @@ function(ly_add_external_target) ) endif() + if(ly_add_external_target_SYSTEM) + set_target_properties(3rdParty::${NAME_WITH_NAMESPACE} PROPERTIES LY_SYSTEM_LIBRARY TRUE) + endif() + endif() endfunction() @@ -327,4 +343,4 @@ if(NOT INSTALLED_ENGINE) ly_include_cmake_file_list(cmake/3rdParty/cmake_files.cmake) ly_get_absolute_pal_filename(pal_3rdparty_dir ${CMAKE_CURRENT_SOURCE_DIR}/cmake/3rdParty/Platform/${PAL_PLATFORM_NAME}) ly_include_cmake_file_list(${pal_3rdparty_dir}/cmake_${PAL_PLATFORM_NAME_LOWERCASE}_files.cmake) -endif() \ No newline at end of file +endif() diff --git a/cmake/Install.cmake b/cmake/Install.cmake index 6f50c78fd7..adcd28fa37 100644 --- a/cmake/Install.cmake +++ b/cmake/Install.cmake @@ -152,3 +152,20 @@ function(ly_install_run_code CODE) ) endfunction() + +#! ly_install_run_script: specifies path to script to be added to the install process (will run at install time) +# +# \notes: +# - refer to cmake's install(SCRIPT documentation for more information +# +function(ly_install_run_script SCRIPT) + + if(NOT LY_INSTALL_ENABLED) + return() + endif() + + install(SCRIPT ${SCRIPT} + COMPONENT ${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME} # use the default for the time being + ) + +endfunction() \ No newline at end of file diff --git a/cmake/LYPython.cmake b/cmake/LYPython.cmake index eeb9f97a55..a8095fbc95 100644 --- a/cmake/LYPython.cmake +++ b/cmake/LYPython.cmake @@ -21,14 +21,17 @@ include(cmake/LySet.cmake) # CMAKE_HOST_SYSTEM_NAME is "Windows", "Darwin", or "Linux" in our cases.. if (${CMAKE_HOST_SYSTEM_NAME} STREQUAL "Linux" ) ly_set(LY_PYTHON_VERSION 3.7.10) + ly_set(LY_PYTHON_VERSION_MAJOR_MINOR 3.7) ly_set(LY_PYTHON_PACKAGE_NAME python-3.7.10-rev2-linux) ly_set(LY_PYTHON_PACKAGE_HASH 6b9cf455e6190ec38836194f4454bb9db6bfc6890b4baff185cc5520aa822f05) elseif (${CMAKE_HOST_SYSTEM_NAME} STREQUAL "Darwin" ) ly_set(LY_PYTHON_VERSION 3.7.10) + ly_set(LY_PYTHON_VERSION_MAJOR_MINOR 3.7) ly_set(LY_PYTHON_PACKAGE_NAME python-3.7.10-rev1-darwin) ly_set(LY_PYTHON_PACKAGE_HASH 3f65801894e4e44b5faa84dd85ef80ecd772dcf728cdd2d668a6e75978a32695) elseif (${CMAKE_HOST_SYSTEM_NAME} STREQUAL "Windows" ) ly_set(LY_PYTHON_VERSION 3.7.10) + ly_set(LY_PYTHON_VERSION_MAJOR_MINOR 3.7) ly_set(LY_PYTHON_PACKAGE_NAME python-3.7.10-rev2-windows) ly_set(LY_PYTHON_PACKAGE_HASH 06d97488a2dbabe832ecfa832a42d3e8a7163ba95e975f032727331b0f49d280) endif() diff --git a/cmake/Platform/Common/Install_common.cmake b/cmake/Platform/Common/Install_common.cmake index fdd3256d78..8fb2effe29 100644 --- a/cmake/Platform/Common/Install_common.cmake +++ b/cmake/Platform/Common/Install_common.cmake @@ -21,15 +21,21 @@ define_property(TARGET PROPERTY LY_INSTALL_GENERATE_RUN_TARGET ly_set(CMAKE_INSTALL_DEFAULT_COMPONENT_NAME Core) -cmake_path(RELATIVE_PATH CMAKE_RUNTIME_OUTPUT_DIRECTORY BASE_DIRECTORY ${CMAKE_BINARY_DIR} OUTPUT_VARIABLE runtime_output_directory) -cmake_path(RELATIVE_PATH CMAKE_LIBRARY_OUTPUT_DIRECTORY BASE_DIRECTORY ${CMAKE_BINARY_DIR} OUTPUT_VARIABLE library_output_directory) - if(LY_MONOLITHIC_GAME) set(LY_BUILD_PERMUTATION Monolithic) else() set(LY_BUILD_PERMUTATION Default) endif() +cmake_path(RELATIVE_PATH CMAKE_RUNTIME_OUTPUT_DIRECTORY BASE_DIRECTORY ${CMAKE_BINARY_DIR} OUTPUT_VARIABLE runtime_output_directory) +cmake_path(RELATIVE_PATH CMAKE_LIBRARY_OUTPUT_DIRECTORY BASE_DIRECTORY ${CMAKE_BINARY_DIR} OUTPUT_VARIABLE library_output_directory) +# Get the output folders, archive is always the same, but runtime/library can be in subfolders defined per target +cmake_path(RELATIVE_PATH CMAKE_ARCHIVE_OUTPUT_DIRECTORY BASE_DIRECTORY ${CMAKE_BINARY_DIR} OUTPUT_VARIABLE archive_output_directory) + +cmake_path(APPEND archive_output_directory "${PAL_PLATFORM_NAME}/$/${LY_BUILD_PERMUTATION}") +cmake_path(APPEND library_output_directory "${PAL_PLATFORM_NAME}/$/${LY_BUILD_PERMUTATION}") +cmake_path(APPEND runtime_output_directory "${PAL_PLATFORM_NAME}/$/${LY_BUILD_PERMUTATION}") + #! ly_setup_target: Setup the data needed to re-create the cmake target commands for a single target function(ly_setup_target OUTPUT_CONFIGURED_TARGET ALIAS_TARGET_NAME absolute_target_source_dir) # De-alias target name @@ -78,9 +84,6 @@ function(ly_setup_target OUTPUT_CONFIGURED_TARGET ALIAS_TARGET_NAME absolute_tar endforeach() endif() - # Get the output folders, archive is always the same, but runtime/library can be in subfolders defined per target - cmake_path(RELATIVE_PATH CMAKE_ARCHIVE_OUTPUT_DIRECTORY BASE_DIRECTORY ${CMAKE_BINARY_DIR} OUTPUT_VARIABLE archive_output_directory) - get_target_property(target_runtime_output_directory ${TARGET_NAME} RUNTIME_OUTPUT_DIRECTORY) if(target_runtime_output_directory) cmake_path(RELATIVE_PATH target_runtime_output_directory BASE_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY} OUTPUT_VARIABLE target_runtime_output_subdirectory) @@ -91,10 +94,6 @@ function(ly_setup_target OUTPUT_CONFIGURED_TARGET ALIAS_TARGET_NAME absolute_tar cmake_path(RELATIVE_PATH target_library_output_directory BASE_DIRECTORY ${CMAKE_LIBRARY_OUTPUT_DIRECTORY} OUTPUT_VARIABLE target_library_output_subdirectory) endif() - cmake_path(APPEND archive_output_directory "${PAL_PLATFORM_NAME}/$/${LY_BUILD_PERMUTATION}") - cmake_path(APPEND library_output_directory "${PAL_PLATFORM_NAME}/$/${LY_BUILD_PERMUTATION}") - cmake_path(APPEND runtime_output_directory "${PAL_PLATFORM_NAME}/$/${LY_BUILD_PERMUTATION}") - if(COMMAND ly_install_target_override) # Mac needs special handling because of a cmake issue ly_install_target_override(TARGET ${TARGET_NAME} @@ -372,6 +371,10 @@ function(ly_setup_o3de_install) COMPONENT ${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME} ) + if(COMMAND ly_post_install_steps) + ly_post_install_steps() + endif() + endfunction() #! ly_setup_cmake_install: install the "cmake" folder @@ -528,7 +531,7 @@ endfunction()" # of baking the path. This is needed so `cmake --install --prefix ` works regardless of the CMAKE_INSTALL_PREFIX # used to generate the solution. # CMAKE_INSTALL_PREFIX is still used when building the INSTALL target - set(install_output_folder "\${CMAKE_INSTALL_PREFIX}/${runtime_output_directory}/${PAL_PLATFORM_NAME}/$/${LY_BUILD_PERMUTATION}") + set(install_output_folder "\${CMAKE_INSTALL_PREFIX}/${runtime_output_directory}") set(target_file_dir "${install_output_folder}/${target_runtime_output_subdirectory}") ly_get_runtime_dependencies(runtime_dependencies ${target}) foreach(runtime_dependency ${runtime_dependencies}) diff --git a/cmake/Platform/Common/RuntimeDependencies_common.cmake b/cmake/Platform/Common/RuntimeDependencies_common.cmake index 95d91da7f7..60e55453f8 100644 --- a/cmake/Platform/Common/RuntimeDependencies_common.cmake +++ b/cmake/Platform/Common/RuntimeDependencies_common.cmake @@ -70,12 +70,23 @@ function(ly_get_runtime_dependencies ly_RUNTIME_DEPENDENCIES ly_TARGET) # link dependencies are not runtime dependencies (we dont have anything to copy) however, we need to traverse # them since them or some dependency downstream could have something to copy over - foreach(link_dependency ${link_dependencies}) - if(NOT ${link_dependency} MATCHES "^::@") # Skip wraping produced when targets are not created in the same directory (https://cmake.org/cmake/help/latest/prop_tgt/LINK_LIBRARIES.html) - unset(dependencies) - ly_get_runtime_dependencies(dependencies ${link_dependency}) - list(APPEND all_runtime_dependencies ${dependencies}) + foreach(link_dependency IN LISTS link_dependencies) + if(${link_dependency} MATCHES "^::@") + # Skip wraping produced when targets are not created in the same directory + # (https://cmake.org/cmake/help/latest/prop_tgt/LINK_LIBRARIES.html) + continue() endif() + + if(TARGET ${link_dependency} AND link_dependency MATCHES "^3rdParty::") + get_target_property(is_system_library ${link_dependency} LY_SYSTEM_LIBRARY) + if(is_system_library) + continue() + endif() + endif() + + unset(dependencies) + ly_get_runtime_dependencies(dependencies ${link_dependency}) + list(APPEND all_runtime_dependencies ${dependencies}) endforeach() # For manual dependencies, we want to copy over the dependency and traverse them diff --git a/cmake/Platform/Mac/Configurations_mac.cmake b/cmake/Platform/Mac/Configurations_mac.cmake index 33a58dbbda..b1c50b751d 100644 --- a/cmake/Platform/Mac/Configurations_mac.cmake +++ b/cmake/Platform/Mac/Configurations_mac.cmake @@ -29,9 +29,7 @@ else() endif() # Signing -# The "-o linker-signed" flag is required as a work-around for the following CMake issue: -# https://gitlab.kitware.com/cmake/cmake/-/issues/21854 -ly_set(CMAKE_XCODE_ATTRIBUTE_OTHER_CODE_SIGN_FLAGS "--deep -o linker-signed") +ly_set(CMAKE_XCODE_ATTRIBUTE_OTHER_CODE_SIGN_FLAGS "--deep") # Generate scheme files for Xcode ly_set(CMAKE_XCODE_GENERATE_SCHEME TRUE) diff --git a/cmake/Platform/Mac/InstallUtils_mac.cmake.in b/cmake/Platform/Mac/InstallUtils_mac.cmake.in new file mode 100644 index 0000000000..de6d9ddf65 --- /dev/null +++ b/cmake/Platform/Mac/InstallUtils_mac.cmake.in @@ -0,0 +1,168 @@ +# +# 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 +# +# + +function(fixup_qt_framework lib_name framework_path) + + file(REMOVE_RECURSE + ${framework_path}/Headers + ${framework_path}/Resources + ${framework_path}/${lib_name} + ${framework_path}/Versions/Current + ${framework_path}/Versions/5/Headers + ) + + execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink 5 Current + WORKING_DIRECTORY ${framework_path}/Versions + ) + execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink Versions/Current/${lib_name} ${lib_name} + WORKING_DIRECTORY ${framework_path} + ) + execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink Versions/Current/Resources Resources + WORKING_DIRECTORY ${framework_path} + ) + +endfunction() + +function(fixup_python_framework framework_path) + + file(REMOVE_RECURSE + ${framework_path}/Versions/Current + ${framework_path}/Versions/@LY_PYTHON_VERSION_MAJOR_MINOR@/Headers + ${framework_path}/Versions/@LY_PYTHON_VERSION_MAJOR_MINOR@/lib/Python + ${framework_path}/Versions/@LY_PYTHON_VERSION_MAJOR_MINOR@/lib/python@LY_PYTHON_VERSION_MAJOR_MINOR@/test + ${framework_path}/Versions/@LY_PYTHON_VERSION_MAJOR_MINOR@/lib/python@LY_PYTHON_VERSION_MAJOR_MINOR@/site-packages/scipy/io/tests + ${framework_path}/Python + ${framework_path}/Resources + ${framework_path}/Headers + ) + + file(GLOB_RECURSE exe_file_list "${framework_path}/**/*.exe") + if(exe_file_list) + file(REMOVE_RECURSE ${exe_file_list}) + endif() + execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink include/python@LY_PYTHON_VERSION_MAJOR_MINOR@m Headers + WORKING_DIRECTORY ${framework_path}/Versions/@LY_PYTHON_VERSION_MAJOR_MINOR@ + ) + execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink @LY_PYTHON_VERSION_MAJOR_MINOR@ Current + WORKING_DIRECTORY ${framework_path}/Versions/ + ) + execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink Versions/Current/Python Python + WORKING_DIRECTORY ${framework_path} + ) + execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink Versions/Current/Headers Headers + WORKING_DIRECTORY ${framework_path} + ) + execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink Versions/Current/Resources Resources + WORKING_DIRECTORY ${framework_path} + ) + file(CHMOD ${framework_path}/Versions/Current/Python + PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_WRITE GROUP_EXECUTE WORLD_READ WORLD_EXECUTE + ) + +endfunction() + +function(codesign_file file entitlement_file) + + if (NOT @LY_ENABLE_HARDENED_RUNTIME@) + return() + endif() + + if(EXISTS ${entitlement_file}) + + execute_process(COMMAND "/usr/bin/codesign" "--force" "--sign" "@LY_CODE_SIGN_IDENTITY@" "--deep" "-o" "runtime" "--timestamp" "--entitlements" "${entitlement_file}" "${file}" + TIMEOUT 300 + OUTPUT_VARIABLE codesign_out + RESULT_VARIABLE codesign_ret + ) + else() + execute_process(COMMAND "/usr/bin/codesign" "--force" "--sign" "@LY_CODE_SIGN_IDENTITY@" "--deep" "-o" "runtime" "--timestamp" "${file}" + TIMEOUT 300 + OUTPUT_VARIABLE codesign_out + RESULT_VARIABLE codesign_ret + ) + endif() + + if(NOT ${codesign_ret} EQUAL "0") + message(FATAL_ERROR "Codesign operation for ${file_path} returned ${codesign_ret} with message ${codesign_out}") + endif() + +endfunction() + +function(codesign_python_framework_binaries framework_path) + + if (NOT @LY_ENABLE_HARDENED_RUNTIME@) + return() + endif() + + # The codesign "--deep" flag will only codesign binaries in folders with specific names. + # We need to codesign all the binaries that the "--deep" flag will miss. + file(GLOB_RECURSE files + LIST_DIRECTORIES false + "${framework_path}/Versions/@LY_PYTHON_VERSION_MAJOR_MINOR@/bin/**" + "${framework_path}/Versions/@LY_PYTHON_VERSION_MAJOR_MINOR@/lib/**" + "${framework_path}/Versions/@LY_PYTHON_VERSION_MAJOR_MINOR@/Resources/**") + + foreach(file ${files}) + if(NOT EXISTS ${file}) + file(REMOVE ${file}) + continue() + endif() + cmake_path(SET path_var "${file}") + cmake_path(GET path_var EXTENSION LAST_ONLY extension) + set(should_codesign FALSE) + set(extension_skip_list ".dylib" ".so" ".7m") + if (NOT extension) + set(should_codesign TRUE) + elseif(extension IN_LIST extension_skip_list) + set(should_codesign TRUE) + endif() + if(${should_codesign}) + codesign_file("${file}" "@LY_ROOT_FOLDER@/python/Platform/Mac/PythonEntitlements.plist") + endif() + endforeach() + +endfunction() + +function(ly_copy source_file target_directory) + + if("${source_file}" MATCHES "\\.[Ff]ramework[^\\.]") + + # fixup origin to copy the whole Framework folder + string(REGEX REPLACE "(.*\\.[Ff]ramework).*" "\\1" source_file "${source_file}") + + endif() + get_filename_component(target_filename "${source_file}" NAME) + file(COPY "${source_file}" DESTINATION "${target_directory}" FILE_PERMISSIONS @LY_COPY_PERMISSIONS@ FOLLOW_SYMLINK_CHAIN) + + # Our Qt and Python frameworks aren't in the correct bundle format to be codesigned. + if("${target_filename}" MATCHES "(Qt[^.]+)\\.[Ff]ramework") + fixup_qt_framework(${CMAKE_MATCH_1} "${target_directory}/${target_filename}") + # For some Qt frameworks(QtCore), signing the bundle doesn't work because of bundle + # format issues(despite the fixes above). But once we've patched the framework above, there's + # only one executable that we need to sign so we can do it directly. + set(target_filename "${target_filename}/Versions/5/${CMAKE_MATCH_1}") + elseif("${target_filename}" MATCHES "Python.framework") + fixup_python_framework("${target_directory}/${target_filename}") + codesign_python_framework_binaries("${target_directory}/${target_filename}") + endif() + codesign_file("${target_directory}/${target_filename}" "none") + +endfunction() + +function(ly_download_and_codesign_sdk_python) + execute_process(COMMAND ${CMAKE_COMMAND} -DPAL_PLATFORM_NAME=Mac -DLY_3RDPARTY_PATH=${CMAKE_INSTALL_PREFIX}/python -P ${CMAKE_INSTALL_PREFIX}/python/get_python.cmake) + fixup_python_framework(${CMAKE_INSTALL_PREFIX}/python/runtime/@LY_PYTHON_PACKAGE_NAME@/Python.framework) + codesign_python_framework_binaries(${CMAKE_INSTALL_PREFIX}/python/runtime/@LY_PYTHON_PACKAGE_NAME@/Python.framework) + codesign_file(${CMAKE_INSTALL_PREFIX}/python/runtime/@LY_PYTHON_PACKAGE_NAME@/Python.framework @LY_ROOT_FOLDER@/python/Platform/Mac/PythonEntitlements.plist) +endfunction() + +function(ly_codesign_sdk) + codesign_file(${LY_INSTALL_PATH_ORIGINAL}/O3DE_SDK.app "none") +endfunction() + + diff --git a/cmake/Platform/Mac/Install_mac.cmake b/cmake/Platform/Mac/Install_mac.cmake index 8f07b1bfde..bdc2300131 100644 --- a/cmake/Platform/Mac/Install_mac.cmake +++ b/cmake/Platform/Mac/Install_mac.cmake @@ -6,6 +6,8 @@ # # +include(cmake/Platform/Common/Install_common.cmake) + # This is used to generate a setreg file which will be placed inside the bundle # for targets that request it(eg. AssetProcessor/Editor). This is the relative path # to the bundle from the installed engine's root. This will be used to compute the @@ -16,7 +18,7 @@ set(installed_binaries_path_template [[ "AzCore": { "Runtime": { "FilePaths": { - "InstalledBinariesFolder": "bin/Mac/$" + "InstalledBinariesFolder": "@runtime_output_directory@" } } } @@ -24,15 +26,20 @@ set(installed_binaries_path_template [[ }]] ) -unset(target_conf_dir) -foreach(conf IN LISTS CMAKE_CONFIGURATION_TYPES) - string(TOUPPER ${conf} UCONF) - string(APPEND target_conf_dir $<$:${CMAKE_RUNTIME_OUTPUT_DIRECTORY_${UCONF}}>) -endforeach() +# This setreg file will be used by all of our installed app bundles to locate installed +# runtime dependencies. It contains the path to binary install directory relative to +# the installed engine root. +string(CONFIGURE "${installed_binaries_path_template}" configured_setreg_file) +file(GENERATE + OUTPUT ${CMAKE_BINARY_DIR}/runtime_install/$/BinariesInstallPath.setreg + CONTENT "${configured_setreg_file}" +) -set(installed_binaries_setreg_path ${target_conf_dir}/Registry/installed_binaries_path.setreg) - -file(GENERATE OUTPUT ${installed_binaries_setreg_path} CONTENT ${installed_binaries_path_template}) +# ly_install_run_script isn't defined yet so we use install(SCRIPT) directly. +# This needs to be done here because it needs to update the install prefix +# before cmake does anything else in the install process. +configure_file(${LY_ROOT_FOLDER}/cmake/Platform/Mac/PreInstallSteps_mac.cmake.in ${CMAKE_BINARY_DIR}/runtime_install/PreInstallSteps_mac.cmake @ONLY) +install(SCRIPT ${CMAKE_BINARY_DIR}/runtime_install/PreInstallSteps_mac.cmake COMPONENT ${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME}) #! ly_install_target_override: Mac specific target installation function(ly_install_target_override) @@ -70,33 +77,62 @@ function(ly_install_target_override) COMPONENT ${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME} ) + set(install_relative_binaries_path "${ly_platform_install_target_RUNTIME_DIR}/${ly_platform_install_target_RUNTIME_SUBDIR}") + if (${is_bundle}) set_property(TARGET ${ly_platform_install_target_TARGET} PROPERTY RESOURCE ${cached_resources_dir}) + set(runtime_output_filename "$.app") + else() + set(runtime_output_filename "$") + endif() + + get_target_property(target_type ${ly_platform_install_target_TARGET} TYPE) + if(target_type IN_LIST LY_TARGET_TYPES_WITH_RUNTIME_OUTPUTS) + get_target_property(entitlement_file ${ly_platform_install_target_TARGET} ENTITLEMENT_FILE_PATH) + if (NOT entitlement_file) + set(entitlement_file "none") + endif() + + ly_file_read(${LY_ROOT_FOLDER}/cmake/Platform/Mac/runtime_install_mac.cmake.in template_file) + string(CONFIGURE "${template_file}" configured_template_file @ONLY) + file(GENERATE + OUTPUT ${CMAKE_BINARY_DIR}/runtime_install/$/${ly_platform_install_target_TARGET}.cmake + CONTENT "${configured_template_file}" + ) endif() -endfunction() - -#! ly_install_add_install_path_setreg: Adds the install path setreg file as a dependency -function(ly_install_add_install_path_setreg NAME) - set_property(TARGET ${NAME} APPEND PROPERTY INTERFACE_LY_TARGET_FILES "${installed_binaries_setreg_path}\nRegistry") endfunction() #! ly_install_code_function_override: Mac specific copy function to handle frameworks function(ly_install_code_function_override) - install(CODE -"function(ly_copy source_file target_directory) - if(\"\${source_file}\" MATCHES \"\\\\.[Ff]ramework[^\\\\.]\") - - # fixup origin to copy the whole Framework folder - string(REGEX REPLACE \"(.*\\\\.[Ff]ramework).*\" \"\\\\1\" source_file \"\${source_file}\") - get_filename_component(target_filename \"\${source_file}\" NAME) - - endif() - file(COPY \"\${source_file}\" DESTINATION \"\${target_directory}\" FILE_PERMISSIONS ${LY_COPY_PERMISSIONS}) -endfunction()" - COMPONENT ${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME} - ) + configure_file(${LY_ROOT_FOLDER}/cmake/Platform/Mac/InstallUtils_mac.cmake.in ${CMAKE_BINARY_DIR}/runtime_install/InstallUtils_mac.cmake @ONLY) + ly_install_run_script(${CMAKE_BINARY_DIR}/runtime_install/InstallUtils_mac.cmake) + +endfunction() + +#! ly_post_install_steps: Any additional platform specific post install steps +function(ly_post_install_steps) + + # On Mac, after CMake is done installing, the code signatures on all our built binaries will be invalid. + # We need to now codesign each dynamic library, executable, and app bundle. It's specific to each target + # because there could potentially be different entitlements for different targets. + get_property(all_targets GLOBAL PROPERTY LY_ALL_TARGETS) + foreach(alias_target IN LISTS all_targets) + ly_de_alias_target(${alias_target} target) + # Exclude targets that dont produce runtime outputs + get_target_property(target_type ${target} TYPE) + if(NOT target_type IN_LIST LY_TARGET_TYPES_WITH_RUNTIME_OUTPUTS) + continue() + endif() + + ly_install_run_script(${CMAKE_BINARY_DIR}/runtime_install/$/${target}.cmake) + endforeach() + + ly_install_run_code(" + ly_download_and_codesign_sdk_python() + ly_codesign_sdk() + set(CMAKE_INSTALL_PREFIX ${LY_INSTALL_PATH_ORIGINAL}) + ") endfunction() -include(cmake/Platform/Common/Install_common.cmake) diff --git a/cmake/Platform/Mac/LYWrappers_mac.cmake b/cmake/Platform/Mac/LYWrappers_mac.cmake index 578f6fe041..245d3bb7c1 100644 --- a/cmake/Platform/Mac/LYWrappers_mac.cmake +++ b/cmake/Platform/Mac/LYWrappers_mac.cmake @@ -6,6 +6,16 @@ # # +set(LY_ENABLE_HARDENED_RUNTIME OFF CACHE BOOL "Enable hardened runtime capability for Mac builds. This should be ON when building the engine for notarization/distribution.") + +define_property(TARGET PROPERTY ENTITLEMENT_FILE_PATH + BRIEF_DOCS "Path to the entitlement file" + FULL_DOCS [[ + On MacOS, entitlements are used to grant certain privileges + to applications at runtime. Use this propery to specify the + path to a .plist file containing entitlements. + ]] +) function(ly_apply_platform_properties target) @@ -14,6 +24,18 @@ function(ly_apply_platform_properties target) INSTALL_RPATH "@executable_path/;@executable_path/../Frameworks" ) + get_property(is_imported TARGET ${target} PROPERTY IMPORTED) + if((NOT is_imported) AND (LY_ENABLE_HARDENED_RUNTIME)) + get_property(target_type TARGET ${target} PROPERTY TYPE) + set(runtime_types_list "MODULE_LIBRARY" "SHARED_LIBRARY" "EXECUTABLE") + if (target_type IN_LIST runtime_types_list) + set_target_properties(${target} PROPERTIES + XCODE_ATTRIBUTE_ENABLE_HARDENED_RUNTIME YES + XCODE_ATTRIBUTE_CODE_SIGN_INJECT_BASE_ENTITLEMENTS NO + ) + endif() + endif() + endfunction() diff --git a/cmake/Platform/Mac/PAL_mac.cmake b/cmake/Platform/Mac/PAL_mac.cmake index b415daf44a..065b35d69c 100644 --- a/cmake/Platform/Mac/PAL_mac.cmake +++ b/cmake/Platform/Mac/PAL_mac.cmake @@ -39,3 +39,6 @@ set(LY_ASSET_DEPLOY_ASSET_TYPE "mac" CACHE STRING "Set the asset type for deploy # Set the python cmd tool ly_set(LY_PYTHON_CMD ${CMAKE_CURRENT_SOURCE_DIR}/python/python.sh) + +# Only x86_64 is currently supported on Mac +ly_set(CMAKE_OSX_ARCHITECTURES "x86_64") diff --git a/cmake/Platform/Mac/PreInstallSteps_mac.cmake.in b/cmake/Platform/Mac/PreInstallSteps_mac.cmake.in new file mode 100644 index 0000000000..5033aadd6e --- /dev/null +++ b/cmake/Platform/Mac/PreInstallSteps_mac.cmake.in @@ -0,0 +1,39 @@ +# +# 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 +# +# + +cmake_minimum_required(VERSION 3.20) + +# The O3DE SDK will be shipped as an app bundle. So we create an O3DE_SDK.app directory +# and install SDK into the app's Contents/Engine directory. +set(LY_INSTALL_PATH_ORIGINAL ${CMAKE_INSTALL_PREFIX}) + +file(INSTALL @LY_ROOT_FOLDER@/Code/Tools/BundleLauncher/info.plist + DESTINATION ${CMAKE_INSTALL_PREFIX}/O3DE_SDK.app/Contents +) + +# This SDK launcher will install python site-packages and then launch the ProjectManager +# when a user double clicks on the SDK from Finder. We're only going to need one version +# of the SDK launcher regardless of what configs of the engine are installed. +if (EXISTS @CMAKE_BINARY_DIR@/bin/profile/O3DE_SDK) + set(sdk_launcher_config profile) +elseif (EXISTS @CMAKE_BINARY_DIR@/bin/debug/O3DE_SDK) + set(sdk_launcher_config debug) +elseif (EXISTS @CMAKE_BINARY_DIR@/bin/release/O3DE_SDK) + set(sdk_launcher_config release) +endif() +file(INSTALL @CMAKE_BINARY_DIR@/bin/${sdk_launcher_config}/O3DE_SDK + DESTINATION ${CMAKE_INSTALL_PREFIX}/O3DE_SDK.app/Contents/MacOS + USE_SOURCE_PERMISSIONS +) +file(INSTALL @CMAKE_BINARY_DIR@/runtime_install/${sdk_launcher_config}/BinariesInstallPath.setreg + DESTINATION ${CMAKE_INSTALL_PREFIX}/O3DE_SDK.app/Contents/MacOS/Registry +) + +# We need to update the CMAKE_INSTALL_PREFIX so that the engine is installed inside the app bundle. +file(MAKE_DIRECTORY ${CMAKE_INSTALL_PREFIX}/O3DE_SDK.app/Contents/Engine) +set(CMAKE_INSTALL_PREFIX ${CMAKE_INSTALL_PREFIX}/O3DE_SDK.app/Contents/Engine) \ No newline at end of file diff --git a/cmake/Platform/Mac/runtime_dependencies_mac.cmake.in b/cmake/Platform/Mac/runtime_dependencies_mac.cmake.in index d65578f82a..11551f608f 100644 --- a/cmake/Platform/Mac/runtime_dependencies_mac.cmake.in +++ b/cmake/Platform/Mac/runtime_dependencies_mac.cmake.in @@ -73,8 +73,8 @@ function(ly_copy source_file target_directory) return() endif() - # fixup the destination so it ends up in Contents/Plugins - string(REGEX REPLACE "(.*\\.app/Contents)/MacOS" "\\1/plugins" target_directory "${target_directory}") + # fixup the destination so it ends up in Contents/PlugIns + string(REGEX REPLACE "(.*\\.app/Contents)/MacOS" "\\1/PlugIns" target_directory "${target_directory}") set(local_plugin_dirs ${plugin_dirs}) list(APPEND local_plugin_dirs "${target_directory}") @@ -212,7 +212,6 @@ if(@target_file_dir@ MATCHES ".app/Contents/MacOS") file(REMOVE_RECURSE ${remove_file_list}) endif() - endif() else() # Non-bundle case diff --git a/cmake/Platform/Mac/runtime_install_mac.cmake.in b/cmake/Platform/Mac/runtime_install_mac.cmake.in new file mode 100644 index 0000000000..65b1ede77b --- /dev/null +++ b/cmake/Platform/Mac/runtime_install_mac.cmake.in @@ -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 +# +# + +cmake_path(SET file_path "${CMAKE_INSTALL_PREFIX}/@install_relative_binaries_path@/@runtime_output_filename@") +cmake_path(GET file_path EXTENSION LAST_ONLY file_ext) + +if(file_ext STREQUAL .app) + + file(INSTALL @CMAKE_BINARY_DIR@/runtime_install/$/BinariesInstallPath.setreg + DESTINATION ${file_path}/Contents/MacOS/Registry + ) + + if(EXISTS "${file_path}/Contents/Frameworks/Python.framework") + codesign_python_framework_binaries("${file_path}/Contents/Frameworks/Python.framework") + endif() + +else() + + find_program(LY_INSTALL_NAME_TOOL install_name_tool) + if (NOT LY_INSTALL_NAME_TOOL) + message(FATAL_ERROR "Unable to locate 'install_name_tool'") + endif() + + execute_process(COMMAND + ${LY_INSTALL_NAME_TOOL} -add_rpath @loader_path ${file_path}) + +endif() + +codesign_file("${file_path}" "@entitlement_file@") diff --git a/python/Platform/Mac/PythonEntitlements.plist b/python/Platform/Mac/PythonEntitlements.plist new file mode 100644 index 0000000000..ed4892befa --- /dev/null +++ b/python/Platform/Mac/PythonEntitlements.plist @@ -0,0 +1,10 @@ + + + + + com.apple.security.cs.disable-library-validation + + com.apple.security.cs.allow-unsigned-executable-memory + + + diff --git a/python/get_python.sh b/python/get_python.sh index 248a0790fc..e8d35a311e 100755 --- a/python/get_python.sh +++ b/python/get_python.sh @@ -30,7 +30,8 @@ cd $DIR python_exitcode=$? if [ $python_exitcode == 0 ]; then echo get_python.sh: Python is already downloaded: $(./python.sh --version) - $DIR/pip.sh install -r $DIR/requirements.txt --quiet --disable-pip-version-check + $DIR/pip.sh install -r $DIR/requirements.txt --disable-pip-version-check --no-warn-script-location + $DIR/pip.sh install -e $DIR/../scripts/o3de --no-deps --disable-pip-version-check --no-warn-script-location exit 0 fi if [[ "$OSTYPE" = *"darwin"* ]]; @@ -73,5 +74,6 @@ if [ $retVal -ne 0 ]; then fi echo installing via pip... -$DIR/pip.sh install -r $DIR/requirements.txt --disable-pip-version-check +$DIR/pip.sh install -r $DIR/requirements.txt --disable-pip-version-check --no-warn-script-location +$DIR/pip.sh install -e $DIR/../scripts/o3de --no-deps --disable-pip-version-check --no-warn-script-location exit $? diff --git a/scripts/build/build_node/Platform/Linux/package-list.ubuntu-focal.txt b/scripts/build/build_node/Platform/Linux/package-list.ubuntu-focal.txt index 21c4755a2e..2cbbe58b38 100644 --- a/scripts/build/build_node/Platform/Linux/package-list.ubuntu-focal.txt +++ b/scripts/build/build_node/Platform/Linux/package-list.ubuntu-focal.txt @@ -13,6 +13,8 @@ libxcb-xinput0 # For Qt plugins at runtime libfontconfig1-dev # For Qt plugins at runtime libcurl4-openssl-dev # For HttpRequestor libsdl2-dev # for WWise/Audio -libxkbcommon-dev +libxcb-xkb-dev # For xcb keyboard input +libxkbcommon-x11-dev # For xcb keyboard input +libxkbcommon-dev # For xcb keyboard input zlib1g-dev mesa-common-dev